Skip to content

Retrieval

Reciprocal rank fusion: what k=60 does to a short result list

Work out how the RRF constant k, the window size and per-list weights reorder a fused BM25 and vector list, then tune them on your own question set.

By Rose Donaldson, Context Retrieval team · Published

This article was drafted with AI assistance from the sources listed below and edited and published by the Context Retrieval team.

One tuning pass for reciprocal rank fusion
  1. 1Know your windowWrite down how many results each list contributes before fusion, for example 10 in Elasticsearch by default.
  2. 2Check n against kWith equal weights and a window of k+1 or less, every hit found by both lists outranks every hit found by one.
  3. 3Compute the weight thresholdA list's top hit beats a two-list hit at the window's last rank only when its weight exceeds (k+1)/(n-1).
  4. 4Sweep and measureTry a few values of k and weights on your question set and compare recall at the k you pass to the model.

Hybrid search gives you two ranked lists, one from BM25 and one from vector search, and reciprocal rank fusion (RRF) turns them into one. Most engines ship it with a constant of 60 and few people change it. That default behaves differently on a list of 10 results than on a list of 1,000, and RAG pipelines usually fuse short lists. This post works through the formula on short lists, so you can predict what your engine will do before you tune it.

The formula and its three knobs

Each document gets, from every list it appears in, a score of 1 / (k + rank), and the scores are summed. A document absent from a list gets nothing from that list.1 The method comes from Cormack, Clarke and Buettcher at SIGIR 2009.2 Azure AI Search describes the constant as best set to a small value such as 60, and notes that it is unrelated to the k that sets how many nearest neighbors a vector query returns.1

Three settings decide the outcome:

  • The constant k. Elasticsearch calls it rank_constant, with a default of 60 and a minimum of 1; its docs say a higher value gives lower-ranked documents more influence.3 PyTerrier's RRFusion also defaults to 60.4
  • The window, meaning how many results each list contributes before fusion. Elasticsearch's rank_window_size defaults to 10 and must be at least the request's size.3 PyTerrier keeps 1,000 results per query by default.4
  • Per-list weights. Elasticsearch multiplies each retriever's RRF score by a weight (default 1.0, generally available since Elastic Stack 9.2).3 Azure AI Search weights vector queries, with 1.0 as the default.1 Qdrant accepts a weights array, one per prefetch, as of v1.17.0.5

Defaults are not uniform. Qdrant's docs give a default k of 2 with zero-based ranks, and allow setting k as of v1.16.0.5 Before you compare results across engines, check which k and which rank base each one uses.

On a short list, k=60 is mostly a vote count

With k=60, the top result in one list scores 1/61 and the tenth scores 1/70. The gap between first and tenth place is small, about 15%. Being in a second list, however, roughly doubles a score.

That gives a simple threshold. With equal weights and a window of n results, a document found by both lists at the worst rank, n, scores 2/(k+n). A document found by one list at rank 1 scores 1/(k+1). The two-list hit wins whenever n < k + 2. With k=60 and a window of 10, every document that both searches returned outranks every document that only one returned, however high it ranked there.

For a RAG index this is often what you want: agreement between keyword and vector search is good evidence. It is not what you want when one list is the only one that can find the answer. A pasted error code is the usual case, as the BM25 vs embeddings post shows. If BM25 ranks the page with the exact string first and the vector search does not return it at all, k=60 on a window of 10 places it below every shared hit.

Lower k: the top of each list dominates

A small k does the opposite. With k=1, first place scores 1/2 and tenth scores 1/11, so position matters far more than agreement. Qdrant's default of 2 with zero-based ranks gives the same top score of 1/2.5

The following runs as written and shows both behaviors on two lists of 10 that share one document at the last rank:

def rrf(ranked_lists, k=60, weights=None):
    weights = weights or [1.0] * len(ranked_lists)
    scores = {}
    for ranking, w in zip(ranked_lists, weights):
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + w / (k + rank)
    return sorted(scores, key=lambda d: (-scores[d], d))

bm25 = ["exact", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "both"]
vector = ["v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "both"]

for k in (60, 1):
    fused = rrf([bm25, vector], k=k)
    print(k, fused[:3], "both at", fused.index("both") + 1)

print(rrf([bm25, vector], k=60, weights=[7, 1])[:3])

It prints:

60 ['both', 'exact', 'v1'] both at 1
1 ['exact', 'v1', 'b1'] both at 9
['exact', 'both', 'b1']

Weights have to be large to matter

The last line shows the cost of rescuing the one-list hit with a weight. Give list A a weight w and list B a weight of 1. The top hit found only by A scores w/(k+1), and a hit found by both at rank n scores (w+1)/(k+n). The one-list hit wins only when w > (k+1)/(n−1). With k=60 and a window of 10, that is a weight above about 6.8, which is why the example needs 7. A weight of 2, the kind of value people try first, changes nothing at the top of a 10-result window.

So on short windows, the real choices are a larger window, a smaller k, or a large weight. Each one also changes the order further down the list.

Tune it on your question set

This post includes no benchmark of these settings, and the right values depend on your pages and questions. A practical pass, using the golden set from the retrieval guide:

  1. Tag each question by type: exact identifier, paraphrase, concept.
  2. Log, per question, whether the correct chunk came from BM25, vectors or both.
  3. Sweep k (for example 1, 10, 60) and the window, and try one weighted run computed from the threshold above.
  4. Compare recall at the number of chunks you actually pass to the model, per question type.

If identifier questions improve and paraphrase questions hold, keep the change. If both move together, the problem is probably in chunking or ingestion, not in fusion.

Footnotes

  1. Microsoft, "Hybrid search scoring (RRF)", Azure AI Search documentation. ↩ ↩2 ↩3

  2. Cormack, Clarke and Buettcher, "Reciprocal rank fusion outperforms Condorcet and individual rank learning methods", SIGIR 2009, pp. 758–759. ↩

  3. Elastic, "RRF retriever", Elasticsearch reference. ↩ ↩2 ↩3

  4. PyTerrier, "Result fusion", pyterrier-alpha documentation. ↩ ↩2

  5. Qdrant, "Hybrid and multi-stage queries", Qdrant documentation. ↩ ↩2 ↩3

Frequently asked questions

Why is the RRF constant usually 60?

Reciprocal rank fusion was introduced in a 2009 SIGIR paper by Cormack, Clarke and Buettcher, and 60 is the value engines settled on. Elasticsearch, Azure AI Search and PyTerrier use 60 as their default or recommended value. Qdrant documents a default of 2, so check your engine.

Does a higher k in RRF help or hurt?

A higher k flattens the gap between ranks inside one list, which gives lower-ranked documents more influence and makes agreement between lists count for more. A lower k lets the top positions of each list dominate.

Is the RRF k the same as top-k?

No. The RRF constant is a smoothing term in the score formula. The number of results each search returns, and the number of chunks you pass to the model, are separate settings, even when an engine also calls them k.

Should I weight BM25 higher than vector search in RRF?

Only if your question set shows that keyword questions miss. On a short window with k=60, a weight has to be large before it changes which documents come first, so measure the effect rather than assume it.

Sources

  1. Reciprocal rank fusion outperforms Condorcet and individual rank learning methods (Cormack, Clarke and Buettcher, SIGIR 2009) (Paper)
  2. Hybrid search scoring (RRF), Azure AI Search docs (Documentation)
  3. RRF retriever, Elasticsearch docs (Documentation)
  4. Hybrid and multi-stage queries, Qdrant docs (Documentation)
  5. Result fusion, PyTerrier docs (Documentation)