EasySearch 两阶段检索实战:BM25 + KNN + RRF + Rerank 全链路
2026/7/22 2:26:26 网站建设 项目流程

EasySearch 两阶段检索实战:BM25 + KNN + RRF + Rerank 全链路

结论先放前面:在 EasySearch 2.3 上,本项目验证了一条候选链路:“BM25 Top20 + KNN Top20 → RRF 融合 Top10 → CrossEncoder 精排 Top5”。它不是所有场景的固定最优架构,是否采用要看真实评测。

文章目录

  • EasySearch 两阶段检索实战:BM25 + KNN + RRF + Rerank 全链路
    • 一、链路总览
    • 二、实验环境
    • 三、链路代码拆解
      • 3.1 第一步:两路召回 + RRF 融合
      • 3.2 第二步:CrossEncoder 精排
      • 3.3 保留每一阶段的 rank
    • 四、真实排名变化案例
    • 五、dev 真实评测结果
    • 六、怎么读这张表(诚实版)
      • 6.1 KNN 单路在这个小数据集上最强
      • 6.2 Rerank 没有超过 KNN
      • 6.3 Rerank 的延迟成本是实打实的
    • 七、如果继续做工程验证,怎么参考这个结论
    • 八、写在最后

一、链路总览

上一篇讲了为什么需要 Rerank,这篇直接上手:在 EasySearch 2.3 上把完整链路跑起来。

整条链路分四步:

用户 Query │ ├─→ BM25 Top20(EasySearch match 查询) │ ├─→ KNN Top20(EasySearch knn_nearest_neighbors) │ ▼ RRF 融合 → Top10 │ ▼ CrossEncoder 精排 → 最终 Top5

每一步对应一个参数:

参数含义我的取值
retrieve_kBM25 和 KNN 各自召回的深度20
rrf_top_kRRF 融合后保留多少条进入精排10
rerank_top_k精排后最终返回多少条5
candidatesKNN 近似搜索的候选数80

为什么要这样分层?因为每一步的成本不同:

  • BM25/KNN:EasySearch 服务端算,毫秒级,可以召回深一点。
  • RRF:本地 Python 做排名融合;本实验没有把融合部分单独计时,不能宣称“零成本”。
  • CrossEncoder:本地 CPU 对候选文本对打分。本次 dev 中,候选从 5 增至 10,P50 增加约 693ms;从 10 增至 20,又增加约 1290ms。延迟不是固定“每 10 条增加 700ms”,所以需要在目标环境实测。

二、实验环境

  • 搜索引擎:EasySearch 2.3.0
  • 索引week3_ops_eval,80 条运维故障文档
  • Embedding 模型BAAI/bge-small-zh-v1.5(KNN 召回用)
  • Reranker 模型BAAI/bge-reranker-base(精排用)
  • 评测 query:25 条 dev query,带 0/1/2 分级相关性标签

文档索引 mapping 和 Week2 一致:text字段做 BM25,embedding字段(knn_dense_float_vector,lsh + cosine)做 KNN。

三、链路代码拆解

3.1 第一步:两路召回 + RRF 融合

defretrieve_and_fuse(baseline,rrf_module,es,embedding_model,index_name,query,retrieve_k,candidates,rrf_top_k,rrf_k,bm25_weight,knn_weight):started_at=perf_counter()# BM25 召回 Top20bm25_hits=baseline.normalize_hits(baseline.bm25_search(es,index_name,query,retrieve_k))# KNN 召回 Top20knn_hits=baseline.normalize_hits(baseline.knn_search(es,embedding_model,index_name,query,retrieve_k,candidates))# RRF 融合,保留 Top10rrf_hits=rrf_module.rrf_fuse(bm25_hits,knn_hits,rrf_k=rrf_k,bm25_weight=bm25_weight,knn_weight=knn_weight,)[:rrf_top_k]forrrf_rank,hitinenumerate(rrf_hits,start=1):hit["rrf_rank"]=rrf_rank retrieval_ms=(perf_counter()-started_at)*1000return{"bm25":bm25_hits,"knn":knn_hits,"rrf":rrf_hits},retrieval_ms

这里复用了 Week2 的三个成果:

  • bm25_search:EasySearchmatch查询。
  • knn_search:EasySearchknn_nearest_neighbors查询。
  • rrf_fuse:手写 RRF 排名融合。

注意:报告中的 RRF 阶段 P50 约 168ms,包含顺序执行的 BM25、KNN 和本地融合;本实验没有单独测量 RRF Python 融合耗时,因此不能写成“几乎零成本”。

3.2 第二步:CrossEncoder 精排

defrerank_candidates(reranker,query,rrf_hits,rerank_top_k,batch_size):ifnotrrf_hits:return[],0.0# 构造 (query, 文档) 配对pairs=[(query,hit["text"])forhitinrrf_hits]# 逐对打分started_at=perf_counter()raw_scores=reranker.predict(pairs,batch_size=batch_size,show_progress_bar=False,convert_to_numpy=True,)rerank_ms=(perf_counter()-started_at)*1000scores=normalize_scores(raw_scores,len(rrf_hits))# 复制候选并写入分数,避免直接覆盖上游对象ranked=[]forhit,scoreinzip(rrf_hits,scores,strict=True):item=dict(hit)item["rerank_score"]=score ranked.append(item)# 按精排分数降序,分数相同时保留较好的 RRF 顺序ranked.sort(key=lambdaitem:(-item["rerank_score"],item["rrf_rank"]))ranked=ranked[:rerank_top_k]forrerank_rank,iteminenumerate(ranked,start=1):item["rerank_rank"]=rerank_rankreturnranked,rerank_ms

这里是本次链路耗时最高的一步:10 条候选组成 10 个文本对,模型可按 batch 处理,但每个文本对都要参与推理。它能联合读取 query 和候选正文,却不保证在当前数据上一定胜过 KNN。

3.3 保留每一阶段的 rank

调试 Rerank 效果时,最重要的是能回答:"这篇文档在每个阶段排第几?"所以每个文档对象里,我都保留了:

{"id":"ops-cpu-002","bm25_rank":3,# BM25 里排第几"knn_rank":1,# KNN 里排第几"rrf_rank":2,# RRF 融合后排第几"rerank_rank":1,# 精排后排第几"rerank_score":0.95# 返回结构示意,不是本文保存的实测分数}

有了这四个 rank,可以确认文档在哪个阶段发生了位置变化;至于模型为什么这样判断,仍需要结合正文、标签和额外实验分析。

四、真实排名变化案例

看一个真实 query:“Java 进程 CPU 突然飙升”

阶段Top3 文档已标注相关文档位置
RRFops-cpu-002 → ops-cpu-001 → ops-cpu-003ops-cpu-002 第 1,ops-cpu-001 第 2
Rerankops-cpu-002 → ops-cpu-005 → ops-disk-003ops-cpu-002 第 1,ops-cpu-001 掉到第 9

这个 case 很有意思:RRF 本来排得很好(一篇 relevance=2、一篇 relevance=1 的文档都在前 2),但 Rerank 反而把 ops-cpu-001 从第 2 拉到了第 9。这就是后面要重点讲的“精排退化”。

再看一个 Rerank 发挥正面作用的真实 dev 场景:q-dev-020 是“Elasticsearch 节点磁盘水位过高怎么办”。RRF Top3 为ops-disk-006 → ops-es-004 → ops-es-003,Rerank Top3 为ops-es-004 → ops-disk-006 → ops-es-006;标注的直接相关文档是ops-es-004ops-es-005。这次精排把更直接的 ES 水位文档从第 2 提到第 1,但不能由这一个案例推断所有 query 都会受益。

五、dev 真实评测结果

在 80 文档、25 条 dev query 上,各策略指标如下(真实跑出,非示意):

策略Top1MRRHit@3Recall@3nDCG@10
BM250.8000.8840.9200.6530.825
KNN0.9600.9600.9600.7670.891
RRF0.8400.9250.9600.7400.871
RRF+Rerank(5)0.9200.9400.9600.6600.858
RRF+Rerank(10)0.9200.9460.9600.6470.881
RRF+Rerank(20)0.9200.9330.9600.6600.861

稳态延迟:

| 策略 | P50 (ms) | P95 (ms) |
|—|—|—|—|
| BM25 | 77.50 | 297.45 |
| KNN | 91.32 | 320.65 |
| RRF | 167.82 | 577.74 |
| RRF+Rerank(10) | 1445.16 | 1967.96 |

指标口径:Top1 要求第一名是 relevance=2 的直接相关文档;MRR/Hit@3 把 relevance>0 视为相关;nDCG@10 用 0/1/2 分级标签。延迟为本地 CPU 串行请求。

可比性限制:评测脚本中的Rerank(5)最多只返回 5 条,却仍按 nDCG@10 计算,因此它在列表深度上天然少于其他策略,nDCG 不能与返回 Top10 的策略做完全公平的横向比较。Rerank(10)Rerank(20)都最终评估 Top10,二者可以直接比较;本次 10 的 nDCG 高于 20。

六、怎么读这张表(诚实版)

这组数据有三个值得说的地方,我如实呈现:

6.1 KNN 单路在这个小数据集上最强

KNN 的 Top1(0.960)、MRR(0.960)、nDCG@10(0.891)全是最高。可能原因包括数据规模较小、query 与文档语义较直接、标签分布对 KNN 有利等;本实验没有做消融,不能把结果确定归因于某一个原因或模型的领域能力。

6.2 Rerank 没有超过 KNN

RRF+Rerank(10) 的 nDCG@10 是 0.881,低于单独 KNN 的 0.891,但高于 RRF 的 0.871。准确说法是:它相对 RRF 有小幅提升,却没有超过本次 dev 上最强的 KNN,因此相对最强基线没有净收益。

我没有为了好看而挑数据。这正是做评测的意义:用固定评估集和指标说话,而不是挑几条 Rerank 表现好的 query 讲故事

6.3 Rerank 的延迟成本是实打实的

RRF 的 P50 是 168ms,加了 Rerank(10) 后 P50 增至 1445ms,P95 接近 2 秒。Rerank(10) 相对 RRF 的 nDCG 有小幅提升,但是否值得这部分延迟,需要结合业务目标判断;相对 KNN,它没有质量优势。

工程结论:在这个 80 文档的小规模实验里,KNN 单路是最优性价比;Rerank 是否值得,要在更大规模数据、更细粒度标签上重新验证。

七、如果继续做工程验证,怎么参考这个结论

如果你的场景和这个实验类似(小规模中文运维知识库),可以参考:

  1. 先把 EasySearch 的 KNN 调优:换更好的 Embedding 模型、调candidates、补充文档,收益可能比上 Rerank 更直接。
  2. Rerank 不是必选项:它是候选集内排序质量不足时的补丁,不是链路标配。本次实验中 P95 明显增加,具体增幅必须在目标硬件、并发和部署方式下重新测量。
  3. 上 Rerank 前必须有评测:dev/test 拆分 + 冻结参数 + 质量/延迟双指标,缺一不可。不然很可能花了延迟,只换来几条好看的个案。

八、写在最后

这条链路的价值不在于"Rerank 一定提升效果",而在于它把每个阶段的贡献都拆开了

  • BM25 的角色是词项匹配召回。
  • KNN 的角色是向量语义召回。
  • RRF 的角色是按排名融合两路结果;本次 Recall@3 高于 BM25、但低于 KNN。
  • Rerank 的角色是候选内重新打分;本次相对 RRF 有小幅提升、但没有超过 KNN。

哪一步有效、哪一步是成本,用固定评估集测出来,而不是拍脑袋。

下一篇我会写:怎么用 nDCG、P95 延迟和 dev/test 拆分,把"Rerank 有没有用"这个问题回答得更可信。


环境:EasySearch 2.3.0 + Python 3.14.4 + sentence-transformers 5.6.0 + BAAI/bge-small-zh-v1.5 + BAAI/bge-reranker-base(CPU)

你在生产里上过 Rerank 吗?质量提升和延迟成本,你是怎么权衡的?评论区聊聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询