RRF 能把不同检索通道的排名合到一起,但它不能把没召回的文档变出来。重排器也是一样。很多“换了重排模型还是答错”的问题,实际发生在候选池形成之前。
10 月 8 日,Qdrant 与 Pento 发布了 miniCOIL EN-ES 的双语稀疏检索研究预览:尝试让英语与西班牙语的概念共享稀疏槽位。这提醒我们,稀疏与稠密检索的差异不只是两组分数范围;语言、词项和模型训练方式都会影响漏召回的类型。该预览没有提供中文业务的效果保证。官方文章
先检查“有没有”,再检查“排第几”
对于一条有人工相关性标注的查询,先算候选池是否覆盖正确文档,再检查最终 Top-k 的顺序。若标注答案是文档 E,而两路候选只含 A、B、C、D,任何重排分数都无法修复这个遗漏。
RRF 对某个文档的贡献是1 / (k + rank),其中 rank 从 1 开始。各通道内部应该先去重,不然重复 ID 会被当成多份证据。k、每路深度、融合后的截断深度需要一起实验;没有一个参数能对所有查询都处在帕累托前沿。
下面只实现确定性的排名融合。它没有生成随机模型分数,也不在输出上伪装成 Cross-Encoder 推理。
图中 E 位于候选池外,它表示召回遗漏。不要把这张示意图理解成某个模型的性能结果;代码中的顺序完全由两份人工给定的列表决定。
from collections import defaultdict def rrf(rankings, k=60): if k < 0: raise ValueError("k must be nonnegative") scores = defaultdict(float) for ranking in rankings: unique = list(dict.fromkeys(ranking)) for rank, doc_id in enumerate(unique, 1): scores[doc_id] += 1.0 / (k + rank) return sorted(scores, key=lambda d: (-scores[d], d)) def candidate_recall(candidates, relevant): if not relevant: raise ValueError("query has no relevance judgment") return len(set(candidates) & relevant) / len(relevant) rankings = [["A", "B", "B", "C"], ["C", "A", "D"]] fused = rrf(rankings) assert len(fused) == 4 and fused[0] == "A" assert rrf([["A", "A"]]) == rrf([["A"]]) assert candidate_recall(fused, {"E"}) == 0.0 assert "E" not in fused print("fused=A,C,B,D; missed candidate E remains unrecoverable")例子跑通以后,还要验证什么
这段代码已在本地 macOS、CPython 3.9.6 下运行,实际输出为fused=A,C,B,D; missed candidate E remains unrecoverable。它验证的是例子中的数据约束与分支,不包含线上服务的吞吐或延迟测量。
未运行 BM25 索引、miniCOIL、向量库或 Cross-Encoder,未测 GPU 或并发性能。接入真实系统后,需要分别记录每路深度、候选并集召回率、融合召回率、重排质量和排队耗时。改一个参数时,其他条件与评测查询应固定;不要同时换模型、向量与分块后把收益全归给 RRF。
延伸阅读
- miniCOIL EN-ES:2026 年 10 月 8 日的研究预览,说明双语稀疏表示的尝试与限制。
- Qdrant Hybrid Queries:查阅融合与多阶段查询语义,实际 API 仍需按部署版本验证。