pg-smart-search
Core Concepts

Architecture & Parallel Fast-Track

Deep dive into how pg-smart-search executes queries concurrently and cancels zombie queries. Actual latency depends on your hardware and dataset — see Benchmarks.

Architecture & Parallel Fast-Track

Here is an interactive simulator showing how pg-smart-search processes queries in parallel and intercepts slow search branches via AbortController-driven cancellation:

⌘ ISO 9 TRANSLITERATION
DISPATCH t = --ms
FTS GIN + ts_rank --
WINNER
KILLED
0 RESULTS
TRIGRAM GiST + KNN (<->) --
WINNER
KILLED
0 RESULTS
ILIKE seq scan --
WINNER
KILLED
0 RESULTS
CACHE HIT
0.5ms — MemoryCache
ABORT SIGNAL FIRED — 0 terminated
FTS empty — still waiting...
RESULTS
ENGINE LOG
// waiting...
cache: IDLE signals: 0/0 mode: PARALLEL
total: --ms

The core philosophy of pg-smart-search is simple: don't wait for slow strategies if a fast one succeeds.

The initial FTS/standard race is parallel (hedged), not a waterfall. Everything past that first race is a waterfall, though: fuzzy, then layout correction, then transliteration, each only tried if the previous step came back empty. See Smart Hybrid Fallback for that chain.

How it Works

  1. Concurrent Execution: When a search query is received on the STANDARD tier, the engine triggers FTS and the standard (ILIKE + trigram pre-filter) search. If ftsColumn (Turbo Mode) is configured, it hedges — starting the standard branch only after a short delay (25ms) or as soon as FTS comes back empty, whichever is first, since FTS is expected to reliably win when it's index-backed. Without ftsColumn, both start immediately.
  2. First Past the Post: As soon as either branch returns non-empty results, the engine prepares the response and cancels the other.
  3. Zombie Query Prevention: The engine fires its internal AbortController, which propagates down to the losing query's AbortSignal. The adapter maps that to cancelling the underlying PostgreSQL backend (via a dedicated cancelPool connection calling pg_cancel_backend($pid)), freeing the connection and CPU cycles instead of letting the query run to completion for a result nobody reads.

Without Zombie Query Prevention, slow trigram searches would continue consuming DB resources even after the client received the response. AbortSignal ensures clean termination. To prevent database connection starvation (Connection Starvation) under heavy concurrent lookups, the engine manages cancellation signals through an isolated pool called cancelPool (size 2-3) reserved exclusively for abort interrupts.

Ranking

FTS relevance uses PostgreSQL's ts_rank (not ts_rank_cd, and not BM25 — ts_rank is a simpler term-frequency/document-length weighting, cheaper to compute than ts_rank_cd's positional-proximity variant since it doesn't need lexeme position data). We haven't published a controlled before/after benchmark for that choice on this codebase — treat any specific percentage improvement as unverified until someone runs one.

Trigram tiers (STANDARD's pre-filter, ADVANCED) rank using word_similarity() instead.

Index Usage Is Tier-Dependent

  • STANDARD uses the <% trigram operator as an index-accelerated pre-filter (benefiting from a GiST or GIN gist_trgm_ops/gin_trgm_ops index on the search columns), then applies ILIKE/threshold checks on the surviving rows.
  • ADVANCED also pre-filters via <%, but ranks with a word_similarity() expression computed per row and sorted with ORDER BY ... DESC — Postgres has no index type that can serve that ordering directly (GIN/GiST/RUM included), so every candidate that survives the WHERE filter gets its relevance computed and sorted in memory. This tier trades a wider index-accelerated pre-filter for reduced row volume, not for an index-served sort — it is not $O(\log N)$ end-to-end, only the pre-filter step is.
  • VECTOR is the tier that gets genuine index-order KNN sorting: ORDER BY embedding <=> $1::vector against an HNSW index natively supports nearest-neighbor ordering from the index itself.