RRF 之后为什么还要 RerankBi-Encoder vs CrossEncoder 大白话结论先放前面RRF 解决的是“两路结果怎么按排名融合”的问题Rerank 解决的是“如何对候选文本对重新打分”的问题。两阶段检索是常见候选架构但是否值得加入 Rerank必须用目标数据同时验证质量和延迟。文章目录RRF 之后为什么还要 RerankBi-Encoder vs CrossEncoder 大白话一、上一篇的坑二、Bi-Encoder vs CrossEncoder两个老员工的差异2.1 Bi-Encoder双塔模型各干各的2.2 CrossEncoder交叉编码器坐下来细聊2.3 一张表看懂差异三、两阶段检索为什么两个都要为什么不能只用 CrossEncoder为什么不能只用 Bi-Encoder四、动手跑一个 smoke test延迟实测五、一个重要提醒分数不能乱加六、写在最后一、上一篇的坑上周我们用 EasySearch 2.3 做了 BM25 KNN 的混合检索还手写了 RRF 排名融合。Week2 脚本里当时标成Recall3的指标达到 100%复核后确认它实际衡量的是“Top3 是否至少命中一篇相关文档”准确名称应为Hit3/Success3不是严格 Recall3。但如果你仔细看 RRF 的结果会发现一个问题RRF 只做了合并没做深判。RRF 的排序依据是这篇文档在 BM25 里排第几这篇文档在 KNN 里排第几它把两路排名用公式1/(krank)加起来。RRF 融合计算本身不会再次读取 query 和文档正文正文信息已经通过上游 BM25/KNN 的排名间接进入结果。这就好比 HR 筛简历第一轮只看学历和工作年限BM25 看关键词KNN 看语义向量把两批候选人合并排序。但真正决定录不录取的应该是第二轮——让面试官坐下来逐份细读简历和岗位的匹配度。这个第二轮就是Rerank重排序/精排。二、Bi-Encoder vs CrossEncoder两个老员工的差异要理解 Rerank先搞清楚 Embedding 模型的两种工作方式。2.1 Bi-Encoder双塔模型各干各的我们做 KNN 向量检索时用的模型比如BAAI/bge-small-zh-v1.5就是 Bi-Encoder。它的工作方式query ──→ 模型 ──→ query 向量 [0.12, -0.05, ...] 文档 ──→ 模型 ──→ 文档向量 [0.08, 0.31, ...] 相关性 cos(query向量, 文档向量)特点query 和文档分开编码互不见面。文档向量可以预先算好存进 EasySearch 的knn_dense_float_vector字段。查询时只需要编码 query然后做向量近邻搜索速度极快。缺点query 和文档没有直接交互匹配精度有限。这就像相亲网站每个人填一份资料文档向量系统根据资料匹配向量相似度。快是快但资料写得再好也不如见面聊一次。2.2 CrossEncoder交叉编码器坐下来细聊Rerank 用的模型比如BAAI/bge-reranker-base就是 CrossEncoder。它的工作方式query 文档1 ──→ 模型 ──→ 分数 0.82 query 文档2 ──→ 模型 ──→ 分数 0.65 query 文档3 ──→ 模型 ──→ 分数 0.91特点query 和每篇候选文档拼接后一起输入模型模型内部可以做深层的交互注意力。直接输出文本对分数通常能做更细的候选排序但是否更准确必须在具体数据集上验证。缺点每篇候选都要过一次模型不能预计算速度慢。这就像面试官一对一面试每个候选人单独聊判断更准但一天只能面几个人。2.3 一张表看懂差异对比项Bi-EncoderKNN 用CrossEncoderRerank 用输入query、文档分开编码query 文档拼接输入输出向量相关性分数能否预计算✅ 文档向量可预存❌ 每对都要实时算计算特点文档向量可预计算可配合近似近邻检索扩展每个候选文本对都要实时计算通常只处理 TopK精度够用更精细适合位置全库召回候选集精排三、两阶段检索为什么两个都要理解了两种模型的差异两阶段检索的设计就顺理成章了用户 Query │ ▼ ┌─────────────────────────────┐ │ 第一阶段召回EasySearch │ │ BM25 Top20 KNN Top20 │ │ → RRF 融合 → Top10 │ │ 目标尽量不漏宁多勿少 │ └─────────────────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 第二阶段精排CrossEncoder│ │ 对 Top10 逐篇打分重排 │ │ → 最终 Top5 │ │ 目标把最相关的排到最前面 │ └─────────────────────────────┘一句话概括第一阶段用 EasySearch 的 BM25 KNN RRF用速度换覆盖——宁可多召回一些也不能漏掉正确答案。第二阶段用 CrossEncoder用延迟换精度——在少量候选里仔细判断把最好的排前面。为什么不能只用 CrossEncoder假设你的知识库有 100 万篇文档。如果每来一个 query就让 CrossEncoder 把 100 万篇全部读一遍打分一个查询就要跑 100 万次模型推理——谁也等不起。所以 CrossEncoder只能放在召回之后对几十到几百条候选做精排。为什么不能只用 Bi-EncoderBi-Encoder 是独立编码query 向量和文档向量分别计算编码文档时看不到当前 query因此缺少 CrossEncoder 那种文本对联合交互。比如 query “Elasticsearch 写入 rejected 怎么办”Bi-Encoder 可能把Redis 写入失败排得也很高因为向量空间里写入失败很接近。CrossEncoder 会联合处理“Elasticsearch”和“rejected”所在的文本对因此有机会做更细的区分这只是说明模型机制的例子不是本文已经测得该 case 一定更准。四、动手跑一个 smoke test理论讲完跑个最小实验。我在本地 CPU 上用BAAI/bge-reranker-base做了 smoke testfromsentence_transformersimportCrossEncoder modelCrossEncoder(BAAI/bge-reranker-base,devicecpu)queryElasticsearch 节点磁盘水位过高怎么办pairs[(query,Elasticsearch 磁盘水位过高节点达到 high 水位后应清理无用索引或扩容。),(query,Linux 磁盘空间不足使用 df 和 du 定位大目录。),(query,Elasticsearch 写入 rejected检查 write 线程池和 bulk 并发。),]scoresmodel.predict(pairs)for(q,doc),scoreinzip(pairs,scores):print(f{score:.4f}|{doc[:30]})输出示意0.9521 | Elasticsearch 磁盘水位过高节点达到 high 水位后... 0.6213 | Linux 磁盘空间不足使用 df 和 du 定位大目录... 0.4105 | Elasticsearch 写入 rejected检查 write 线程池...这段输出只用于说明返回格式不是本项目保存下来的真实分数。CrossEncoder 会分别给每个 query-document 文本对打分再按分数排序真实结论应以评测报告为准。延迟实测在本地 CPU 上我也测了延迟基线模型加载约3.7 秒一次性可以复用。5 条候选精排约750 毫秒P50。10 条候选精排约1450 毫秒P50。20 条候选精排约2740 毫秒P50。在这次本地 CPU 实验里候选数翻倍时 P50 延迟也接近翻倍。这是 Rerank 通常只处理召回 TopK、而不直接扫描全库的重要原因之一。说明这是个人本地 CPU、串行请求下的 dev 实测不是生产性能测试。GPU、量化或 ONNX 等手段可能降低延迟但本项目没有实测这些方案因此不预设固定的加速倍数。五、一个重要提醒分数不能乱加最后一个工程上容易踩的坑Reranker 的分数不能和 BM25、KNN、RRF 的分数直接相加。BM25 的_score由 BM25 公式、查询和索引统计共同决定范围不是固定的。KNN 的_score由相似度定义和检索实现决定本项目使用 LSH cosine也不能假设固定范围。RRF 分数由候选排名、rrf_k和权重决定。CrossEncoder 分数尺度取决于模型及激活函数可能是原始 logit也可能被映射到 0~1。这四个分数来源和尺度不同未经归一化、校准或专门训练的融合模型时不能直接相加。本项目采用的做法是召回阶段用 BM25/KNN/RRF 各自排序、融合选出候选集。精排阶段按 CrossEncoder 分数对候选集重新排序并保留原排名用于分析。更复杂的系统可以做经过验证的分数融合但不能把不同_score原样相加后就宣称相关性更准。六、写在最后回到标题的问题RRF 之后为什么还要 RerankRRF 融合的是排名它不关心文档内容。Rerank 读的是内容它用 CrossEncoder 逐篇判断相关性。在 EasySearch 场景下可以验证下面这条候选链路EasySearch BM25/KNN 召回 → RRF 融合 → CrossEncoder 精排 → TopK召回阶段偏重候选覆盖精排阶段尝试改善候选顺序两者组合不保证自动“既快又准”。本项目 dev 结果中Rerank(10) 优于 RRF但仍未超过单路 KNN而且延迟更高。下一篇我会写这条完整链路在 EasySearch 2.3 上怎么落地包括每一步的代码和真实排名变化。参考代码本文实验对应本地脚本10-rerank-baseline.py后续整理到 GitHub 后可以在这里补充仓库链接。环境EasySearch 2.3.0 Python 3.14.4 sentence-transformers 5.6.0 BAAI/bge-reranker-baseCPU官方资料Sentence Transformers CrossEncoderBAAI/bge-reranker-base 模型卡EasySearch 向量查询如果你正在做 RAG 或知识库搜索可以留个言你现在的链路是只有召回还是已经上了 Rerank精排带来的延迟你能接受多少
# RRF 之后为什么还要 Rerank?Bi-Encoder vs CrossEncoder 大白话
RRF 之后为什么还要 RerankBi-Encoder vs CrossEncoder 大白话结论先放前面RRF 解决的是“两路结果怎么按排名融合”的问题Rerank 解决的是“如何对候选文本对重新打分”的问题。两阶段检索是常见候选架构但是否值得加入 Rerank必须用目标数据同时验证质量和延迟。文章目录RRF 之后为什么还要 RerankBi-Encoder vs CrossEncoder 大白话一、上一篇的坑二、Bi-Encoder vs CrossEncoder两个老员工的差异2.1 Bi-Encoder双塔模型各干各的2.2 CrossEncoder交叉编码器坐下来细聊2.3 一张表看懂差异三、两阶段检索为什么两个都要为什么不能只用 CrossEncoder为什么不能只用 Bi-Encoder四、动手跑一个 smoke test延迟实测五、一个重要提醒分数不能乱加六、写在最后一、上一篇的坑上周我们用 EasySearch 2.3 做了 BM25 KNN 的混合检索还手写了 RRF 排名融合。Week2 脚本里当时标成Recall3的指标达到 100%复核后确认它实际衡量的是“Top3 是否至少命中一篇相关文档”准确名称应为Hit3/Success3不是严格 Recall3。但如果你仔细看 RRF 的结果会发现一个问题RRF 只做了合并没做深判。RRF 的排序依据是这篇文档在 BM25 里排第几这篇文档在 KNN 里排第几它把两路排名用公式1/(krank)加起来。RRF 融合计算本身不会再次读取 query 和文档正文正文信息已经通过上游 BM25/KNN 的排名间接进入结果。这就好比 HR 筛简历第一轮只看学历和工作年限BM25 看关键词KNN 看语义向量把两批候选人合并排序。但真正决定录不录取的应该是第二轮——让面试官坐下来逐份细读简历和岗位的匹配度。这个第二轮就是Rerank重排序/精排。二、Bi-Encoder vs CrossEncoder两个老员工的差异要理解 Rerank先搞清楚 Embedding 模型的两种工作方式。2.1 Bi-Encoder双塔模型各干各的我们做 KNN 向量检索时用的模型比如BAAI/bge-small-zh-v1.5就是 Bi-Encoder。它的工作方式query ──→ 模型 ──→ query 向量 [0.12, -0.05, ...] 文档 ──→ 模型 ──→ 文档向量 [0.08, 0.31, ...] 相关性 cos(query向量, 文档向量)特点query 和文档分开编码互不见面。文档向量可以预先算好存进 EasySearch 的knn_dense_float_vector字段。查询时只需要编码 query然后做向量近邻搜索速度极快。缺点query 和文档没有直接交互匹配精度有限。这就像相亲网站每个人填一份资料文档向量系统根据资料匹配向量相似度。快是快但资料写得再好也不如见面聊一次。2.2 CrossEncoder交叉编码器坐下来细聊Rerank 用的模型比如BAAI/bge-reranker-base就是 CrossEncoder。它的工作方式query 文档1 ──→ 模型 ──→ 分数 0.82 query 文档2 ──→ 模型 ──→ 分数 0.65 query 文档3 ──→ 模型 ──→ 分数 0.91特点query 和每篇候选文档拼接后一起输入模型模型内部可以做深层的交互注意力。直接输出文本对分数通常能做更细的候选排序但是否更准确必须在具体数据集上验证。缺点每篇候选都要过一次模型不能预计算速度慢。这就像面试官一对一面试每个候选人单独聊判断更准但一天只能面几个人。2.3 一张表看懂差异对比项Bi-EncoderKNN 用CrossEncoderRerank 用输入query、文档分开编码query 文档拼接输入输出向量相关性分数能否预计算✅ 文档向量可预存❌ 每对都要实时算计算特点文档向量可预计算可配合近似近邻检索扩展每个候选文本对都要实时计算通常只处理 TopK精度够用更精细适合位置全库召回候选集精排三、两阶段检索为什么两个都要理解了两种模型的差异两阶段检索的设计就顺理成章了用户 Query │ ▼ ┌─────────────────────────────┐ │ 第一阶段召回EasySearch │ │ BM25 Top20 KNN Top20 │ │ → RRF 融合 → Top10 │ │ 目标尽量不漏宁多勿少 │ └─────────────────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 第二阶段精排CrossEncoder│ │ 对 Top10 逐篇打分重排 │ │ → 最终 Top5 │ │ 目标把最相关的排到最前面 │ └─────────────────────────────┘一句话概括第一阶段用 EasySearch 的 BM25 KNN RRF用速度换覆盖——宁可多召回一些也不能漏掉正确答案。第二阶段用 CrossEncoder用延迟换精度——在少量候选里仔细判断把最好的排前面。为什么不能只用 CrossEncoder假设你的知识库有 100 万篇文档。如果每来一个 query就让 CrossEncoder 把 100 万篇全部读一遍打分一个查询就要跑 100 万次模型推理——谁也等不起。所以 CrossEncoder只能放在召回之后对几十到几百条候选做精排。为什么不能只用 Bi-EncoderBi-Encoder 是独立编码query 向量和文档向量分别计算编码文档时看不到当前 query因此缺少 CrossEncoder 那种文本对联合交互。比如 query “Elasticsearch 写入 rejected 怎么办”Bi-Encoder 可能把Redis 写入失败排得也很高因为向量空间里写入失败很接近。CrossEncoder 会联合处理“Elasticsearch”和“rejected”所在的文本对因此有机会做更细的区分这只是说明模型机制的例子不是本文已经测得该 case 一定更准。四、动手跑一个 smoke test理论讲完跑个最小实验。我在本地 CPU 上用BAAI/bge-reranker-base做了 smoke testfromsentence_transformersimportCrossEncoder modelCrossEncoder(BAAI/bge-reranker-base,devicecpu)queryElasticsearch 节点磁盘水位过高怎么办pairs[(query,Elasticsearch 磁盘水位过高节点达到 high 水位后应清理无用索引或扩容。),(query,Linux 磁盘空间不足使用 df 和 du 定位大目录。),(query,Elasticsearch 写入 rejected检查 write 线程池和 bulk 并发。),]scoresmodel.predict(pairs)for(q,doc),scoreinzip(pairs,scores):print(f{score:.4f}|{doc[:30]})输出示意0.9521 | Elasticsearch 磁盘水位过高节点达到 high 水位后... 0.6213 | Linux 磁盘空间不足使用 df 和 du 定位大目录... 0.4105 | Elasticsearch 写入 rejected检查 write 线程池...这段输出只用于说明返回格式不是本项目保存下来的真实分数。CrossEncoder 会分别给每个 query-document 文本对打分再按分数排序真实结论应以评测报告为准。延迟实测在本地 CPU 上我也测了延迟基线模型加载约3.7 秒一次性可以复用。5 条候选精排约750 毫秒P50。10 条候选精排约1450 毫秒P50。20 条候选精排约2740 毫秒P50。在这次本地 CPU 实验里候选数翻倍时 P50 延迟也接近翻倍。这是 Rerank 通常只处理召回 TopK、而不直接扫描全库的重要原因之一。说明这是个人本地 CPU、串行请求下的 dev 实测不是生产性能测试。GPU、量化或 ONNX 等手段可能降低延迟但本项目没有实测这些方案因此不预设固定的加速倍数。五、一个重要提醒分数不能乱加最后一个工程上容易踩的坑Reranker 的分数不能和 BM25、KNN、RRF 的分数直接相加。BM25 的_score由 BM25 公式、查询和索引统计共同决定范围不是固定的。KNN 的_score由相似度定义和检索实现决定本项目使用 LSH cosine也不能假设固定范围。RRF 分数由候选排名、rrf_k和权重决定。CrossEncoder 分数尺度取决于模型及激活函数可能是原始 logit也可能被映射到 0~1。这四个分数来源和尺度不同未经归一化、校准或专门训练的融合模型时不能直接相加。本项目采用的做法是召回阶段用 BM25/KNN/RRF 各自排序、融合选出候选集。精排阶段按 CrossEncoder 分数对候选集重新排序并保留原排名用于分析。更复杂的系统可以做经过验证的分数融合但不能把不同_score原样相加后就宣称相关性更准。六、写在最后回到标题的问题RRF 之后为什么还要 RerankRRF 融合的是排名它不关心文档内容。Rerank 读的是内容它用 CrossEncoder 逐篇判断相关性。在 EasySearch 场景下可以验证下面这条候选链路EasySearch BM25/KNN 召回 → RRF 融合 → CrossEncoder 精排 → TopK召回阶段偏重候选覆盖精排阶段尝试改善候选顺序两者组合不保证自动“既快又准”。本项目 dev 结果中Rerank(10) 优于 RRF但仍未超过单路 KNN而且延迟更高。下一篇我会写这条完整链路在 EasySearch 2.3 上怎么落地包括每一步的代码和真实排名变化。参考代码本文实验对应本地脚本10-rerank-baseline.py后续整理到 GitHub 后可以在这里补充仓库链接。环境EasySearch 2.3.0 Python 3.14.4 sentence-transformers 5.6.0 BAAI/bge-reranker-baseCPU官方资料Sentence Transformers CrossEncoderBAAI/bge-reranker-base 模型卡EasySearch 向量查询如果你正在做 RAG 或知识库搜索可以留个言你现在的链路是只有召回还是已经上了 Rerank精排带来的延迟你能接受多少