Repository navigation
perf(core): read FTS chunk candidates from the GIN index - #1795
Merged
Merged
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
phernandez
added this pull request to stack #1797
October 20, 2026 15:56
Chunk tsvectors live in TOAST, which the planner does not cost. It priced a sequential scan of search_index_fts_chunks at the heap's size and chose it over the GIN index, then detoasted every chunk: about 375 MB per query on a 95k-chunk project, half the buffer touches left after the candidate fence. A GIN bitmap reads only matching rows and never detoasts; on a production-shaped table it won at every selectivity measured (921 buffers against 481k with every chunk matching). FTS statements with a text candidate join now run with sequential scans off. The setting is local to the statement: a savepoint restores it before release or reverts it on rollback, and FTS's own session ends with its transaction. Refs #1791 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N9c6ouiT4Wid3arGR41Dv Signed-off-by: phernandez <paul@basicmachines.co>
phernandez
force-pushed
the
search-fts-chunk-gin
branch
from
October 20, 2026 16:07
300ad50 to
58e72b0
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.
Part 3 of #1791, stacked on #1794: the chunk-candidate leg.
Problem
The FTS candidate UNION reads
search_index_fts_chunksfor chunks matching the query's operands. The chunktsvectors are stored in TOAST, and the planner does not cost TOAST. It priced a sequential scan at the size of the heap: 81 MB on the #1791 tenant, against 375 MB of TOAST. So it chose the sequential scan overidx_search_index_fts_chunks_fts, then detoasted every chunk to test it.After #1794 this leg is about half of the remaining buffer touches (259k of 516k on the measured query). Production: 19.5 s cold, 0.43 s warm.
Change
FTS statements that join text candidates (
CompiledFilter.prefer_index_scans) run withenable_seqscanoff. Statements without a text join, such as filtered listings, plan normally.The setting is local to the statement:
enable_seqscan = offdoes not forbid sequential scans. It prices them out, so a table with no usable index still plans.Evidence
On a production-shaped local table (95k chunks, 939 MB TOAST, 7 MB heap, GIN index):
The GIN bitmap is exact, so it never rechecks and never detoasts. In production the planner estimated 16% of chunks matching and chose the sequential scan.
I could not measure the whole statement in production: the read-only connection rejects
SET LOCAL. To confirm before or after deploy, run this in the Neon SQL editor on a large tenant, with the rendered FTS SQL from #1794:The
Seq Scan on search_index_fts_chunks candidate_chunknode (about 259k buffers) should become a Bitmap Heap Scan.Tests
test_postgres_chunk_candidates_use_the_gin_index: the compiled search statement, run throughexecute_fts_statement, plans noSeq Scanonsearch_index_fts_chunks.test_postgres_fts_leaves_the_callers_session_planning_normally: after a search on a caller's session,enable_seqscanisonagain. Covered for a normal query and for a malformed one that takes the relaxed retry.'Seq Scan' not in {'Index Scan', 'Seq Scan'}). Dropping the restore fails both session cases. Restored, all pass.pymilvuserrors. SQLite is unaffected (the flag defaults to false).Still open in #1791
search.countrepeats the FTS cost. perf(core): run FTS text conditions only on the GIN candidates #1794's fence and this PR both apply to it, since it uses the same compiled filter. Capping it or turning it into a has-more probe changes the API'stotal, which is a product call.🤖 Generated with Claude Code
https://claude.ai/code/session_016N9c6ouiT4Wid3arGR41Dv