在混合检索中,关键词检索可能返回 12.8 分,向量检索可能返回 0.83
分。这两个数来自不同评分体系,直接相加没有可靠的解释。
RRF(Reciprocal Rank
Fusion)改用排名位置进行融合:先看一篇文档在各路结果中排第几,再累加这些位置对应的贡献。
先统一排名的起点
本文排名从 1 开始,使用公式:
RRF(d) = Σᵢ 1 / (k + rankᵢ(d))
文档没有出现在某一路返回列表中时,该路贡献为 0。若代码下标从 0
开始,则写作
1 / (k + index + 1)。两种写法等价,但不能对已经从 1
开始的排名再加一次 1。公式与原始论文一致。RRF
原始论文
k 是平滑参数,常见取值为 60;它不是最终返回条数 top-k。k
越大,相邻名次之间的贡献差异越小。
完整算一遍
假设两路检索列表中,关注以下五篇文档。列表还可能包含其他文档,因此第二路出现第
6 名并不矛盾。
| 文档 | 检索器一排名 | 检索器二排名 | k=60 时的总分 |
|---|---|---|---|
| D1 | 1 | 4 | 1/61 + 1/64 = 0.032018 |
| D2 | 2 | 1 | 1/62 + 1/61 = 0.032522 |
| D3 | 3 | 6 | 1/63 + 1/66 = 0.031025 |
| D4 | 未返回 | 2 | 0 + 1/62 = 0.016129 |
| D5 | 4 | 未返回 | 1/64 + 0 = 0.015625 |
这五篇文档的顺序为 D2 > D1 > D3 > D4 > D5。D2
在两路中都靠前,因此总分最高。
用 Python 实现
from collections import defaultdict
def rrf(rankings, k=60):
if k < 0:
raise ValueError("k 不能为负数")
scores = defaultdict(float)
for ranking in rankings:
seen = set()
for rank, doc_id in enumerate(ranking, start=1):
if doc_id in seen:
raise ValueError("同一路结果不能包含重复文档 ID")
seen.add(doc_id)
scores[doc_id] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda item: (-item[1], item[0]))
print(rrf([["A", "B", "C"], ["B", "D", "A"]]))
这里用文档 ID 合并结果。不同检索器必须对同一文档使用相同
ID,否则同一内容会被误认为多篇文档。并列分数则按 ID
排序,方便复现实验。
使用边界
RRF
不需要对原始分数进行归一化,但会丢掉分差信息:第一名仅领先第二名一点,与领先很多,在
RRF 中没有区别。
它也无法找回所有检索器都没召回的文档。每一路返回多少候选,会改变参与融合的集合;大量高度相似的检索器还可能重复强化同一种偏好。因此,增加检索路数不等于一定提高质量。
RRF
适合作为多路检索的初始融合基线。如果验证集表明不同检索路的重要程度明显不同,可以比较加权分数融合等方法。最终应依据检索质量和下游回答表现做选择。
自测:排名已经从 1 开始时,第 1 名在 k=60 下的贡献是多少?答案是
1/61。