RRF (Reciprocal Rank Fusion)

Search Engineering intermediate Building with AI 11 min read

Overview

Reciprocal Rank Fusion (RRF) is a rank aggregation algorithm that merges multiple ranked result lists into a single, unified ranking. Originally published by Cormack, Clarke, and Büttcher in 2009, RRF has become the default fusion algorithm in most hybrid search implementations because of its remarkable simplicity, robustness, and resistance to the score normalization problems that plague other fusion methods.

The core insight of RRF is elegant: instead of trying to compare raw scores from different ranking systems (which operate on incompatible scales), it works exclusively with ordinal rank positions. A document’s contribution from each list depends only on where it appears in that list — first, fifth, or hundredth — not on the numerical score that put it there. This makes RRF inherently immune to the score distribution differences between keyword search (BM25 scores) and vector search (cosine similarity scores).

RRF is used in production at Azure AI Search, Elasticsearch, OpenSearch, Weaviate, Milvus, MongoDB Atlas Vector Search, and virtually every other search platform that offers hybrid search. Understanding how it works — and its limitations — is essential for any engineer building retrieval pipelines.

How It Works

The RRF Formula

The algorithm is defined by a single formula. For each document d appearing in one or more ranked lists, the RRF score is:

RRF Formula

RRF_score(d) = Σ 1 / (k + rank_i(d))

Where rank_i(d) is the position of document d in ranked list i (starting from 1), k is a constant (typically 60), and the sum is over all lists where d appears. Documents not present in a list are simply omitted from that list’s contribution.

Step-by-Step Walkthrough

Let’s trace through a concrete example. Suppose we run a hybrid search and get these results:

# BM25 Results (keyword search)
Rank 1: Doc_A  (BM25 score: 18.7)
Rank 2: Doc_B  (BM25 score: 15.2)
Rank 3: Doc_C  (BM25 score: 12.1)
Rank 4: Doc_D  (BM25 score: 8.4)
Rank 5: Doc_E  (BM25 score: 6.9)

# Vector Results (semantic search)
Rank 1: Doc_C  (cosine: 0.94)
Rank 2: Doc_F  (cosine: 0.91)
Rank 3: Doc_A  (cosine: 0.88)
Rank 4: Doc_G  (cosine: 0.85)
Rank 5: Doc_B  (cosine: 0.82)

Now we calculate the RRF score for each document (using k=60):

# RRF Scores (k=60)
Doc_A: 1/(60+1) + 1/(60+3) = 0.01639 + 0.01587 = 0.03226  ← appears in both
Doc_B: 1/(60+2) + 1/(60+5) = 0.01613 + 0.01538 = 0.03151  ← appears in both
Doc_C: 1/(60+3) + 1/(60+1) = 0.01587 + 0.01639 = 0.03226  ← appears in both
Doc_D: 1/(60+4)            = 0.01563                        ← BM25 only
Doc_E: 1/(60+5)            = 0.01538                        ← BM25 only
Doc_F: 1/(60+2)            = 0.01613                        ← Vector only
Doc_G: 1/(60+4)            = 0.01563                        ← Vector only

# Final Ranking:
1. Doc_A: 0.03226  (tied with Doc_C)
2. Doc_C: 0.03226
3. Doc_B: 0.03151
4. Doc_F: 0.01613
5. Doc_D: 0.01563  (tied with Doc_G)
6. Doc_G: 0.01563
7. Doc_E: 0.01538

Notice how documents appearing in both lists (Doc_A, Doc_B, Doc_C) score much higher than those in only one list. This is the consensus reward — RRF inherently boosts documents that multiple retrievers agree are relevant.

The k Parameter

The constant k controls how much RRF compresses rank differences. With the standard k=60:

The difference between rank 1 and rank 10 is only 0.0021, while the difference between rank 1 and rank 100 is 0.0101. This means top-10 documents are treated relatively equally, while documents beyond rank 50-100 contribute negligibly. This compression effect is intentional — it prevents a single outlier ranking from dominating the fused result.

The k=60 Standard

The original paper tested multiple values and found k=60 to be optimal across their benchmarks. This value has become the industry standard and is the default in Azure AI Search, Elasticsearch, and most other implementations. Lower values of k make RRF more sensitive to top ranks; higher values flatten the curve further. In practice, k=60 works well for the vast majority of use cases without tuning.

Implementation

RRF is straightforward to implement. Here’s a clean Python implementation:

from collections import defaultdict

def reciprocal_rank_fusion(
    ranked_lists: list[list[str]],
    k: int = 60
) -> list[tuple[str, float]]:
    """
    Fuse multiple ranked document lists using RRF.

    Args:
        ranked_lists: List of ranked document ID lists.
                      Each inner list is ordered by relevance (best first).
        k: Ranking constant (default 60).

    Returns:
        List of (doc_id, rrf_score) tuples, sorted by score descending.
    """
    scores = defaultdict(float)

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

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


# Example usage with hybrid search results
bm25_results = ["doc_a", "doc_b", "doc_c", "doc_d", "doc_e"]
vector_results = ["doc_c", "doc_f", "doc_a", "doc_g", "doc_b"]

fused = reciprocal_rank_fusion([bm25_results, vector_results])
for doc_id, score in fused:
    print(f"{doc_id}: {score:.5f}")

Weighted RRF (WRRF)

A common extension is Weighted RRF, which allows different lists to contribute differently to the final score:

def weighted_rrf(
    ranked_lists: list[list[str]],
    weights: list[float],
    k: int = 60
) -> list[tuple[str, float]]:
    scores = defaultdict(float)

    for weight, ranked_list in zip(weights, ranked_lists):
        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)

# Weight semantic search higher than keyword search
fused = weighted_rrf(
    [bm25_results, vector_results],
    weights=[0.3, 0.7],  # 30% keyword, 70% semantic
    k=60
)

This gives you similar control to the alpha parameter in hybrid search systems while retaining RRF’s rank-based robustness.

Handling More Than Two Lists

RRF naturally extends to any number of ranked lists. In advanced RAG systems, you might fuse results from three or more sources:

The formula remains the same — each list contributes its 1/(k + rank) score independently, and the sum determines the final ranking.

Practical Applications

Azure AI Search uses RRF as its default fusion strategy for hybrid queries. When you issue a hybrid query combining text search and vector search, Azure internally generates two ranked lists and fuses them with RRF (k=60). The fused results are returned to your application with the RRF scores, which you can use for relevance thresholding.

Elasticsearch and OpenSearch

Elasticsearch 8.x introduced native RRF support for combining traditional BM25 queries with kNN vector queries. OpenSearch followed with similar RRF support for its hybrid search pipeline. Both use k=60 as the default and allow configuring the window size (how many results from each list to consider).

RAG Pipeline Retrieval

In RAG applications, RRF is typically applied at the retrieval stage before reranking. The pipeline looks like: (1) run BM25 and vector searches in parallel, (2) fuse with RRF to get top-N candidates, (3) optionally rerank with a cross-encoder, (4) inject top-K into the LLM prompt. RRF at step 2 ensures the candidate pool for the reranker is diverse and captures both keyword and semantic relevance.

RRF vs. Relative Score Fusion (RSF)

The primary alternative to RRF is Relative Score Fusion, which normalizes raw scores instead of using ranks. The tradeoffs:

Key Takeaways

Further Reading