Trovella Wiki

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 document d in source i
  • k is 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 valueEffect
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:

  1. Iterates keyword results. For each result, either creates a new entry in a Map<id, FusedResult> with inKeyword: true and an RRF score contribution, or adds to an existing entry's score.

  2. Iterates semantic results. Same logic, setting inSemantic: true. If a document already exists from the keyword pass, its score accumulates.

  3. Sorts and truncates. Converts the map to an array, sorts by rrfScore descending, and slices to the requested limit.

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:

TestWhat It Verifies
Empty inputsReturns empty array when both sources are empty
Single-source resultsCorrectly handles keyword-only and semantic-only cases
Dual-source boostDocuments in both sets rank higher than single-source documents
Limit parameterTruncates output to the requested limit
Default k=60Score equals 1/61 for a rank-1 single-source result
Custom kAccepts a custom k parameter and computes correctly
Sort orderOutput is sorted by RRF score descending
Snippet preservationKeyword snippets take priority over semantic snippets

On this page