0Pricing
AI Engineering Academy · 课时

使用倒数排名融合合并分数

实现倒数排名融合,在无需标准化不兼容相似度分数的情况下,合并来自稠密检索器和稀疏检索器的排序结果列表。

使用倒数排名融合合并分数 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

The Score Incompatibility Problem

Dense retrievers produce cosine similarity scores between -1 and 1, while BM25 produces unbounded positive scores that depend on corpus size and term frequencies. You cannot add these numbers directly — a BM25 score of 3.7 and a cosine similarity of 0.85 mean completely different things. Simple score normalization (dividing by max score) is brittle because outlier documents distort the scale. We need a method that is agnostic to absolute score values.

The Core Idea Behind RRF

Reciprocal Rank Fusion (RRF) sidesteps the score incompatibility problem by converting each retriever's results into ranked positions and fusing those ranks rather than raw scores. A document ranked first gets a high RRF contribution, a document ranked tenth gets a much lower one, and the final score is the sum of RRF contributions across all retrievers. The formula is: RRF(d) = sum(1 / (k + rank_i(d))) where k is a smoothing constant (typically 60).

# RRF formula
# For each retriever i, document d receives:
#   contribution = 1 / (k + rank_i(d))
# Final RRF score = sum of contributions from all retrievers
# k = 60 is the standard constant from the original 2009 paper

# Example:
# Document A: rank 1 in BM25, rank 4 in dense
#   RRF(A) = 1/(60+1) + 1/(60+4) = 0.01639 + 0.01563 = 0.03202
# Document B: rank 2 in BM25, rank 2 in dense
#   RRF(B) = 1/(60+2) + 1/(60+2) = 0.01613 + 0.01613 = 0.03226
# Document B scores slightly higher due to consistent top-2 ranking

Implementing RRF from Scratch

The implementation is surprisingly simple. For each retriever, iterate over its ranked result list and accumulate RRF scores into a dictionary keyed by document ID. Documents that appear in multiple retrievers accumulate contributions from each. Finally, sort by total RRF score descending to produce the merged ranking.

from collections import defaultdict

def reciprocal_rank_fusion(
    result_lists: list[list[str]],
    k: int = 60,
) -> list[tuple[str, float]]:
    '''Merge multiple ranked result lists using RRF.
    result_lists: each inner list is a ranked list of document IDs
    Returns: sorted list of (doc_id, rrf_score) tuples
    '''
    rrf_scores = defaultdict(float)

    for ranked_list in result_lists:
        for rank, doc_id in enumerate(ranked_list, start=1):
            rrf_scores[doc_id] += 1.0 / (k + rank)

    sorted_results = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
    return sorted_results

End-to-End Hybrid Search with RRF

In practice, you run both retrievers, collect their ranked document ID lists, pass them to RRF, and look up the top-K document IDs in your document store. Notice that no score normalization is required — only the rank ordering of each retriever's results matters. This makes RRF extremely robust to distribution shifts and corpus size changes.

def hybrid_search_rrf(
    query: str,
    bm25_index,
    dense_retriever,
    documents: dict,  # id -> text
    top_k: int = 5,
) -> list[dict]:
    # Get ranked lists from each retriever
    bm25_ids = bm25_index.search(query, top_k=20)  # over-retrieve then fuse
    dense_ids = dense_retriever.search(query, top_k=20)

    # Run RRF
    fused = reciprocal_rank_fusion([bm25_ids, dense_ids])

    # Return top-K with scores
    results = []
    for doc_id, score in fused[:top_k]:
        results.append({
            'id': doc_id,
            'text': documents[doc_id],
            'rrf_score': round(score, 6),
        })
    return results

Why Over-Retrieve Before Fusing

Notice the pattern of retrieving 20 candidates from each retriever and then taking the top 5 after fusion. This over-retrieval strategy is important because a document that appears at rank 10 in BM25 but rank 1 in dense should bubble up in the fused list. If you only retrieve 5 from each, you miss such documents. A common practice is to retrieve top_k * 4 from each retriever before fusing and then return top_k final results.

The Smoothing Constant K

The constant k in the RRF formula controls how much weight is given to top-ranked versus lower-ranked documents. A small k amplifies the difference between ranks (rank 1 is much better than rank 2), while a large k flattens the distribution (all ranks matter roughly equally). The original paper used k=60 and empirical studies show values between 30 and 100 all perform similarly. k=60 is a robust default.

# Visualize effect of k on contribution weights
for k in [10, 60, 100]:
    weights = [1.0 / (k + rank) for rank in range(1, 11)]
    print(f'k={k}: rank1={weights[0]:.4f}, rank5={weights[4]:.4f}, rank10={weights[9]:.4f}')

# k=10:  rank1=0.0909, rank5=0.0667, rank10=0.0500  (steep)
# k=60:  rank1=0.0164, rank5=0.0154, rank10=0.0143  (flat)
# k=100: rank1=0.0099, rank5=0.0095, rank10=0.0091  (very flat)

Handling Documents Absent from One Retriever

RRF handles missing documents gracefully: a document that appears in only one retriever still accumulates that retriever's RRF contribution. It simply does not receive a second contribution. This means documents that both retrievers agree on will naturally score higher than documents found by only one. This consensus signal is exactly what you want in a hybrid system.

# Example showing consensus effect
example_docs = {
    'doc_A': [1, 3],   # ranks in [bm25, dense]
    'doc_B': [2, 2],   # both retrievers like it
    'doc_C': [3, None],  # only in BM25
    'doc_D': [None, 1],  # only in dense
}

k = 60
for doc, ranks in example_docs.items():
    score = sum(1/(k + r) for r in ranks if r is not None)
    print(f'{doc}: RRF = {score:.5f}')
# doc_B ranks highest because both retrievers agree
# doc_D (rank 1 in dense only) may outscore doc_A (1,3) despite lower consensus

RRF with More Than Two Retrievers

RRF scales naturally to three or more retrievers. You might combine BM25, a dense embedding retriever, and a sparse learned retriever (like SPLADE) that explicitly optimizes for keyword matching in a learned sparse space. Each additional retriever adds another RRF contribution per document, and documents that rank highly across all retrievers win by a clear margin.

# Three-way hybrid: BM25 + dense + SPLADE
bm25_ids = ['doc_B', 'doc_A', 'doc_C', 'doc_D']
dense_ids = ['doc_D', 'doc_A', 'doc_B', 'doc_E']
splade_ids = ['doc_B', 'doc_D', 'doc_A', 'doc_F']

fused = reciprocal_rank_fusion([bm25_ids, dense_ids, splade_ids])
print('Fused ranking:')
for doc_id, score in fused:
    print(f'  {doc_id}: {score:.5f}')
# doc_A and doc_B likely dominate because they appear in all three lists

Weighted RRF for Asymmetric Retrievers

Standard RRF treats all retrievers equally, but in practice one retriever may be more reliable than another for your specific domain. Weighted RRF multiplies each retriever's contribution by a weight before summing. A weight of 1.5 on the dense retriever and 0.5 on BM25 emphasizes semantic matching, while equal weights of 1.0 treat them symmetrically. Tune weights on a validation set.

def weighted_rrf(
    result_lists: list[list[str]],
    weights: list[float],
    k: int = 60,
) -> list[tuple[str, float]]:
    from collections import defaultdict
    scores = defaultdict(float)
    for ranked_list, weight in zip(result_lists, weights):
        for rank, doc_id in enumerate(ranked_list, start=1):
            scores[doc_id] += weight * (1.0 / (k + rank))
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

# Emphasize dense retrieval (0.7) over BM25 (0.3)
fused = weighted_rrf([bm25_ids, dense_ids], weights=[0.3, 0.7])

RRF vs Score Normalization: Why RRF Wins

Alternatives to RRF include min-max normalization (scale each retriever's scores to [0,1]) and z-score normalization. Both are sensitive to outlier documents that inflate the maximum score and collapse other scores toward zero. RRF is robust to outliers because it only uses rank positions, not raw scores. Empirical benchmarks consistently show RRF outperforms normalization-based fusion approaches.

Implementing RRF with LangChain

LangChain's EnsembleRetriever implements RRF fusion under the hood. It accepts a list of retrievers and optional weights, runs them in parallel, and returns RRF-merged results. This lets you integrate hybrid search into LCEL chains with minimal boilerplate and swap out individual retrievers without changing the fusion logic.

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# Dense retriever
vectorstore = FAISS.from_documents(langchain_docs, OpenAIEmbeddings())
vector_retriever = vectorstore.as_retriever(search_kwargs={'k': 20})

# Sparse retriever
bm25_retriever = BM25Retriever.from_documents(langchain_docs, k=20)

# Hybrid ensemble using RRF internally
ensemble = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6],
)
results = ensemble.invoke('hybrid search reciprocal rank fusion')

Quick Check

Test your understanding of reciprocal rank fusion from this lesson.

Lesson Recap

In this lesson you learned: RRF merges ranked lists without normalizing incompatible scores, the formula 1/(k + rank) gives each document a contribution from each retriever, and documents ranked highly by multiple retrievers earn the highest fused scores. The smoothing constant k=60 is a robust default. LangChain's EnsembleRetriever implements RRF natively. Next up we configure hybrid search directly in Pinecone and pgvector.

常见问题解答

「使用倒数排名融合合并分数」课时是免费的吗?

是的 — 「使用倒数排名融合合并分数」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。

「使用倒数排名融合合并分数」这节课中我会学到什么?

实现倒数排名融合,在无需标准化不兼容相似度分数的情况下,合并来自稠密检索器和稀疏检索器的排序结果列表。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Engineering Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「使用倒数排名融合合并分数」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Engineering Academy 课中编写并运行代码吗?

能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 稠密检索与稀疏检索:权衡取舍
  2. 实现 BM25 关键词搜索
  3. 使用倒数排名融合合并分数
  4. 在 Pinecone 和 pgvector 中实现混合搜索
← 返回 AI Engineering Academy