RRF 名次、去重和分数
策略的具体实现如下。
ts
export function reciprocalRankFusion(
lists: readonly RankedList[],
options: { limit: number; k?: number },
): FusedRank[] {
// 60 is the smoothing constant popularised by the original RRF paper and keeps rank
// differences meaningful without letting the top hit dominate.
const k = options.k ?? 60,
sources = new Set<string>(),
candidates = new Map<string, FusedRank>();
breakWhile(
!Number.isFinite(k) ||
k < 0 ||
!Number.isSafeInteger(options.limit) ||
options.limit < 1 ||
options.limit > 1000,
'Invalid RRF options',
);
for (const list of lists) {
const weight = list.weight ?? 1,
sourceId = list.sourceId;
breakWhile(
!sourceId?.trim() ||
sources.has(sourceId) ||
!Array.isArray(list.ids) ||
list.ids.some((id) => typeof id !== 'string' || !id.trim()) ||
!Number.isFinite(weight) ||
weight <= 0,
'Invalid ranked source',
);
sources.add(sourceId);
const seen = new Set<string>();
for (const id of list.ids) {
if (seen.has(id)) continue;
seen.add(id);
const rank = seen.size;
const candidate = candidates.get(id) ?? {
id,
score: 0,
contributions: [],
};
candidate.score += weight / (k + rank);
candidate.contributions.push({ sourceId, rank, weight });
candidates.set(id, candidate);
}
}
return [...candidates.values()]
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
.slice(0, options.limit);
}