You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Postgres search reads whole tables per query on large projects (admission count, FTS predicate placement, count) #1791
Measured on a tenant with 480k vector chunks and 95k FTS chunks, on core 91d83d6 (includes #1746). Four other large tenants show the same shape.
Defects, in priority order
Admission count (pgvector_index.PgVectorIndex.search, if admission is not None): SELECT count(*) FROM (... JOIN ... LIMIT :exact_cap) plans as a parallel hash join over both full tables. LIMIT cannot short-circuit the hash build. Measured on a 480k-chunk tenant: 8.2 s cold, 4.2 s warm, 318k pages. Fix ideas: decide from per-project manifest counts (or counts cached per scope and type), or count from search_vector_chunks alone with an index-only scan on (project_id, source_type, embedding_status). Acceptance: under 50 ms and under 1k pages at 480k chunks.
FTS predicate placement (compile_fts_filter / document_fts_vector_sql): the document-vector predicate and its per-operand EXISTS subqueries run on every row in scope, before the candidate join, because of the planner's 1-row estimate. Fix ideas: a MATERIALIZED candidate CTE joined by primary key, with operand presence computed in one grouped pass over search_index_fts_chunks restricted to candidates (bool_or(tsv @@ op_i) per search_index_id). Acceptance: pages touched scale with candidate count, not scope size.
Chunk candidate leg uses a seq scan. The GIN index on search_index_fts_chunks.textsearchable_index_col loses to a seq scan because TOAST is not costed. Each scan detoasts about 375 MB. Make the GIN path explicit.
Count: PostgresFts.count repeats the full FTS cost. Replace it with a limit+1 has-more probe, or a capped count.
Manifest join: the HNSW candidate path fetches fat search_vector_chunks heap rows only to check status columns. Add a covering index or move the status columns.
Test
Include a reproduction harness: 300k+ chunk fixture, cold cache (restart, or pg_prewarm off), and report EXPLAIN (ANALYZE, BUFFERS) page counts, not only timings. Warm-cache benchmarks hide all of this. The #1746 numbers were warm.
Cloud is adding a per-tenant search concurrency guard separately. Raising compute is a stopgap only: until the admission count is fixed, its 2.5 GB sweep flushes most of a larger cache.
Type: Enhancement (performance). The search results are correct. The cost per query is the problem.
Component: Database/search (Postgres FTS and pgvector). The label is cloud because the impact is on large hosted tenants.
Priority: High. A hybrid search takes about 12.8 s alone on a large tenant and times out at 30 s under MCP fan-out. Four other large tenants show the same shape.
Complexity: Complex. The work touches the pgvector admission path, FTS query compilation, the count path, and indexes (which means an Alembic migration). It also needs a cold-cache benchmark harness.
Notes
No duplicates found (gh search issues).
The issue's priority order looks right. The admission count is the largest single cost, at about 8.2 s cold and 318k pages. It is also the cheapest to fix, either from manifest counts or from a count over search_vector_chunks alone.
Every acceptance criterion is measurable, so the harness the issue asks for (300k+ chunks, cold cache, EXPLAIN (ANALYZE, BUFFERS) page counts) should be written first. It would let each fix be checked against the targets.
Per the repo's consistency model, the FTS and admission paths read derived state. Fixes there should stay lock-free. That means no FOR UPDATE and no wider transactions.
Coverage must stay at 100%, and Postgres-specific tests need the testcontainers run.
The issue could be split into one PR per defect, in the listed order, so each improvement can be measured on its own.
Activity
claude commentedon Oct 20, 2026
Claude finished @phernandez's task in 17s —— View job
Triage
cloudbecause the impact is on large hosted tenants.Notes
gh search issues).search_vector_chunksalone.EXPLAIN (ANALYZE, BUFFERS)page counts) should be written first. It would let each fix be checked against the targets.FOR UPDATEand no wider transactions.This was triage only. I made no code changes.
perf(core): read FTS chunk candidates from the GIN index
perf(core): count admitted vector chunks from the manifest alone (#1793)
perf(core): run FTS text conditions only on the GIN candidates (#1794)
perf(core): read FTS chunk candidates from the GIN index (#1795)
perf(core): collect multiword FTS operand matches through GIN (#1802)