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

  1. Verify parallel merge at scale. Parallel merge (bm25_merge_all_parallel) is implemented and verified correct locally (parallel build → many segments → parallel fts_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_processes set high enough that LaunchParallelWorkers succeeds), 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.

  2. 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.

  3. 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.

  4. 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.

  5. COUNT / aggregation Custom Scan pushdown. A transparent count(*) WHERE col @@@ query currently 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. A set_rel_pathlist_hook / create_upper_paths_hook Custom Scan that pushes COUNT into the index would make plain count(*) fast without the explicit call.

  6. 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.

  7. Storage AIO / read_stream prefetch for the cold merge full-scan. The build heap scan already gets core read_stream prefetch for free. The remaining candidate is the cold merge full-scan of posting pages, if BM25SegMeta recorded a contiguous posting block range so a blk++ 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)

  1. 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

  1. 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.

  2. fts_search SRF under-fetch safety. The top-k over-fetch is tight (k*2). This is safe for the ordering scan (which retries), but the fts_search() SRF does not retry — under a heavy-delete workload where more than half the top rows are invisible it could return fewer than k. A small internal retry in bm25_topk_visible (grow the requested count and re-scan when nvis < k) would make tight over-fetch fully safe everywhere.

Correctness / robustness (lower urgency)

  1. Sparsemap error-path leaks. sm_create / blob buffers are palloc/libc allocations; on an ereport between create and free they leak for the duration of the statement (reclaimed at transaction/backend end). A PG_TRY/PG_FINALLY around the few error-prone spots would tidy this. Low severity — rare error paths only.