From da8ad27afcc85bab638c5e26ac6c1770094aa869 Mon Sep 17 00:00:00 2001 From: atoz96 Date: Wed, 29 Jul 2026 20:31:36 +0300 Subject: [PATCH] fix(context): apply rerank on embedding and fts5 backends too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit retrieval.rerank is global config, but _retrieve only ran the rerank stage on the auto/hybrid branch. an operator who pinned retrieval.backend to embedding or fts5 and set retrieval.rerank.enabled: true got no reranking at all, silently — the setting reads as active in config.yaml and does nothing. the embedding branch already carries the comment for exactly this class of bug ("parity with the hybrid path: an operator who opted into recency gets it regardless of which backend serves the query"); rerank was missed when that parity was established. same stage, same order — recency, pages-first, then rerank — on both single-retriever branches. the substring fall-through is left alone: it is the last-resort scan after both retrievers came back empty, and it applies none of the three stages today. covered by a parametrized regression over both backends; it fails on the previous code with the unreranked order. --- src/vouch/context.py | 5 ++++- tests/test_retrieval_backend.py | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/vouch/context.py b/src/vouch/context.py index 2eaa8707..fdfb55ba 100644 --- a/src/vouch/context.py +++ b/src/vouch/context.py @@ -455,9 +455,11 @@ def _retrieve( if raw: filtered = filter_hits(store, raw, viewer, limit=limit) # Parity with the hybrid path: an operator who opted into - # recency gets it regardless of which backend serves the query. + # recency or rerank gets it regardless of which backend serves + # the query — both are configured globally, not per backend. filtered = _maybe_recency(store, hits=filtered) filtered = _maybe_pages_first(store, hits=filtered) + filtered = _maybe_rerank(store, query=query, hits=filtered, limit=limit) return [(k, i, s, sc, "embedding") for k, i, s, sc in filtered] return [] @@ -468,6 +470,7 @@ def _retrieve( filtered = filter_hits(store, hits, viewer, limit=limit) filtered = _maybe_recency(store, hits=filtered) filtered = _maybe_pages_first(store, hits=filtered) + filtered = _maybe_rerank(store, query=query, hits=filtered, limit=limit) return [(k, i, s, sc, "fts5") for k, i, s, sc in filtered] except sqlite3.Error: pass diff --git a/tests/test_retrieval_backend.py b/tests/test_retrieval_backend.py index 344888fa..4e1d552b 100644 --- a/tests/test_retrieval_backend.py +++ b/tests/test_retrieval_backend.py @@ -241,6 +241,43 @@ def fake_rerank(*, query, hits, reranker, top_k): assert [item["id"] for item in pack["items"]] == ["c3", "c2", "c1"] +@pytest.mark.parametrize("backend", ["embedding", "fts5"]) +def test_context_rerank_applies_on_single_retriever_backends( + store: KBStore, monkeypatch: pytest.MonkeyPatch, backend: str +) -> None: + """retrieval.rerank is global config, so it must not be hybrid-only. + + An operator pinning retrieval.backend to embedding or fts5 and enabling + rerank previously got no reranking at all, silently — the stage only ran + on the auto/hybrid branch. + """ + from vouch.embeddings import rerank as rerank_mod + + src = store.put_source(b"e2") + store.put_claim(Claim(id="c2", text="OAuth refresh flow", evidence=[src.id])) + health.rebuild_index(store) + hits = [ + ("claim", "c1", "JWT token rotation", 0.90), + ("claim", "c2", "OAuth refresh flow", 0.80), + ] + monkeypatch.setattr(context.index_db, "search_semantic", lambda *a, **k: list(hits)) + monkeypatch.setattr(context.index_db, "search", lambda *a, **k: list(hits)) + monkeypatch.setattr(context, "_RERANKER_CACHE", None) + monkeypatch.setattr(rerank_mod, "default_reranker", lambda: object()) + monkeypatch.setattr( + rerank_mod, + "rerank", + lambda *, query, hits, reranker, top_k: list(reversed(hits))[:top_k], + ) + _set_backend(store, backend) + _set_rerank(store, enabled=True, top_k=2) + + pack = context.build_context_pack(store, query="auth", limit=3) + + assert _backends(pack) == {backend} + assert [item["id"] for item in pack["items"]] == ["c2", "c1"] + + def test_context_rerank_reuses_default_reranker( store: KBStore, monkeypatch: pytest.MonkeyPatch ) -> None: