Reciprocal Rank Fusion per unire i punteggi
Implementi la reciprocal rank fusion per unire gli elenchi di risultati ordinati dei retriever dense e sparse, senza dover normalizzare punteggi di similarità incompatibili.
Reciprocal Rank Fusion per unire i punteggi è una lezione AI Engineering Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Engineering Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Engineering Academy include 4 lezioni in totale.
Parti di questa lezione non sono ancora state tradotte e vengono mostrate in inglese.
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 rankingImplementing 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_resultsEnd-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 resultsWhy 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 consensusRRF 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 listsWeighted 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.
Domande Frequenti
La lezione «Reciprocal Rank Fusion per unire i punteggi» è gratuita?
Sì — il testo completo di «Reciprocal Rank Fusion per unire i punteggi» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Engineering Academy, passa a CoddyKit PRO. Il corso AI Engineering Academy include 4 lezioni in totale.
Cosa imparerò in «Reciprocal Rank Fusion per unire i punteggi»?
Implementi la reciprocal rank fusion per unire gli elenchi di risultati ordinati dei retriever dense e sparse, senza dover normalizzare punteggi di similarità incompatibili. Eserciti AI Engineering Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare AI Engineering Academy?
Non è richiesta alcuna esperienza precedente. AI Engineering Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.
Quanto tempo richiede la lezione «Reciprocal Rank Fusion per unire i punteggi»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione AI Engineering Academy?
Sì. Ogni lezione AI Engineering Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Retrieval dense e sparse: compromessi
- Implementare la ricerca per parole chiave con BM25
- Reciprocal Rank Fusion per unire i punteggi
- Ricerca ibrida in Pinecone e pgvector