# How wigolo Implements Reciprocal-Rank Fusion: A Deep Dive into the RRF Search Algorithm

> Discover how wigolo implements reciprocal-rank fusion in its search algorithm. Learn how ranked lists are combined for a unified relevance ranking with the 1/(k + rank) formula.

- Repository: [Towhid Khan/wigolo](https://github.com/KnockOutEZ/wigolo)
- Tags: deep-dive
- Published: 2026-07-20

---

**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`](https://github.com/KnockOutEZ/wigolo/blob/main/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).

```typescript
// 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.

```typescript
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`](https://github.com/KnockOutEZ/wigolo/blob/main/src/search/hybrid/merge.ts) and [`src/search/find-similar.ts`](https://github.com/KnockOutEZ/wigolo/blob/main/src/search/find-similar.ts).

### Hybrid Search Merging

When operating in hybrid mode, [`src/search/hybrid/merge.ts`](https://github.com/KnockOutEZ/wigolo/blob/main/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`.

```typescript
// 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`](https://github.com/KnockOutEZ/wigolo/blob/main/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

```typescript
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

```typescript
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 `reciprocalRankFusion` function in [`src/search/rrf.ts`](https://github.com/KnockOutEZ/wigolo/blob/main/src/search/rrf.ts) implements the standard RRF formula **1/(k + rank)** with a default **k=60**.
- **Pipeline Integration**: RRF is invoked by [`src/search/hybrid/merge.ts`](https://github.com/KnockOutEZ/wigolo/blob/main/src/search/hybrid/merge.ts) for combining core and SearXNG results, and by [`src/search/find-similar.ts`](https://github.com/KnockOutEZ/wigolo/blob/main/src/search/find-similar.ts) for embedding-web fusion.
- **Input Preparation**: The `buildRankMap` utility 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_score` for 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`](https://github.com/KnockOutEZ/wigolo/blob/main/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`](https://github.com/KnockOutEZ/wigolo/blob/main/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.