纯 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@10 | nDCG@10 | p95 延迟 |
|---|---|---|---|
| 纯 BM25 | 0.62 | 0.51 | 12ms |
| 纯 kNN | 0.58 | 0.47 | 35ms |
| RRF 融合 | 0.79 | 0.58 | 48ms |
| RRF 加 rerank | 0.79 | 0.71 | 210ms |
融合带来召回提升,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 数据。检索质量的评估与迭代是一条长期路线,下一篇将转向缓存体系——检索链路上的每一层缓存,都在决定这套架构能否扛住真实流量。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。