1import { describe, expect, it } from "vitest";
2
3import { RRF_K, fuseHybrid, reciprocalRankFusion } from "./rrf.js";
4
5describe("reciprocalRankFusion", () => {
6 it("uses the standard constant k=60", () => {
7 expect(RRF_K).toBe(60);
8 });
9
10 it("scores a doc in both lists higher than a doc in one", () => {
11 const fused = reciprocalRankFusion([
12 { name: "bm25", docIds: ["a", "b"] },
13 { name: "vec", docIds: ["a", "c"] },
14 ]);
15
16 expect(fused[0]!.docId).toBe("a");
17 expect(fused[0]!.score).toBeCloseTo(2 / 61, 12);
18 expect(fused[0]!.sources).toEqual(["bm25", "vec"]);
19 expect(fused[1]!.score).toBeCloseTo(1 / 62, 12);
20 expect(fused[2]!.score).toBeCloseTo(1 / 62, 12);
21 });
22
23 it("rank matters: 2nd place contributes 1/(k+2)", () => {
24 const fused = reciprocalRankFusion([{ name: "x", docIds: ["a", "b", "c"] }]);
25 expect(fused[0]!.docId).toBe("a");
26 expect(fused[0]!.score).toBeCloseTo(1 / 61, 12);
27 expect(fused[1]!.score).toBeCloseTo(1 / 62, 12);
28 expect(fused[2]!.score).toBeCloseTo(1 / 63, 12);
29 });
30
31 it("a doc ranked 1 in one list beats a doc ranked 1+1 in both only when the sum is higher", () => {
32
33 const fused = reciprocalRankFusion([
34 { name: "l1", docIds: ["x", "y"] },
35 { name: "l2", docIds: ["x"] },
36 ]);
37 expect(fused[0]!.docId).toBe("x");
38 expect(fused[1]!.docId).toBe("y");
39 });
40
41 it("breaks score ties deterministically by docId", () => {
42 const fused = reciprocalRankFusion([
43 { name: "a", docIds: ["z"] },
44 { name: "b", docIds: ["m"] },
45 ]);
46 expect(fused.map((f) => f.docId)).toEqual(["m", "z"]);
47 });
48
49 it("respects limit", () => {
50 const fused = reciprocalRankFusion(
51 [{ name: "x", docIds: ["a", "b", "c", "d"] }],
52 60,
53 2,
54 );
55 expect(fused).toHaveLength(2);
56 });
57
58 it("handles empty lists", () => {
59 expect(reciprocalRankFusion([{ name: "x", docIds: [] }])).toEqual([]);
60 });
61
62 it("throws on non-positive k", () => {
63 expect(() => reciprocalRankFusion([], 0)).toThrow();
64 });
65
66 it("fuseHybrid names the sources bm25/vec", () => {
67 const fused = fuseHybrid(["a", "b"], ["b", "c"]);
68 const b = fused.find((f) => f.docId === "b");
69 expect(b?.sources).toEqual(["bm25", "vec"]);
70 expect(fused[0]!.docId).toBe("b");
71 });
72});
73