fix(rag): rank keyword candidates in SQL before the LIMIT truncates them - #523
Merged
Conversation
keywordSearchRagTable ORed the per-term ILIKE patterns and then applied LIMIT 20 with no ORDER BY, so Postgres could return any 20 of the matching rows. rankByTerms then ranked that arbitrary sample precisely. The comment claimed "over-fetch, then rank precisely" and did neither. Measured on the prod corpus for golden case adr-langgraph (7 terms, 256 of 478 chunks match at least one): before 20 candidates, 0 chunks of the target doc, overlap 1-2 of 7 after 20 candidates, 3 chunks of the target doc, overlap 2-6 of 7 The target was never in the candidate pool, which is why keyword-only recall@5 measured 0.0% on 37/37 golden queries with no error and no empty result to make it visible. Fix: build the SQL mirror of scoreByTerms (one CASE arm per term, summed), ORDER BY match_count DESC, content — the second key keeps ties deterministic per rule #16 — then LIMIT. rankByTerms still refines the top candidates. Gate: 298 files / 3198 tests green; all six architecture gates at baseline.
pushkarverma3698
force-pushed
the
fix/keyword-rag-ranking
branch
from
August 20, 2026 06:39
d5dac40 to
43654c8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the 0.0% keyword recall measured by #515 (AG-010). #515 measured the defect; this fixes it.
The bug
keywordSearchRagTableORed the per-termILIKEpatterns, then appliedLIMIT 20with noORDER BY. Postgres was free to return any 20 matching rows.rankByTermsthen ranked that arbitrary sample precisely.The comment said
// over-fetch, then rank precisely. It did neither.Nothing errored and nothing came back empty, so from the outside a dead keyword lane and an unhelpful one were indistinguishable — for weeks.
Failing test first
tests/unit/db/keyword-rag-ranking.test.tscaptures the SQL the function actually sends and asserts the ranking happens before the truncation. Before the fix it captured, verbatim:3 of 4 assertions failed. All 4 pass after.
Measured on the prod corpus, not asserted
Golden case
adr-langgraph— 7 significant terms, 256 of 478 chunks match at least one:The target document was never in the pool. recall@5 = 0% was structurally guaranteed, not a ranking weakness.
The fix
Build the SQL mirror of
scoreByTerms— oneCASE WHEN content ILIKE … THEN 1 ELSE 0 ENDper term, summed asmatch_count— thenORDER BY match_count DESC, contentbeforeLIMIT. The second sort key keeps ties deterministic (rule #16: retrieval is reproducible, two identical runs cannot disagree).rankByTermsstill refines the top candidates in JS, and now receives the right ones.Test plan
pnpm gate— 298 files / 3198 tests greenpnpm verify:arch— all six gates= baselinepnpm lint— exit 0pnpm eval:retrievalrecall@5/MRR re-run. It needs a local corpus + Ollama; this machine has no local Postgres up. The numbers to beat are hybrid 83.8% / MRR 0.674 vs vector-only 83.8% / 0.761 — fusion is currently negative-value because one input is noise. Re-run before judging RRF.