Contents
pg_fts — roadmap (planned enhancements, not yet implemented)
Enhancements that are designed or prototyped but not yet shipped, tracked so they are not rediscovered. Ordered roughly by value.
Performance
Verify parallel merge at scale. Parallel merge (
bm25_merge_all_parallel) is implemented and verified correct locally (parallel build → many segments → parallelfts_merge→ one segment, byte-identical counts). It has not yet been timed on a very large (multi-million-document) corpus. When enough parallel worker slots are available (max_worker_processesset high enough thatLaunchParallelWorkerssucceeds), the code takes the parallel path and otherwise falls back to a correct serial merge. TODO: capture the parallel-merge speedup vs the serial path at scale.Level-2 recursive parallel merge (W → W/2 → … → 1). The current parallel merge does one parallel pass into (workers+1) segments, then a serial final combine to one. For very large indexes that final combine is still O(index) single-threaded. Recursing the parallel merge so the final combine also parallelizes would remove it. Deferred — one parallel pass already removes the dominant per-segment decode cost.
Parallel build: fewer, larger per-worker segments. Each worker currently flushes several segments (budget-triggered), so a parallel build leaves many segments needing a merge. Giving each worker a larger flush budget (its share of
maintenance_work_mem) would leave ~1 segment per worker, shrinking the post-build merge input. Complements #1/#2.Ranked common-term latency (the headline gap). Ranked top-k over a very common term (docid-ordered block-max WAND) is the largest gap vs VectorChord/pg_textsearch (
bench/RESULTS_VS_VCHORD_PGTEXTSEARCH.md): block-max WAND cannot skip blocks when a term appears in most documents. Profiling shows such a query is roughly one-third decode + block-load and two-thirds scoring/heap/executor, so an incremental codec tweak is capped and cannot enable additional skipping. Two attempts were prototyped and reverted with evidence (bench/NOTE_IMPACT_ORDERING.md,bench/NOTE_PARALLEL_RANKED.md). The real lever is a format-v3 codec: a compact columnar posting layout (rank/select-friendly docid set + quantized-impact sidecar, positions optional) with a hard top-k early-termination like VectorChord’s block-WeakAND — a substantial rewrite, and the only thing shown capable of closing the gap. A cheaper adjacent win: an optional no-positions index mode (WITH (positions=off)) for phrase-free workloads — smaller index, faster build and scan — keeping phrase/NEAR support opt-in.COUNT / aggregation Custom Scan pushdown. A transparent
count(*) WHERE col @@@ querycurrently runs as a bitmap heap scan, which goes lossy on a huge match set and rechecks the heap.fts_count()already avoids this with a visibility-map-based bulk count, but it is an explicit function call. Aset_rel_pathlist_hook/create_upper_paths_hookCustom Scan that pushes COUNT into the index would make plaincount(*)fast without the explicit call.Parallel scan (
amcanparallel). Query execution is single-threaded. A parallel bitmap / ordering scan would help large scans, and underpins the flat common-term latency described in #4. Warm-cache selective queries benefit little, so this targets large or common-term workloads.Storage AIO /
read_streamprefetch for the cold merge full-scan. The build heap scan already gets coreread_streamprefetch for free. The remaining candidate is the cold merge full-scan of posting pages, ifBM25SegMetarecorded a contiguous posting block range so ablk++read_stream callback could prefetch. Low priority — pointer chains and WAND block-skipping defeat prefetch elsewhere. Deferred until a cold-merge I/O bottleneck is measured.
Sparsemap (vendored)
- Exercise batch/cached sparsemap APIs under a delete-heavy workload.
The batched tombstone filter (
sm_contains_many) and MRU-cached membership test (sm_contains_cached) are integrated into the WAND cursor and merge paths. They only help the tombstone/merge paths, so a delete-free read benchmark shows no effect. TODO: quantify the gain on a delete/update-churn workload where they should help.
Benchmark / competitive
Multi-engine real-corpus comparison — done; iterate. A clean 3-way comparison (build time, index size, per-query latency across selectivity bands) vs VectorChord-bm25 and Timescale pg_textsearch on 2.19M Wikipedia articles is in
bench/RESULTS_VS_VCHORD_PGTEXTSEARCH.md. It shows pg_fts trailing on ranked latency and index size (the codec gap, #4) while leading on query-language breadth and index-native COUNT. The follow-up is the format-v3 codec work (#4), not more benchmarking.fts_searchSRF under-fetch safety. The top-k over-fetch is tight (k*2). This is safe for the ordering scan (which retries), but thefts_search()SRF does not retry — under a heavy-delete workload where more than half the top rows are invisible it could return fewer thank. A small internal retry inbm25_topk_visible(grow the requested count and re-scan whennvis < k) would make tight over-fetch fully safe everywhere.
Correctness / robustness (lower urgency)
- Sparsemap error-path leaks.
sm_create/ blob buffers are palloc/libc allocations; on anereportbetween create and free they leak for the duration of the statement (reclaimed at transaction/backend end). APG_TRY/PG_FINALLYaround the few error-prone spots would tidy this. Low severity — rare error paths only.