A hybrid agent memory queries several retrievers at once - a dense vector index, a BM25 lexical index, a graph walk - and each returns its own ranked list. Their scores are on incompatible scales, so you cannot simply average them. Reciprocal Rank Fusion (RRF) solves this by throwing the scores away and fusing on rank alone.
For a document d appearing at 1-based rank ri in ranked list i, RRF assigns
RRF(d)=∑i:d∈Lik+ri1
The constant k (conventionally 60) damps the influence of the very top of any single list, so a document ranked #1 by one retriever cannot dominate a document ranked #2 or #3 by all of them. That consensus bias is exactly why RRF is the default fusion step in hybrid memory stores.
Implement:
def reciprocal_rank_fusion(rankings, k=60, top_n=None):
Steps:
Returns list[list] - each element is [str, float]. Rounding happens before sorting, so two documents whose scores agree to 6 dp are ordered by id.
lists = [["a", "b", "c"], ["b", "a", "d"]]
print(reciprocal_rank_fusion(lists, k=1))
# [['a', 0.833333], ['b', 0.833333], ['c', 0.25], ['d', 0.25]]
a scores 1/2 + 1/3 = 0.833333 and b scores 1/3 + 1/2 = 0.833333 - a tie, broken by id.
print(reciprocal_rank_fusion([['a','b','c'], ['b','a','d']], k=1))
[['a', 0.833333], ['b', 0.833333], ['c', 0.25], ['d', 0.25]]
With k=1: a is rank 1 then rank 2, giving 1/2 + 1/3 = 0.8333333 -> 0.833333. b is rank 2 then rank 1, the same total. c only appears at rank 3 (1/4 = 0.25) and d only at rank 3 (0.25). Both ties are broken alphabetically.
1 <= len(rankings) <= 20, each inner list has at most 1000 idsk >= 1; ranks are 1-based (the first element of a list has rank 1)