fix: give each query lexeme its own BM25 candidate budget - #279
Open
nuemaan wants to merge 1 commit into
Open
Conversation
The FTS prefilter bounded candidates with a single global ts_rank_cd ordering. ts_rank_cd scores term density inside a chunk and ignores how rare a term is across the corpus, while BM25 weights rare terms heavily. A short chunk holding the one rare term in a query therefore sorts near the bottom of that ordering and is truncated first, even though BM25 ranks it top. Measured on 5001 chunks where 5000 densely repeat a common term and one holds a rare term: the rare chunk ranks 5001 of 5001 under ts_rank_cd and 1 of 5001 under rank_rows_by_bm25, so a 2000 candidate limit dropped the best match before BM25 ran. Each lexeme now draws from its own share of the budget through a lateral join, so a lexeme matching few chunks always contributes them. A floor keeps many-lexeme queries from dividing the budget into slivers. The same corpus now yields 1001 candidates including the rare chunk, fewer rows than the old path loaded while keeping the match that matters. Also logs a warning when the pool saturates. The debug line reported candidates == limit whether the corpus held exactly that many or far more, so silent truncation looked identical to a healthy query. The bounded-prefilter test asserted on the literal LIMIT clause as a position marker. Its intent, that scope filters land ahead of any candidate bound, is unchanged and now asserts against the per-lexeme clause. Closes Ontos-AI#278
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.
Summary
ts_rank_cdordering. That ordering scores term density inside a chunk and ignores corpus-wide rarity, while BM25 weights rare terms heavily, so the chunk BM25 ranks first can be the first one truncated._MIN_CANDIDATES_PER_LEXEMEkeeps many-lexeme queries from dividing the budget into slivers.candidates == limitwhether the corpus held exactly that many or far more, so silent truncation was indistinguishable from a healthy query.Measured on 5001 chunks where 5000 densely repeat a common term and one holds a rare term, querying
data zebra:The per-lexeme pool is smaller and still keeps the match BM25 wants. Cost is one GIN probe per lexeme rather than one overall, and
_MAX_FTS_QUERY_TOKENSalready caps that at 50.Verification
uv run pytest apps/api/tests/unit/test_bm25_channel_tsquery.py apps/api/tests/contract/test_bm25_fts_prefilter_contract.py— 16 passed against real Postgres 16uv run pytest packages/shared-python/shared/tests/test_retrieval_search_channels.py— 4 passeduvx ruff check packages/shared-python apps/api/tests— cleanAssertionError: assert 'common-2' == 'rare-zebra'test_content_channel_uses_bounded_or_fts_after_scope_filtersasserted on the literalLIMIT :fts_candidate_limitas a position marker for the candidate bound. That clause is nowLIMIT lb.per_lexeme_limit, so the assertions were repointed. The intent from #244, that scope filters land ahead of any candidate bound so exclusions cannot consume the budget, is unchanged and still asserted.Not tested: production-scale corpora, and lateral-probe cost at the 50 lexeme ceiling on a large namespace. Both need a dataset I do not have locally.
Deployment Notes
RETRIEVAL_POSTGRES_FTS_CANDIDATE_LIMITkeeps its meaning as the total budget, now spent per lexeme.Checklist