export function reciprocalRankFusion(
resultLists: RankedResult[][],
weights: number[] = [],
k: number = 60
): RankedResult[] {
const scores = new Map<string, { result: RankedResult; rrfScore: number; topRank: number }>();
for (let listIdx = 0; listIdx < resultLists.length; listIdx++) {
const list = resultLists[listIdx];
const weight = weights[listIdx] ?? 1.0;
for (let rank = 0; rank < list.length; rank++) {
const result = list[rank];
// RRF formula: weight / (k + rank + 1)
const rrfContribution = weight / (k + rank + 1);
const existing = scores.get(result.file);
if (existing) {
existing.rrfScore += rrfContribution;
existing.topRank = Math.min(existing.topRank, rank);
} else {
scores.set(result.file, {
result,
rrfScore: rrfContribution,
topRank: rank,
});
}
}
}
// Top-rank bonus
for (const entry of scores.values()) {
if (entry.topRank === 0) {
entry.rrfScore += 0.05; // #1 rank bonus
} else if (entry.topRank <= 2) {
entry.rrfScore += 0.02; // #2-3 rank bonus
}
}
return Array.from(scores.values())
.sort((a, b) => b.rrfScore - a.rrfScore)
.map(e => ({ ...e.result, score: e.rrfScore }));
}