RRF Algorithm
Reciprocal Rank Fusion implementation -- formula, k parameter, mathematical properties, and code walkthrough.
Reciprocal Rank Fusion (RRF) is the algorithm that merges keyword and semantic search results into a single ranked list. The implementation lives in packages/search/src/fusion.ts.
This page focuses on the relevance implications of RRF -- how the k parameter affects result quality, the mathematical properties that matter for tuning, and the test coverage. For the mechanical description of how RRF fits into the query pipeline, see Fusion Algorithm.
The Formula
For each document d that appears in one or more result sets, the RRF score is:
RRF(d) = SUM( 1 / (k + rank_i(d)) ) for each source i where d appears
Where:
rank_i(d)is the 1-based rank position of documentdin sourceikis a smoothing constant (default: 60)- The sum is taken over all sources where the document appears
With two sources (keyword and semantic), a document appearing in both gets:
RRF(d) = 1/(k + rank_keyword) + 1/(k + rank_semantic)
A document appearing in only one source gets just one term.
The k Parameter
The smoothing constant k controls how much rank position matters relative to simply appearing in a result set.
k = 60 is the default, taken from the original Cormack, Clarke, and Buettcher (2009) paper. This value was empirically determined to work well across a range of information retrieval tasks.
How k Affects Scoring
| k value | Effect |
|---|---|
| Low (e.g., 1) | Rank position dominates. A rank-1 result scores 50% of maximum, rank-2 scores 33%. The gap between adjacent ranks is large. |
| Medium (e.g., 60) | Rank position matters but the gap between adjacent ranks is small. Rank-1 scores 1/61 = 0.0164, rank-2 scores 1/62 = 0.0161. The main signal is whether a document appears in both sources. |
| High (e.g., 1000) | Nearly flat -- all ranks score similarly. Appearing in both sources is almost all that matters. |
At k=60, the practical effect is:
- A document at rank 1 in one source scores
1/61 = 0.01639 - A document at rank 10 in one source scores
1/70 = 0.01429 - A document at rank 1 in both sources scores
1/61 + 1/61 = 0.03279 - A document at rank 1 in one source only scores
1/61 = 0.01639
The ratio between a dual-source rank-1 result and a single-source rank-1 result is 2:1. This means appearing in both sources is a much stronger signal than rank position within a single source.
Implementation Walkthrough
The reciprocalRankFusion() function in fusion.ts:
-
Iterates keyword results. For each result, either creates a new entry in a
Map<id, FusedResult>withinKeyword: trueand an RRF score contribution, or adds to an existing entry's score. -
Iterates semantic results. Same logic, setting
inSemantic: true. If a document already exists from the keyword pass, its score accumulates. -
Sorts and truncates. Converts the map to an array, sorts by
rrfScoredescending, and slices to the requestedlimit.
Key Types
interface RankedResult {
id: string; // Unique chunk ID
sourceTable: string; // "research_artifact" | "research_output" | "extraction_result"
sourceId: string; // ID of the source record
title: string;
rank: number; // 1-based rank in this source
score: number; // Raw score from the source (not used by RRF)
textSnippet?: string;
}
interface FusedResult {
id: string;
sourceTable: string;
sourceId: string;
title: string;
rrfScore: number; // Combined RRF score
inKeyword: boolean; // Appeared in BM25 results
inSemantic: boolean; // Appeared in vector results
textSnippet?: string;
}
Snippet Handling
When a document appears in both sources, the keyword snippet takes priority. If the keyword result has no snippet, the semantic snippet is used as a fallback. This preference exists because Typesense provides BM25-highlighted snippets with the matching terms emphasized, which are more useful for display than raw embedded text.
Properties of RRF
Score range. With two sources, the maximum possible score is 2/(k + 1) (rank 1 in both). The minimum is 1/(k + N) where N is the largest rank in the result set.
Rank-only. RRF deliberately ignores raw scores. A BM25 score of 500 vs 200 makes no difference if both documents are at the same rank. This is a strength when merging scores from different scales (BM25 scores are unbounded, cosine similarity is -1 to 1) but a weakness when score magnitude carries meaningful signal.
No weighting. The current implementation treats keyword and semantic sources equally. There is no source weight parameter that would let you bias toward one source. The Tuning Guide discusses when and how weighting might be added.
Deduplication by ID. Documents are identified by their chunk id. If the same logical document has different chunk IDs in keyword vs semantic results (e.g., different chunks from the same source document), RRF treats them as separate documents. The current system uses the same chunk IDs in both Typesense and pgvector, so this is not an issue in practice.
Test Coverage
The fusion logic has comprehensive unit tests in packages/search/src/__tests__/fusion.test.ts:
| Test | What It Verifies |
|---|---|
| Empty inputs | Returns empty array when both sources are empty |
| Single-source results | Correctly handles keyword-only and semantic-only cases |
| Dual-source boost | Documents in both sets rank higher than single-source documents |
| Limit parameter | Truncates output to the requested limit |
| Default k=60 | Score equals 1/61 for a rank-1 single-source result |
| Custom k | Accepts a custom k parameter and computes correctly |
| Sort order | Output is sorted by RRF score descending |
| Snippet preservation | Keyword snippets take priority over semantic snippets |
Related Pages
- Relevance Overview -- how RRF fits into the broader scoring system
- Tuning Guide -- practical guidance on adjusting k and other parameters
- Query Pipeline -- the pipeline that produces the ranked inputs to RRF