Reference · Lesson 0004

Reciprocal Rank Fusion (RRF) Cheat Sheet

Printable. Designed to live next to your editor.

The formula

RRF(d) = Σᵢ  wᵢ / (k + rankᵢ(d))

Our constants (from config.py)

NameValueMeaning
RRF_K60Smoothing constant (Cormack default)
VECTOR_WEIGHT0.7Weight for cosine-similarity ranker
BM25_WEIGHT0.3Weight for BM25 ranker

Rank-decay table (k = 60)

rank1/(k+rank)% of rank-1weighted (×0.7 vec, ×0.3 bm25)
10.01639100%0.01148 / 0.00492
20.0161398%0.01129 / 0.00484
30.0158797%0.01111 / 0.00476
50.0153894%0.01077 / 0.00462
100.0142987%0.01000 / 0.00429
200.0125076%0.00875 / 0.00375
500.0090955%0.00636 / 0.00273
1000.0062538%0.00438 / 0.00188
Top 5 ranks contribute almost identically. After rank 50, contributions are still non-trivial. After rank 100, they're down to ~38% of rank-1.

Algorithm (pseudocode)

function RRF(rank_lists, weights, k=60):
    # rank_lists: dict[ranker_name → list of doc_ids]   (rank 1 first)
    # weights:    dict[ranker_name → float]
    scores = {}
    for name, rlist in rank_lists.items():
        w = weights[name]
        for rank, doc in enumerate(rlist, start=1):
            scores[doc] = scores.get(doc, 0) + w / (k + rank)
    return sorted(scores.items(), key=lambda x: -x[1])

How our HybridSearch.search does it

vec_results  = vector_store.vector_search(query_vec, top_k=top_k * 2)
bm25_results = bm25.search(query,              top_k=top_k * 2)
scores = {}

# Vector list: starts each chunk with vector contribution
for rank, r in enumerate(vec_results):
    cid = r["id"]
    scores[cid] = {
        "rrf_score":    VECTOR_WEIGHT / (RRF_K + rank + 1),
        "vector_rank":  rank + 1,
        "bm25_rank":    None,
        "vector_score": 1 - r["score"],   # cosine in [0,1]
    }

# BM25 list: adds BM25 contribution (or starts a new entry)
for rank, r in enumerate(bm25_results):
    cid = r["doc_id"]
    boost = BM25_WEIGHT / (RRF_K + rank + 1)
    if cid in scores:
        scores[cid]["rrf_score"] += boost
        scores[cid]["bm25_rank"] = rank + 1
    else:
        scores[cid] = hit_from_bm25_only(cid, ..., rank + 1, boost)

return sorted(scores.values(), key=lambda x: -x["rrf_score"])[:top_k]

Fusion algorithm comparison

AlgorithmNeeds normalized scores?Handles missing chunks?ComplexityUsed by
RRFNo (ranks only)Yes (missing = 0)O(N)Elasticsearch, OpenSearch, Vespa, Weaviate, us
CombSUMYes (min-max / z-score)Breaks if scale differsO(N)Older IR systems
CombMNZYesBreaks if scale differsO(N)Older IR systems
CondorcetNoYesO(N²)Rare; superseded by RRF
Linear blendYesBreaks if scale differsO(N)Naive / first attempt

When to change k

Smaller k (10–30)

Sharper top ranks. Top 1 dominates more. Use when:

  • top_k is small (≤ 5)
  • You trust both rankers near-perfectly
  • You want to "punish" lower ranks harshly

Larger k (100+)

Flatter curve, more contribution from tail. Use when:

  • top_k is large (≥ 50)
  • One ranker has a noisy long tail
  • You want "any presence in a list" to count

Default k=60 is fine for almost everything. Don't tune it before you've first tuned your actual rankers.

When to change weights

Corpus / query typeSuggested splitReason
Long-form prose (articles, books)vec 0.5 / bm25 0.5BM25 has many terms to work with
Short technical docs (TILs, API refs)vec 0.7 / bm25 0.3Vocab is small but semantic still wins
Code with identifiers / error codesvec 0.3 / bm25 0.7Exact matches matter more than semantics
General web searchvec 0.6 / bm25 0.4Common starting point

Common RRF failure modes

Glossary

TermDefinition
Rank1-indexed position in a sorted list. Rank 1 = best.
RankerA retrieval system that produces a sorted list of candidates.
FusionCombining multiple rankers' outputs into one ranking.
Reciprocal rank1 / rank. The smaller the rank, the larger the reciprocal. RRF uses a smoothed version: 1 / (k + rank).
Smoothing constant (k)Prevents the top ranks from completely dominating. Higher k = flatter curve.
Rescue patternWhen a chunk is high in one list and low (or missing) in another, RRF gives it a partial score from the high list. This is the main value of hybrid search.