RAG 离线索引阶段的去重
在离线索引阶段进行去重非常关键,因为重复或近似重复的文本块会降低检索质量:它们会使结果充斥冗余内容,扭曲相关性评分,并浪费向量数据库的存储容量。
重复产生的原因
- 文档更新后重新导入(只有部分文本块发生变化)
- 重叠的分块策略(如高重叠的滑动窗口)
- 多个数据源管道写入同一向量数据库
- 文档间的镜像内容(模板、免责声明、共享章节等)
去重策略
1. 精确去重(嵌入前)
在调用嵌入模型之前进行 —— 成本最低。
哈希指纹法示例:
python
import hashlib
def chunk_fingerprint(text: str) -> str:
normalized = " ".join(text.lower().split()) # 标准化空格和大小写
return hashlib.sha256(normalized.encode()).hexdigest()
# 插入时:
if fingerprint in seen_hashes:
skip_chunk()
else:
seen_hashes.add(fingerprint)
embed_and_insert(chunk)可以将哈希作为 元数据字段 存入向量数据库,以便无需全量扫描即可查询。
2. 近似重复检测(嵌入前或嵌入后)
用于语义相同但表面略有差异的文本块(空格、标点、版本差异等)。
MinHash / LSH(嵌入前)示例:
python
from datasketch import MinHash, MinHashLSH
lsh = MinHashLSH(threshold=0.85, num_perm=128)
def get_minhash(text):
m = MinHash(num_perm=128)
for word in text.lower().split():
m.update(word.encode())
return m
# 插入时:
m = get_minhash(chunk_text)
if lsh.query(m): # 找到近似重复
skip_or_merge(chunk)
else:
lsh.insert(chunk_id, m)
embed_and_insert(chunk)基于嵌入向量的余弦相似度(嵌入后):
python
# 插入前查询向量数据库的最近邻
results = vector_db.query(embedding, top_k=3)
if results and results[0].score > 0.97: # 阈值可调
skip_chunk()
else:
vector_db.insert(chunk_id, embedding, metadata)⚠️ 在大规模场景下成本较高 —— 仅在 LSH 预筛选不足时使用。
3. 文档级版本管理 & Upsert
在重新导入更新文档时,避免累积过时的文本块:
python
# 给每个文本块打上文档 ID + 版本/哈希
metadata = {
"doc_id": "policy_v3.pdf",
"doc_hash": document_hash,
"chunk_index": i,
}
# 重新导入时:先删除该文档旧的文本块,再插入新块
vector_db.delete(filter={"doc_id": "policy_v3.pdf"})
vector_db.insert_batch(new_chunks)相比直接更新/Upsert,这种方式更优,因为不同版本的文本块数量和边界可能不同。
4. 文本块级去重策略
并非所有重复都应删除 —— 有时 来源信息很重要:
| 情况 | 推荐操作 |
|---|---|
| 完全相同,来源相同 | 丢弃 —— 纯重复 |
| 完全相同,来源不同 | 保留一份,合并来源元数据 |
| 近似重复,语境不同 | 保留两份,标注相似度 |
| 模板内容(页脚、免责声明等) | 全局丢弃,通过黑名单过滤 |
5. 模板/结构噪声移除
在文本块到达去重阶段前先剔除重复、无信息量的内容:
python
BOILERPLATE_PATTERNS = [
r"page \d+ of \d+",
r"confidential – do not distribute",
r"all rights reserved",
]
def is_boilerplate(text: str) -> bool:
text_lower = text.lower().strip()
if len(text_lower) < 40: # 太短无意义
return True
return any(re.search(p, text_lower) for p in BOILERPLATE_PATTERNS)推荐管道架构
原始文档
│
▼
[分块] ──► [模板过滤]
│
▼
[精确哈希检查] ── 重复 → 跳过
│ 唯一
▼
[MinHash/LSH 检查] ── 近似重复 → 跳过或合并
│ 唯一
▼
[嵌入模型]
│
▼
[可选:高价值内容余弦相似度检查]
│
▼
[向量数据库插入并存储元数据]必须存储的关键元数据字段
json
{
"chunk_id": "uuid-v4",
"doc_id": "source-document-identifier",
"doc_hash": "sha256 of full document",
"chunk_hash": "sha256 of normalized chunk text",
"chunk_index": 2,
"ingested_at": "2026-05-07T10:00:00Z",
"source_path": "s3://bucket/policy_v3.pdf"
}这些元数据可支持 选择性重建索引、审计跟踪,以及在检索阶段进行运行时去重检查。
阈值调优建议
| 信号 | 保守(低噪声) | 激进(高去重) |
|---|---|---|
| 余弦相似度阈值 | 0.99 | 0.93 |
| MinHash Jaccard 阈值 | 0.90 | 0.80 |
| 最小文本块长度(字符数) | 100 | 200 |
建议先采用保守策略,根据检索质量指标(MRR、nDCG)在保留集上进行调优。