2699 字
约 8 分钟
17
S20-A18 · 检索的多样性:为什么前十名都是同一份文档

用户搜"退货流程",返回的十条结果全部来自同一份《售后手册》的不同段落——内容都对,但用户仍然找不到"该点哪里提交"的答案。因为同一文档的多个片段挤占了全部名额,而答案可能在另一份《操作指引》里。

检索系统的默认行为是"按相关度排序取前 N",而它在"单文档多片段"的语料上会退化成"只返回一份文档"。这个区分的价值在于:调分数改的是"谁排第一",去重改的是"有多少不同来源能出现"。前者无论怎么调,都无法让被挤掉的文档进入前十。
解法不是调分数,而是在取结果前做去重与打散。


1. 三种"数量够但多样性不够"

现象 表现
单文档垄断 前 10 条来自 1 份文档
单章节垄断 前 10 条来自 1 个章节的相邻段落
单版本垄断 新旧两版制度都返回,且新版排在后面

第一项最常见,尤其在"长文档切分很细"的语料里:一份 50 页手册被切成 200 个片段,于是任何相关查询都会被它占满。

第三项在"制度更新"时出现:旧版片段因为历史点击多而分数更高,于是旧制度排在前面。第三项还有个更隐蔽的版本:两版内容高度相似,于是"旧版排在前面"而用户完全看不出差别——直到他按旧流程办事失败。

2. 四种打散手段

手段 效果
按文档去重(每文档最多 N 条) 最直接
MMR(最大边际相关性) 兼顾相关与差异
按来源分组轮询 保证来源覆盖
分数加"重复惩罚" 平滑,实现简单

第一项的上限值有个取舍:per_doc=1 会让"同一文档的多角度内容"只出现一段,而 per_doc=3 又可能让两份文档占满六个名额。常见折中是 2,且"允许在名额不足时放宽"。
第一项最实用:限制"每个文档最多 2–3 条",其余名额留给其它文档。它不需要改打分,只是在取 Top-K 时做约束。

第二项更优雅但成本高:每次选下一个结果时,同时考虑"与查询的相关度"与"与已选结果的相似度"。第二项的代价在"两两相似度":选 10 条要做几十次比较,而每次比较都要算向量相似度。所以在"召回几百条"的场景里,通常是"先粗排再对前 50 条做 MMR"。
它适合"摘要类"查询。

// 按文档去重的取数逻辑
def diversify(hits, per_doc=2, k=10):
    seen, out = {}, []
    for h in hits:                       # hits 已按分数降序
        n = seen.get(h.doc_id, 0)
        if n >= per_doc: continue
        seen[h.doc_id] = n + 1
        out.append(h)
        if len(out) == k: break
    return out
// 兜底:若去重后不足 k 条,再按分数补足(并标注"同文档")

3. 去重与"相邻片段"

问题 说明
相邻片段内容重叠 切分带 overlap 时
同一段落被切成两半 边界效应
重复文档(多份副本) 语料导入问题

第一项是"切分重叠"的副作用:overlap 200 字的两个片段,前 200 字完全相同,于是"两条结果"实际是一条。

第三项的现象很迷惑:两条结果标题一样、内容一样,但 ID 不同,于是"看起来是重复 bug 但删不掉"(因为按 ID 删了两个中的一个,另一个还在)。
第三项在"多来源导入"时常见:同一份文件被导了两次(不同批次),于是索引里有两份。去重应当按"内容指纹"而不是"路径"。

// 三层去重的顺序(先粗后细)
1 内容指纹(文档级):同一文件只留一份
2 片段指纹(chunk 级):完全相同的片段丢弃
3 重叠检测(可选):相似度 > 0.9 的相邻片段合并展示
// 只做第 3 步会让"两份重复文档"仍然占满名额

4. 打散与"排序质量"的冲突

情形 是否该打散
查询很具体(精确短语) 不该(准确优先)
查询较泛("怎么退货") 该(覆盖优先)
只有一份文档相关 打散无意义
有多份文档相关 该

第一项的关键:当查询是"精确短语"时,用户要的就是那一段,打散反而让他多翻几页。第一项在"错误码/订单号"这类查询上尤其明显:返回十条不同文档的"包含该编号的片段",全是噪声——用户要的是唯一那一条。
所以"是否打散"应当由查询类型决定,而不是全局开关。

判据可以很简:Top-1 的分数显著高于 Top-2(如高 30% 以上)时,不打散;分数接近时,打散。

// 按分数分布决定是否打散
max1 = 0.92, max2 = 0.91, max3 = 0.90  → 分数接近,打散(可能有多份都相关)
max1 = 0.95, max2 = 0.62, max3 = 0.60  → Top-1 显著领先,不打散

"打散"与"重排"的先后顺序会影响结果:先重排再打散,能让"更相关的文档"获得名额;先打散再重排,则名额被原始分数决定,可能把不相关文档塞进来。

// 两种顺序的差别
先重排后打散:Top50(重排)→ 每文档取 2 → 前 10   ← 推荐
先打散后重排:Top50(原始分)→ 每文档取 2 → 重排 → 前 10
// 后者的问题:名额在"粗排分数"上分配,而粗排分数噪声大

5. 多版本与时效

手段 效果
版本号进排序 新版优先
时效衰减 旧文档降权
显式标注版本 用户自行判断
过滤已失效文档 最干净

第四项的前提是"有失效标记":文档需要 status 字段(active/superseded/expired),而这需要在入库时维护。没有这个字段,"过滤"无从下手。
第四项最彻底:把"已失效"的文档从索引里移出(或加过滤条件),而不是靠排序把它压下去。因为"压下去"仍会偶尔出现。

第三项在"制度类"知识库里很实用:结果里标注"2024 版",让用户知道自己在看哪一版——即使排序有偏差,用户也能判断。

监控上要能看出"垄断",否则这个问题不会自己暴露:统计"前十的独立文档数"与"单文档占比"。健康值通常在 3–8 之间;长期为 1,说明该做去重了。

// 两个观测指标(按查询批次聚合)
distinct_docs_top10:均值 1.2(异常,垄断)→ 3–6(健康)
top1_share:单一文档占前十的比例,> 0.8 需处理
// 用法:按查询类型分组看,而不是看整体均值

6. 排错:五种"结果都是同一份"

现象 成因 处理方向
前 10 条同一文档 未做文档级去重 加 per-doc 上限
两条结果内容几乎一样 overlap 过大 减小 overlap 或合并
旧制度排在前面 无时效权重 加版本/时效
打散后质量下降 对精确查询也打散 按分数分布判断
去重后结果不足 10 条 语料太少 补内容或放宽

第五项提醒一件事:打散不能凭空造出结果。第五项还有一个"诚实"要求:当结果不足时应当说明"仅找到 N 条",而不是把不相关的内容补进来凑数。后者会让用户以为"系统觉得这些也相关",从而对系统整体失去信任。
如果语料里只有 2 份相关文档,去重后就是 4–6 条,此时应当"如实返回"而不是"补无关结果"。

最后一条"用户视角"的边界:多样性对不同用户价值不同。做研究的用户希望"一次看到所有相关来源",而做操作的员工希望"直接给我那一步"。所以同一个知识库,面向两类用户可能需要两套结果——而不是一个折中值。

// 两种模式的默认策略
研究模式:per_doc=3,打散开,展示来源与版本
操作模式:per_doc=1,不打散,直接给"该点哪里"
// 实现方式:按入口(知识库 / 帮助中心)区分,而不是让用户手动切

7. 边界:什么情况不必做打散

场景 为什么 做法
语料只有一份权威文档 无多样性可言 不去重
精确检索(编号、错误码) 要的就是那一条 不打散
结果数量本来就少(< 5) 打散无意义 不处理
用户明确"看某一文档" 已限定范围 不处理

最后一条与"评测"有关的边界:多样性也要被评测,否则"前十条同一文档"永远发现不了。加一个指标"前十的独立文档数"(越大越好,但不必追高),就能持续观察。
判断口径一句话:用户的问题"能被一份文档完整回答"吗。能,多样性就是噪声;不能(需要多个来源拼起来),打散才必要。


8. 自测

  1. 三种"数量够但多样性不够"里,哪一种最常见?为什么"长文档切分细"会加剧它?
  2. 四种打散手段里,哪一种最实用、成本最低?MMR 适合什么场景?
  3. 为什么"按内容指纹去重"比"按路径去重"更可靠?三层去重的顺序为什么是"先粗后细"?
  4. 什么情况下不该打散?"Top-1 显著领先"这个判据的原理是什么?
  5. "过滤已失效文档"为什么比"排序压下去"更干净?
  6. 判断"是否需要打散"的那个问题是什么?
S20-A18 · 检索的多样性:为什么前十名都是同一份文档
https://www.clxhxhhr.top/posts/6534/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。