混合检索与重排序:BM25 与向量融合、RRF 与重排模型

系统讲解 Elasticsearch 的混合检索与重排序:BM25 词法检索与 kNN 向量检索的互补性、RRF 倒数排名融合与加权线性组合、cross-encoder 重排模型接入,以及 nDCG、recall 等检索质量评估方法。

纯 BM25 检索在关键词精确匹配上表现稳定,却对同义改写、口语化表达束手无策;纯向量检索擅长语义泛化,却容易在专有名词、订单号、错误码这类「必须精确命中」的场景翻车。生产环境的检索系统几乎都不是二选一,而是把两路召回结果融合起来,再用更贵的模型做一次精排。本文讲清混合检索的融合策略、RRF 的数学形式、rerank 模型的接入方式,以及如何用离线指标衡量「检索是不是真的变好了」。

1. 为什么需要混合检索

一句话总结: 词法检索与语义检索的失败模式互补,混合检索用两路召回覆盖彼此的盲区,是当前检索质量性价比最高的手段。

1.1 两种检索的盲区

BM25 基于词频与逆文档频率打分,本质是「词是否出现」。它对拼写变体、同义词、跨语言表达无能为力,用户搜「怎么退钱」时匹配不到标题写着「退款流程」的文档。向量检索把文本编码成稠密向量后计算余弦相似度,能跨越字面差异捕捉语义,但它的弱点同样明显:对高频专有名词不敏感,把「iPhone 15」和「iPhone 14」编码得很近,也几乎无法保证「错误码 ES-5031」这类长尾精确匹配。

1.2 混合检索的价值

把两路结果融合后,召回率通常比任一单路高出一截。经验数据是:在电商、问答、日志检索等场景中,混合检索的 recall@10 相比纯 BM25 提升 10% 到 30%,具体幅度取决于查询里「语义模糊」与「精确匹配」的比例。代价是查询延迟上升、架构变复杂,需要额外维护向量字段与融合逻辑。

1.3 融合的两个时机

  • 召回后融合:两路各自取 top-N,再按某种规则合并成最终列表。RRF 与加权线性组合都属于这一类,实现简单、可解释。
  • 精排融合:两路只负责扩大召回,真正的排序交给 cross-encoder 重排模型,模型同时看到查询与文档全文,质量最高但成本也最高。

实践中常见「三段式」:BM25 + kNN 双路召回各取 50 到 100 条,RRF 融合成候选集,最后对 top-20 做 rerank。

2. BM25 与向量检索的互补

一句话总结: BM25 是稀疏词袋打分、可解释且零训练成本,向量检索是稠密语义打分、泛化强但需模型支撑,二者在打分尺度上不可直接比较。

2.1 BM25 的打分机制

BM25 的打分由词频饱和、文档长度归一化和 IDF 三部分构成,公式中的 k1 与 b 控制饱和速度与长度惩罚。它的分数是无界的正实数,受索引统计信息影响,同一个查询在不同索引上的分数不可比。这也意味着 BM25 分数天然适合排序,但不适合当作跨查询的「相关度阈值」。

GET /articles/_search
{
  "query": {
    "match": {
      "title": {
        "query": "退款流程",
        "boost": 2.0
      }
    }
  }
}

2.2 向量检索的打分机制

kNN 检索返回的是相似度,Elasticsearch 内部统一转换为 _score:余弦相似度会被映射到 0 到 1 附近((1 + cosine) / 2),点积则直接使用。与 BM25 不同,向量分数有明确的上下界,语义相近程度可直接比较,这让「分数阈值过滤」在向量路可行。

2.3 尺度不可比问题

BM25 分数可能是 3.2、17.5、42.0,向量分数在 0.6 到 0.95 之间。直接把两路分数相加会被 BM25 的量级支配,融合完全失效。这正是 RRF 存在的理由:它只使用排名,不使用原始分数,从根本上绕开了尺度问题。

2.4 参数与字段权重

BM25 的 k1 与 b 可以在映射里按字段调整:标题这类短字段可以调低 b(减弱长度归一化),长正文字段保持默认 0.75。字段级 boost 则控制不同字段的贡献比重,标题命中通常给 2 到 3 倍权重:

PUT /articles/_mapping
{
  "properties": {
    "title": {
      "type": "text",
      "similarity": "custom_bm25"
    }
  }
}
PUT /articles/_settings
{
  "index": {
    "similarity": {
      "custom_bm25": { "type": "BM25", "k1": 1.5, "b": 0.5 }
    }
  }
}

这些参数属于「调一次、长期有效」的配置,改完需要重建索引或做 reindex 才能生效。

3. RRF 倒数排名融合

一句话总结: RRF 用 1 / (k + rank) 把每路的排名换算成分数再相加,只依赖名次不依赖分数尺度,是混合检索最稳健的默认融合方式。

3.1 RRF 的数学形式

对每一路召回结果,第 r 名文档贡献 1 / (k + r) 分,其中 k 是平滑常数,默认 60。文档的最终得分是所有路贡献之和。排名靠前的文档贡献急剧上升,但因为有 k 的平滑,单路排名第一不会压倒性支配。

score(d) = Σ_over_retrievers  1 / (k + rank_r(d))

k 越大,融合越平缓,各路的话语权越接近;k 越小,头部排名越重要。

3.2 在 Elasticsearch 中使用 RRF

8.8 之后提供了内置的 rrf 检索器,把多个子检索的结果融合:

GET /articles/_search
{
  "retriever": {
    "rrf": {
      "retrievers": [
        { "standard": { "query": { "match": { "title": "退款流程" } } } },
        { "knn": { "field": "title_vector", "query_vector": [0.12, -0.44], "k": 50, "num_candidates": 200 } }
      ],
      "rank_constant": 60,
      "rank_window_size": 50
    }
  }
}

rank_window_size 决定每路取多少条参与融合,rank_constant 就是公式里的 k。结果里的 _score 是 RRF 分数,量级很小(通常 0.01 到 0.03),不要与 BM25 分数混用。

3.3 加权 RRF

内置检索器支持给每路配权重,让 BM25 或向量路占更大话语权:

{
  "retriever": {
    "rrf": {
      "retrievers": [
        { "retriever": { "standard": { "query": { "match": { "title": "退款流程" } } } }, "weight": 1.5 },
        { "retriever": { "knn": { "field": "title_vector", "query_vector": [0.12, -0.44], "k": 50 } }, "weight": 1.0 }
      ]
    }
  }
}

权重适合表达先验:如果业务里精确匹配更可信,就调高 BM25 路的权重。

3.4 RRF 的局限

RRF 丢弃了原始分数信息。如果某一路的分数分布本身携带强信号(比如向量相似度 0.95 与 0.62 的差距很关键),RRF 会把它压缩成相邻的名次,反而损失信息。对这类场景,可以考虑先做分数归一化再线性加权,或者在 RRF 之后接 rerank 模型补回精度。

4. 重排序模型

一句话总结: rerank 用 cross-encoder 对「查询 + 文档」整体打分,精度显著高于双塔召回,但计算量与候选数成正比,只应作用在小候选集上。

4.1 双塔与 cross-encoder 的区别

召回阶段的向量模型是双塔结构:查询和文档分别编码,可离线预计算文档向量,线上只算查询向量再做大范围近似最近邻。代价是两者在编码时互相看不见。cross-encoder 把查询与文档拼在一起送入模型,注意力机制让两者充分交互,打分精度高得多,但每个候选都要跑一次前向,无法预计算。

4.2 接入方式

Elasticsearch 本身不内置通用 rerank 模型,主流做法是把 top-N 结果取出后调用外部推理服务:

def rerank(query, hits, model, top_k=10):
    pairs = [(query, h["_source"]["title"] + " " + h["_source"]["body"]) for h in hits]
    scores = model.predict(pairs)
    ranked = sorted(zip(hits, scores), key=lambda x: -x[1])
    return [h for h, _ in ranked[:top_k]]

推理服务可以是本地的 ONNX Runtime,也可以是托管的 rerank API。关键是控制候选数:20 到 50 条比较合适,超过 100 条延迟会明显上升。

4.3 延迟与成本权衡

以常见的 cross-encoder 模型为例,单条打分在 GPU 上约几毫秒,CPU 上可能到几十毫秒。50 条候选意味着 50 次前向,必须做批处理(batch inference)才能压住延迟。工程上常用「两阶段截断」:RRF 融合后取 top-30 送 rerank,rerank 只重排这 30 条,最终返回 top-10。

4.4 用学习排序替代

如果已有用户点击日志,可以用 LTR(learning to rank)训练排序模型,把 BM25 分数、向量分数、文档质量特征一起喂给模型。相比固定权重的 RRF,LTR 能学到更贴合业务的排序,但需要标注数据与特征工程,冷启动阶段建议先用 RRF 打底。

4.5 模型选型

rerank 模型的选择要看三个维度:语言支持(中英文混合场景需要多语模型)、输入长度(长文档需要截断或分段)、推理成本(参数量与硬件匹配)。通用多语模型在多数场景已经够用,垂直领域(法律、医疗、代码)则建议在自有数据上做微调,微调后的 nDCG 提升通常比换更大的通用模型更明显。

5. 检索质量评估

一句话总结: 没有离线指标就无法判断融合是否真的有效,nDCG、recall@k、MRR 是检索评估的三个基本指标,必须在固定测试集上对比。

5.1 三个核心指标

  • recall@k:前 k 条里是否包含全部相关文档,衡量召回能力,混合检索的主要收益点。
  • MRR:第一条相关文档排名的倒数均值,衡量「用户多快看到第一个好结果」。
  • nDCG@k:考虑相关度等级与位置折扣的排序质量指标,是最贴近真实体验的综合指标。

5.2 构建评估集

评估集由「查询 + 相关文档标注」组成。没有人工标注时,可以用点击日志反推:被点击且停留时间长的文档视为相关。评估集要覆盖不同查询类型——精确匹配型、语义模糊型、长尾型,否则指标会被某一类查询主导。

5.3 离线对比

固定评估集后,对比几种配置的 nDCG@10:

配置recall@10nDCG@10p95 延迟
纯 BM250.620.5112ms
纯 kNN0.580.4735ms
RRF 融合0.790.5848ms
RRF 加 rerank0.790.71210ms

融合带来召回提升,rerank 带来排序提升,延迟代价清晰可见。是否上 rerank,取决于业务对延迟的容忍度。

5.4 线上验证

离线指标提升不等于线上收益。上线时用 A/B 实验,观察点击率、转化率、零结果率。特别注意长尾查询的表现,融合策略有时会牺牲长尾精确匹配换取整体语义提升。

5.5 评估集的维护

评估集会随业务漂移:新品上架、文档更新、用户表达方式变化,都会让旧的标注逐渐失真。建议每季度抽样一批新查询补标注,并保留一份「黄金集」长期不动用于跨版本对比。评估集本身也要纳入版本管理,否则指标变化无法归因。

6. 工程实现要点

一句话总结: 混合检索的实现难点在向量字段的维护与双路查询的并行化,映射设计、向量生成管道、查询路由都需要提前规划。

6.1 映射设计

向量字段用 dense_vector,需要指定维度与相似度函数:

PUT /articles
{
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "body": { "type": "text" },
      "title_vector": { "type": "dense_vector", "dims": 768, "index": true, "similarity": "cosine" }
    }
  }
}

index: true 是开启 kNN 检索的前提,相似度函数一旦写入不可更改,选型要慎重。

6.2 向量生成管道

文档写入时要同步生成向量。常见做法是在 ingest pipeline 里调用推理处理器,或在应用层调用 embedding 服务后一起写入。关键是一致性:查询向量与文档向量必须来自同一个模型、同一版本,模型升级时要重建全量向量。

6.3 查询并行化

双路查询在 Elasticsearch 内部由 retriever 框架并行调度,无需应用层自己发两次请求。但如果把 rerank 放在外部服务,应用层就要串行「检索 → 重排」两跳,这两跳的网络往返是延迟的主要来源,应尽量让推理服务与 ES 集群同机房。

6.4 分片与资源

kNN 检索对内存和 CPU 敏感,向量字段的图索引常驻堆外内存。混合检索的集群建议给搜索线程池留足余量,并把向量路与词法路的负载在监控上分开看,否则延迟劣化时难以定位是哪一路变慢。

6.5 监控与告警

需要重点盯的指标有三类:各路的召回条数(某一路长期返回空说明配置或数据有问题)、rerank 服务的 p99 延迟与失败率(外部依赖最容易抖动)、以及零结果率(融合逻辑写错时最直观的信号)。把这些指标与检索请求量放在同一张看板上,才能快速判断劣化来自流量、数据还是模型。

7. 生产实践与调优

一句话总结: 混合检索调优的核心是「每路取多少条、融合用什么策略、要不要 rerank」,这些参数应在真实查询分布上调,而不是拍脑袋。

7.1 候选数量的调优

每路召回条数太少,融合无米下锅;太多则延迟上升且引入噪声。经验起点是每路 50 条、融合窗口 50、最终返回 10。用评估集扫描候选数,找到指标饱和点——通常 recall 在候选数 50 到 100 之间趋于平稳。

7.2 查询路由

并非所有查询都需要混合。含订单号、错误码、引号短语的查询,BM25 一路就够,强行走向量路反而浪费资源。可以做简单的查询分类:命中「精确模式」的走纯 BM25,其余走混合。这类路由能显著降低平均延迟。

7.3 缓存策略

混合检索结果受向量模型版本影响,缓存键必须包含模型版本号,否则模型升级后旧缓存会返回过期排序。对高频重复查询,可以缓存 RRF 融合后的文档 ID 列表,rerank 阶段仍需实时计算。

7.4 常见坑

  • 向量维度不一致:查询向量与索引向量维度不同会直接报错,写入前校验。
  • 分数混用:把 RRF 分数与 BM25 分数放在同一阈值下比较,逻辑必然出错。
  • 忽略空结果:某一路返回空时 RRF 仍能工作,但如果两路都空要提前返回,避免无谓的 rerank 调用。
  • 模型漂移:embedding 模型升级未重建索引,会导致新旧向量语义空间不一致,检索质量悄然劣化。

8. 总结

环节要点
互补性BM25 管精确匹配,向量管语义泛化
尺度问题两路分数不可直接相加,RRF 用排名绕开
RRF 公式每路贡献 1 / (k + rank),默认 k 为 60
内置检索器rrf retriever 融合多路,支持权重
重排序cross-encoder 精度高,只作用于 top-N 候选
评估指标recall@k 看召回,nDCG 看排序,MRR 看首条
工程要点向量字段与模型版本必须严格一致
调优方向候选数、融合策略、查询路由三处发力

混合检索不是「把两个检索拼起来」那么简单,它的每一步都在做取舍:RRF 用信息损失换稳健,rerank 用延迟换精度,查询路由用复杂度换资源。判断某次改动的价值,唯一可靠的依据是固定评估集上的离线指标加上线后的 A/B 数据。检索质量的评估与迭代是一条长期路线,下一篇将转向缓存体系——检索链路上的每一层缓存,都在决定这套架构能否扛住真实流量。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 可搜索快照与冻结层:把冷数据放进对象存储还能查
  2. 分页与深度分页:from/size、search_after、PIT 与 scroll
  3. 嵌套与父子关联查询:nested、join 字段与性能取舍