How wigolo Implements Reciprocal-Rank Fusion: A Deep Dive into the RRF Search Algorithm
The wigolo search tool implements reciprocal-rank fusion by converting ranked result lists from multiple engines into position-based maps, then aggregating scores using the formula 1/(k + rank) where k defaults to 60, to produce a unified relevance ranking.
The KnockOutEZ/wigolo repository uses Reciprocal-Rank Fusion (RRF) to merge disparate search signals—including core rankings, SearXNG results, embedding-based ranks, and web ranks—into a single coherent ordering. This approach avoids the complexity of training a learned ranker while effectively combining the strengths of multiple retrieval sources. The implementation lives primarily in TypeScript modules under src/search/, with a configurable constant allowing fine-tuning of the fusion behavior.
Core RRF Implementation in src/search/rrf.ts
The heart of wigolo's fusion logic resides in src/search/rrf.ts, which exports three key utilities: buildRankMap, reciprocalRankFusion, and sortByRRFScore. These functions transform ranked lists from individual engines into a combined score map.
Building Rank Maps with buildRankMap
Before fusion, each engine's ordered results must be converted into a Map<string, number> where keys are canonical URLs and values represent 1-based positions. The buildRankMap function performs this conversion, creating the input structure required by the RRF algorithm.
The reciprocalRankFusion Function
The actual fusion applies the classic RRF formula. For every URL appearing in any input list, the system sums contributions calculated as 1/(k + rank), where k defaults to 60 (configurable via the k parameter).
// src/search/rrf.ts
export function reciprocalRankFusion(
lists: Map<string, number>[],
k: number = 60,
): Map<string, number> {
const scores = new Map<string, number>();
for (const list of lists) {
for (const [url, rank] of list) {
const contribution = 1 / (k + rank);
scores.set(url, (scores.get(url) ?? 0) + contribution);
}
}
return scores;
}
The function accepts an array of rank maps (lists) and returns a single Map<string, number> containing fused scores for each unique URL.
Normalizing Results with sortByRRFScore
After fusion, sortByRRFScore converts the score map into a descending sorted array. Higher scores indicate stronger consensus across engines.
export function sortByRRFScore(scores: Map<string, number>) {
return [...scores.entries()].sort((a, b) => b[1] - a[1]);
}
The hybrid merger then normalizes these scores so the top result receives a relevance_score of 1.0, with lower results scaled proportionally.
Integrating RRF into the Search Pipeline
The RRF logic integrates at multiple points within wigolo's search architecture, primarily through src/search/hybrid/merge.ts and src/search/find-similar.ts.
Hybrid Search Merging
When operating in hybrid mode, src/search/hybrid/merge.ts orchestrates the fusion of core and SearXNG result sets. The mergeResults function collects keyed results from each provider, builds rank maps, and invokes reciprocalRankFusion.
// src/search/hybrid/merge.ts (excerpt)
const coreKeys = collectKeyedResults(core, byKey);
const searxngKeys = collectKeyedResults(searxng, byKey);
const lists: Map<string, number>[] = [];
if (coreKeys.length) lists.push(buildRankMap(coreKeys));
if (searxngKeys.length) lists.push(buildRankMap(searxngKeys));
const fused = reciprocalRankFusion(lists, RRF_K);
const sorted = sortByRRFScore(fused);
Each result is deduplicated using collectKeyedResults, which maps canonical URLs to SearchResultItem objects before rank map construction.
Find-Similar Query Processing
In src/search/find-similar.ts, RRF combines embedding-rank and web-rank signals for similarity queries. This module exposes the internal ranking_debug.rrf_score field when debugging is enabled, allowing inspection of each result's raw fused contribution before normalization.
Configuring the RRF Constant
The k constant controls how aggressively rank positions discount scores. While the default value of 60 works well for general search blending, callers can override this when invoking reciprocalRankFusion directly. Lower values emphasize top-ranked items more heavily; higher values create a more uniform distribution across the result set.
Practical Code Examples
Manual RRF Fusion of Two Ranked Lists
import { buildRankMap, reciprocalRankFusion, sortByRRFScore } from './search/rrf.js';
// Ranked URL arrays (highest rank first)
const listA = ['https://a.com/doc1', 'https://a.com/doc2', 'https://a.com/doc3'];
const listB = ['https://b.com/doc2', 'https://b.com/doc1', 'https://a.com/doc3'];
const rankMapA = buildRankMap(listA); // Maps to {url→1, url→2, ...}
const rankMapB = buildRankMap(listB);
const fused = reciprocalRankFusion([rankMapA, rankMapB], 60);
const ordered = sortByRRFScore(fused);
console.log(ordered);
// [['https://a.com/doc3', 0.0327], ['https://a.com/doc1', 0.0164], ...]
Using the Built-in Hybrid Merge API
import { mergeResults } from './search/hybrid/merge.js';
import { coreSearch, searxngSearch } from './providers/index.js';
async function hybridSearch(query: string) {
const core = await coreSearch(query);
const searx = await searxngSearch(query);
const merged = mergeResults(core, searx, {
maxResults: 10,
includeRankingDebug: true
});
return merged.results; // RRF-fused with relevance_score normalized to top=1.0
}
When includeRankingDebug is enabled, results include the ranking_debug.rrf_score field showing the raw fused value before normalization.
Summary
- Core Algorithm: The
reciprocalRankFusionfunction insrc/search/rrf.tsimplements the standard RRF formula 1/(k + rank) with a default k=60. - Pipeline Integration: RRF is invoked by
src/search/hybrid/merge.tsfor combining core and SearXNG results, and bysrc/search/find-similar.tsfor embedding-web fusion. - Input Preparation: The
buildRankMaputility converts ordered result arrays into rank-position maps required by the fusion algorithm. - Score Exposure: The system exposes raw RRF contributions via
ranking_debug.rrf_scorefor debugging and audit purposes. - Normalization: Fused scores are sorted and normalized so the highest score equals 1.0, producing the final
relevance_score.
Frequently Asked Questions
What is the default k value in wigolo's RRF implementation and why is it set to 60?
The default k constant is 60, defined as the default parameter in reciprocalRankFusion. This value follows established information retrieval literature where k=60 provides a balanced dampening effect, preventing top-ranked items from dominating while still allowing lower-ranked items to contribute meaningfully to the fused score.
How does wigolo handle duplicate URLs when merging results from different engines?
Duplicates are managed by collectKeyedResults in src/search/hybrid/merge.ts, which creates a canonical key for each URL before building rank maps. When the same URL appears across multiple engines, its contributions are summed within the reciprocalRankFusion loop, effectively boosting its final score based on consensus across sources.
What is the purpose of the ranking_debug.rrf_score field?
The ranking_debug.rrf_score field exposes the raw reciprocal-rank fusion value for a result before normalization occurs. Developers enable this by setting includeRankingDebug: true when calling merge functions, allowing inspection of how each engine contributed to the final ranking and facilitating debugging of fusion behavior.
Can the RRF constant k be customized when using the hybrid merge API?
Yes, while mergeResults in src/search/hybrid/merge.ts typically uses a default constant, the underlying reciprocalRankFusion function accepts k as a configurable parameter. Direct callers can pass custom values to adjust the balance between high-ranked precision and recall diversity, though the hybrid merger defaults to the standard 60.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →