If you've built a RAG application, you’ve probably hit this wall:
- Keyword search (Best Match 25 [BM25]) is great at exact matches.
- Vector search captures semantic meaning.
- But combining the two ranked lists is painful because their scores live on completely different scales.
Manually normalizing scores is fragile. Reciprocal Rank Fusion (RRF) solves this elegantly by ignoring raw scores entirely and only looking at rank positions.
RRF has become the standard fusion method in MemoFS, Elasticsearch, OpenSearch, Azure AI Search, Weaviate, MongoDB Atlas, and modern RAG pipelines.
In this post, I’ll show you exactly how RRF works and give you clean, production-ready TypeScript implementations.
How RRF Works
RRF assigns a score to each document using this simple formula:
Where:
- = document
- = list of ranked result sets
- = position of the document in that result set (starting at 1)
- = smoothing constant (default: 60)
Documents are then sorted by their final RRF score (highest first).
TypeScript Implementation
Here’s a clean, generic, and reusable TypeScript version:
/**
* Reciprocal Rank Fusion (RRF)
* Fuses multiple ranked lists into a single ranked list
*/
export function reciprocalRankFusion<T>(
rankedLists: T[][],
options: {
k?: number; // Smoothing constant (default: 60)
topK?: number; // Limit number of results returned
} = {}
): Array<{ item: T; score: number }> {
const { k = 60, topK } = options;
const rrfScores = new Map<T, number>();
for (const rankedList of rankedLists) {
rankedList.forEach((item, index) => {
const rank = index + 1;
const currentScore = rrfScores.get(item) ?? 0;
rrfScores.set(item, currentScore + 1 / (k + rank));
});
}
// Convert to array and sort by score (descending)
let results = Array.from(rrfScores.entries()).map(([item, score]) => ({
item,
score,
}));
results.sort((a, b) => b.score - a.score);
if (topK !== undefined) {
results = results.slice(0, topK);
}
return results;
}
Usage example:
const bm25Results = ["doc_B", "doc_A", "doc_D", "doc_C"];
const vectorResults = ["doc_B", "doc_E", "doc_A", "doc_F"];
const fusedResults = reciprocalRankFusion([bm25Results, vectorResults], {
k: 60,
topK: 10,
});
console.log(fusedResults);
// [
// { item: 'doc_B', score: 0.03125 },
// { item: 'doc_A', score: 0.02548 },
// ...
// ]
Note: This implementation works best when items are primitives (
string,number) or the same object references. For complex objects, consider using document IDs or aMapwith string keys.
Working with LangChain.js
You can easily integrate RRF into LangChain.js workflows. Here’s a practical pattern:
import { EnsembleRetriever } from "langchain/retrievers/ensemble"; // or implement manually
import { BM25Retriever } from "@langchain/community/retrievers/bm25";
import { VectorStoreRetriever } from "langchain/vectorstores/base";
// Example: Combine BM25 + Vector retriever
const bm25Retriever = BM25Retriever.fromDocuments(docs);
const vectorRetriever = vectorStore.asRetriever({ k: 20 });
const ensembleRetriever = new EnsembleRetriever({
retrievers: [bm25Retriever, vectorRetriever],
weights: [0.5, 0.5],
// Internally uses weighted RRF
});
const results = await ensembleRetriever.invoke("your query");
If you prefer full control, just use the reciprocalRankFusion function above after fetching results from multiple retrievers.
MemoFS handles this automatically for you out of the box.
Why RRF is So Effective
- No score normalization required
- Rewards documents that rank well across multiple methods
- Extremely robust (proven in the original 2009 SIGIR paper)
- Only one parameter (
k), and60works great in most cases
Best Practices in TypeScript Projects
- Fetch more candidates than you need (
k: 20–50per retriever) - Fuse and then take the top 5–10 results
- Use document IDs when working with objects
- Consider adding light weighting when one retrieval method is consistently stronger
Are you a developer using coding agents, building RAG systems, or implementing AI memories?
Check out the full documentation and ready-to-use AI agents/apps memory at:
Whether you're deploying to a Node environment or Cloudflare workers, you'll find practical guides to take your retrieval quality to the next level.
What’s your current approach to hybrid search? Are you still normalizing scores, or have you switched to RRF? Let me know in the comments!
Top comments (0)