1. 什么是 RRF? #

想象一下,你同时使用多个搜索引擎(比如百度、谷歌、必应)搜索同一个问题,每个搜索引擎都会返回一个结果列表。RRF 的作用就是将这些不同的结果列表合并成一个统一的、更准确的最终排名列表。

RRF 的核心优势在于:

2. 为什么需要 RRF? #

在实际应用中,我们经常会遇到需要合并多个排名列表的场景:

  1. 混合检索系统:结合关键词检索(如 BM25)和语义检索(如向量检索)的结果
  2. 多数据源搜索:从多个数据库或搜索引擎获取结果后需要统一排序
  3. 推荐系统:融合基于内容的推荐、协同过滤推荐等多种策略的结果
  4. 元搜索:聚合多个搜索引擎的结果

如果只是简单地将多个列表合并,可能会出现:

RRF 通过数学公式巧妙地解决了这些问题,让排名靠前的文档获得更高的权重,同时兼顾多个列表的一致性。

3. 前置知识 #

在学习 RRF 之前,我们需要了解一些基础概念:

3.1 排名列表(Ranking List) #

排名列表是一个有序的文档序列,按照相关性或重要性从高到低排列。例如:

列表 A: [文档1, 文档2, 文档3]
列表 B: [文档3, 文档1, 文档4]

在这个例子中:

3.2 排名(Rank) #

排名是指文档在列表中的位置,通常从 1 开始计数:

排名越小,表示文档越相关或越重要。

3.3 倒数(Reciprocal) #

倒数是指一个数的倒数,即 1 除以这个数。例如:

在 RRF 中,我们使用 1 / (k + rank) 作为分数,这样排名越靠前(rank 越小),分数越高。

3.4 Python 基础知识 #

需要了解:

4. 核心思想与公式 #

RRF 的核心思想是:排名越靠前的文档,应该获得越高的分数;如果一个文档在多个列表中排名都靠前,它的最终分数应该更高。

RRF 的数学公式如下:

$$ \text{RRF_score}(d) = \sum_{i=1}^{n} \frac{1}{k + \text{rank}_i(d)} $$

其中:

公式的含义:

  1. 对于每个排名列表,计算文档的倒数排名分数:$1 / (k + \text{rank})$
  2. 将所有列表中该文档的分数相加,得到最终分数
  3. 如果文档在某个列表中未出现,则该项为 0(不参与求和)

为什么使用倒数?

可以看到,排名越靠前,分数越高,但差距会逐渐缩小。这样既能突出高排名文档,又不会让低排名文档完全被忽略。

5. 算法步骤 #

RRF 算法的执行步骤非常简单清晰:

5.1 准备输入 #

输入是多个已经排序的排名列表,每个列表中的文档按照相关性从高到低排列。

5.2 初始化分数字典 #

创建一个空字典,用于存储每个文档的累计分数。

5.3 遍历所有排名列表 #

对于每个排名列表:

5.4 排序输出 #

按照最终分数从高到低排序,返回排序后的文档列表。

注意事项:

6. 代码实现 #

# 定义RRF(Reciprocal Rank Fusion)融合函数,rankings为多个排名列表,k为平滑常数
def reciprocal_rank_fusion(rankings, k=60):
    """
    实现 Reciprocal Rank Fusion 算法

    参数:
        rankings: 多个排名列表的列表,每个列表包含已排序的文档
        k: 平滑常数,默认值为 60

    返回:
        按 RRF 分数降序排列的 (文档, 分数) 元组列表
    """
    # 创建一个空字典存储所有文档的累加分数
    scores = {}

    # 遍历每个排名列表
    for ranking in rankings:
        # 从1开始枚举,对每个文档及其排名遍历
        for rank, doc in enumerate(ranking, 1):
            # 如果该文档还没有加入分数字典,则初始化为0
            if doc not in scores:
                scores[doc] = 0

            # 计算每个文档在此排序中的RRF分数,1/(k+rank)
            rrf_score = 1 / (k + rank)

            # 将RRF分数加到该文档的总分中
            scores[doc] += rrf_score

    # 按照分数从高到低对文档进行排序,获得排序结果
    sorted_results = sorted(scores.items(), key=lambda x: x[1], reverse=True)

    # 返回排序后的文档和分数组成的元组列表
    return sorted_results

# 定义关键词检索结果列表,模拟基于关键词检索的返回(如BM25)
keyword_results = [
    "doc1",
    "doc2",
    "doc3"
]

# 定义向量检索结果列表,模拟基于语义向量的检索结果
vector_results = [
    "doc3",
    "doc2",
    "doc4"
]

# 将关键词检索结果和向量检索结果进行RRF融合
fused_results = reciprocal_rank_fusion([keyword_results, vector_results], k=60)

# 打印关键词检索结果
print("关键词检索结果:", keyword_results)
# 打印向量检索结果
print("向量检索结果:", vector_results)
# 打印融合后的最终排序结果
print("\n融合后的最终结果:")
# 枚举输出融合后的文档及其RRF得分
for i, (doc, score) in enumerate(fused_results, 1):
    print(f"{i}. {doc} (分数: {score:.6f})")

7. 常见问题 #

7.1 k 值应该设置为多少? #

k 值是一个平滑常数,用于控制排名对分数的影响程度:

建议:

7.2 如果文档在某个列表中未出现怎么办? #

如果文档在某个排名列表中未出现,则该项不参与计算(相当于加 0)。例如:

7.3 可以处理多少个排名列表? #

理论上可以处理任意多个列表,但实际应用中:

7.4 RRF 分数需要归一化吗? #

通常不需要。RRF 分数本身已经具有可比性,直接按分数排序即可。如果确实需要归一化(例如与其他算法结合),可以使用:

# 归一化示例(可选)
def normalize_scores(results):
    """
    将 RRF 分数归一化到 [0, 1] 区间
    """
    if not results:
        return []

    # 获取最高分和最低分
    max_score = max(score for _, score in results)
    min_score = min(score for _, score in results)

    # 如果所有分数相同,返回原结果
    if max_score == min_score:
        return [(doc, 1.0) for doc, _ in results]

    # 归一化公式:(score - min) / (max - min)
    normalized = [
        (doc, (score - min_score) / (max_score - min_score))
        for doc, score in results
    ]

    return normalized

# 使用示例
results = reciprocal_rank_fusion([list1, list2], k=60)
normalized = normalize_scores(results)
print("归一化后的分数:")
for doc, score in normalized:
    print(f"{doc}: {score:.4f}")

8. 总结 #

Reciprocal Rank Fusion (RRF) 是一种简单而有效的排名列表融合算法,具有以下特点:

优势:

  1. 简单易用:算法逻辑清晰,实现简单,只需几行代码
  2. 无需训练:不需要训练数据,可以直接使用
  3. 参数少:只需要一个参数 k,且通常使用默认值 60
  4. 效果稳定:在各种场景下都能取得不错的效果
  5. 计算高效:时间复杂度为 O(n),n 为所有列表中的文档总数

适用场景:

核心思想:

使用建议: