diff --git a/src/state/reranker.ts b/src/state/reranker.ts index d0aae68c..69a2a1b2 100644 --- a/src/state/reranker.ts +++ b/src/state/reranker.ts @@ -53,18 +53,27 @@ export async function rerank( for (const pair of pairs) { try { const output = await reranker(pair.text); - const score = Array.isArray(output) ? output[0]?.score ?? 0 : 0; + const rawScore = Array.isArray(output) ? output[0]?.score : undefined; + const score = Number.isFinite(rawScore) ? Number(rawScore) : pair.result.combinedScore; scores.push({ result: pair.result, rerankScore: score }); } catch { scores.push({ result: pair.result, rerankScore: pair.result.combinedScore }); } } + const distinctScores = new Set(scores.map((s) => s.rerankScore)); + if (distinctScores.size <= 1) { + return candidates.map((result, i) => ({ + ...result, + rerankPosition: i + 1, + })); + } + scores.sort((a, b) => b.rerankScore - a.rerankScore); return scores.map((s, i) => ({ ...s.result, - combinedScore: s.rerankScore, + rerankScore: s.rerankScore, rerankPosition: i + 1, })); } diff --git a/src/types.ts b/src/types.ts index b734a4d2..0d54c49c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -257,6 +257,7 @@ export interface HybridSearchResult { vectorScore: number; graphScore: number; combinedScore: number; + rerankScore?: number; sessionId: string; graphContext?: string; } diff --git a/test/reranker.test.ts b/test/reranker.test.ts index 0694e6d5..44ff4ee9 100644 --- a/test/reranker.test.ts +++ b/test/reranker.test.ts @@ -1,19 +1,25 @@ import { describe, it, expect, vi } from "vitest"; vi.mock("@xenova/transformers", () => { - throw new Error("not installed"); + return { + pipeline: vi.fn(async () => vi.fn(async (text: string) => { + if (text.includes("same-score")) return [{ label: "LABEL_0", score: 1 }]; + if (text.includes("strong-match")) return [{ label: "LABEL_0", score: 0.9 }]; + return [{ label: "LABEL_0", score: 0.1 }]; + })), + }; }); import { rerank, isRerankerAvailable } from "../src/state/reranker.js"; describe("reranker", () => { - it("returns results unchanged when @xenova/transformers is unavailable", async () => { + it("keeps retrieval scores when the reranker returns constant scores", async () => { const results = [ { observation: { id: "o1", title: "First", - narrative: "First result", + narrative: "same-score first result", }, bm25Score: 0.5, vectorScore: 0.6, @@ -25,7 +31,7 @@ describe("reranker", () => { observation: { id: "o2", title: "Second", - narrative: "Second result", + narrative: "same-score second result", }, bm25Score: 0.3, vectorScore: 0.4, @@ -36,11 +42,76 @@ describe("reranker", () => { ] as any; const reranked = await rerank("test query", results); - expect(reranked).toEqual(results); + expect(reranked.map((r) => r.observation.id)).toEqual(["o1", "o2"]); + expect(reranked.map((r) => r.combinedScore)).toEqual([0.8, 0.5]); + expect(reranked.map((r) => r.rerankPosition)).toEqual([1, 2]); }); - it("isRerankerAvailable returns false when not loaded", () => { - expect(isRerankerAvailable()).toBe(false); + it("uses discriminative reranker scores for ordering without overwriting retrieval score", async () => { + const results = [ + { + observation: { + id: "o1", + title: "First", + narrative: "weak-match first result", + }, + bm25Score: 0.5, + vectorScore: 0.6, + graphScore: 0, + combinedScore: 0.8, + sessionId: "s1", + }, + { + observation: { + id: "o2", + title: "Second", + narrative: "strong-match second result", + }, + bm25Score: 0.3, + vectorScore: 0.4, + graphScore: 0, + combinedScore: 0.5, + sessionId: "s1", + }, + ] as any; + + const reranked = await rerank("test query", results); + expect(reranked.map((r) => r.observation.id)).toEqual(["o2", "o1"]); + expect(reranked[0].combinedScore).toBe(0.5); + expect(reranked[0].rerankScore).toBe(0.9); + expect(reranked[0].rerankPosition).toBe(1); + }); + + it("isRerankerAvailable reflects the loaded pipeline", async () => { + const results = [ + { + observation: { + id: "o1", + title: "Availability", + narrative: "strong-match availability result", + }, + bm25Score: 0.5, + vectorScore: 0.6, + graphScore: 0, + combinedScore: 0.8, + sessionId: "s1", + }, + { + observation: { + id: "o2", + title: "Second", + narrative: "weak-match second result", + }, + bm25Score: 0.3, + vectorScore: 0.4, + graphScore: 0, + combinedScore: 0.5, + sessionId: "s1", + }, + ] as any; + + await rerank("test query", results); + expect(isRerankerAvailable()).toBe(true); }); it("handles single result gracefully", async () => {