ParadeDB vs Postgres FTS
ParadeDB is Postgres, so this comparison is really about the search machinery. Postgres ships tsvector and GIN, and that is exactly what every managed Postgres (RDS, Cloud SQL, Azure, Supabase, Neon) bundles as search. ParadeDB replaces that machinery with a dedicated search engine living in the same database.
Latency and throughput
ParadeDB (pg_search 0.25.6 on Postgres 18) ran against stock Postgres 18 full-text search in its best case: stored generated tsvector columns, a GIN index on each, and btree indexes for the filters.
Both engines ran on identical hardware, four pinned CPUs and 8 GB of memory each, over the full 28.7-million-row Hacker News dataset, queried through pgbouncer in transaction pooling mode.
Every workload below ran for 30 seconds against a rotating pool of 40 query terms, after an identical 30-second warmup, and any query that took longer than 30 seconds was cancelled.
Dense and sparse vector search, plus hybrid BM25 and vector ranking, all served from the same index. We're adding these workloads to the benchmark next.
Full results table · all workloads at every concurrency
| Workload | Field | Detail | Clients | Metric | ParadeDB | Postgres FTS |
|---|---|---|---|---|---|---|
| Top K | title | 1 term · top 10 | 1 | p50 latency | 2.5 ms | 160 ms |
| Top K | title | 1 term · top 10 | 1 | p99 latency | 3.1 ms | 1.1 s |
| Top K | title | 1 term · top 10 | 1 | throughput (QPS) | 412 | 5 |
| Top K | title | 1 term · top 10 | 4 | p50 latency | 3.1 ms | 112 ms |
| Top K | title | 1 term · top 10 | 4 | p99 latency | 3.9 ms | 627 ms |
| Top K | title | 1 term · top 10 | 4 | throughput (QPS) | 1354 | 24 |
| Top K | title | 1 term · top 10 | 8 | p50 latency | 4.9 ms | 148 ms |
| Top K | title | 1 term · top 10 | 8 | p99 latency | 7.6 ms | 978 ms |
| Top K | title | 1 term · top 10 | 8 | throughput (QPS) | 1656 | 36 |
| Top K | text | 1 term · top 10 | 1 | p50 latency | 2.8 ms | 712 ms |
| Top K | text | 1 term · top 10 | 1 | p99 latency | 3.6 ms | 2.9 s |
| Top K | text | 1 term · top 10 | 1 | throughput (QPS) | 348 | 1 |
| Top K | text | 1 term · top 10 | 4 | p50 latency | 3.3 ms | 3.3 s |
| Top K | text | 1 term · top 10 | 4 | p99 latency | 4.6 ms | 12.6 s |
| Top K | text | 1 term · top 10 | 4 | throughput (QPS) | 1180 | 1 |
| Top K | text | 1 term · top 10 | 8 | p50 latency | 5.8 ms | 6.8 s |
| Top K | text | 1 term · top 10 | 8 | p99 latency | 9.2 ms | 20.2 s |
| Top K | text | 1 term · top 10 | 8 | throughput (QPS) | 1401 | 1 |
| Top K | text | 10 terms · top 10 | 1 | p50 latency | 30.9 ms | >30 s |
| Top K | text | 10 terms · top 10 | 1 | p99 latency | 79.3 ms | >30 s |
| Top K | text | 10 terms · top 10 | 1 | throughput (QPS) | 28 | 0 |
| Top K | text | 10 terms · top 10 | 4 | p50 latency | 32.1 ms | >30 s |
| Top K | text | 10 terms · top 10 | 4 | p99 latency | 79.0 ms | >30 s |
| Top K | text | 10 terms · top 10 | 4 | throughput (QPS) | 113 | 0 |
| Top K | text | 10 terms · top 10 | 8 | p50 latency | 64.3 ms | >30 s |
| Top K | text | 10 terms · top 10 | 8 | p99 latency | 174 ms | >30 s |
| Top K | text | 10 terms · top 10 | 8 | throughput (QPS) | 113 | 0 |
| Filtered | text | score > 10 | 1 | p50 latency | 18.0 ms | 161 ms |
| Filtered | text | score > 10 | 1 | p99 latency | 33.5 ms | 351 ms |
| Filtered | text | score > 10 | 1 | throughput (QPS) | 58 | 6 |
| Filtered | text | score > 10 | 4 | p50 latency | 17.4 ms | 173 ms |
| Filtered | text | score > 10 | 4 | p99 latency | 33.7 ms | 377 ms |
| Filtered | text | score > 10 | 4 | throughput (QPS) | 235 | 22 |
| Filtered | text | score > 10 | 8 | p50 latency | 30.3 ms | 354 ms |
| Filtered | text | score > 10 | 8 | p99 latency | 79.3 ms | 805 ms |
| Filtered | text | score > 10 | 8 | throughput (QPS) | 239 | 22 |
| Filtered | text | type = 'story' | 1 | p50 latency | 9.0 ms | 686 ms |
| Filtered | text | type = 'story' | 1 | p99 latency | 27.7 ms | 923 ms |
| Filtered | text | type = 'story' | 1 | throughput (QPS) | 98 | 2 |
| Filtered | text | type = 'story' | 4 | p50 latency | 9.3 ms | 735 ms |
| Filtered | text | type = 'story' | 4 | p99 latency | 28.7 ms | 1.1 s |
| Filtered | text | type = 'story' | 4 | throughput (QPS) | 377 | 6 |
| Filtered | text | type = 'story' | 8 | p50 latency | 18.4 ms | 1.6 s |
| Filtered | text | type = 'story' | 8 | p99 latency | 57.1 ms | 2.4 s |
| Filtered | text | type = 'story' | 8 | throughput (QPS) | 394 | 5 |
| Count | title | — | 1 | p50 latency | 17.2 ms | 67.9 ms |
| Count | title | — | 1 | p99 latency | 37.7 ms | 529 ms |
| Count | title | — | 1 | throughput (QPS) | 56 | 9 |
| Count | title | — | 4 | p50 latency | 19.5 ms | 84.6 ms |
| Count | title | — | 4 | p99 latency | 41.5 ms | 563 ms |
| Count | title | — | 4 | throughput (QPS) | 204 | 32 |
| Count | title | — | 8 | p50 latency | 32.3 ms | 99.8 ms |
| Count | title | — | 8 | p99 latency | 84.4 ms | 649 ms |
| Count | title | — | 8 | throughput (QPS) | 218 | 56 |
| Count | text | — | 1 | p50 latency | 37.6 ms | 378 ms |
| Count | text | — | 1 | p99 latency | 75.2 ms | 2.1 s |
| Count | text | — | 1 | throughput (QPS) | 27 | 2 |
| Count | text | — | 4 | p50 latency | 38.8 ms | 1.2 s |
| Count | text | — | 4 | p99 latency | 75.7 ms | 4.9 s |
| Count | text | — | 4 | throughput (QPS) | 103 | 3 |
| Count | text | — | 8 | p50 latency | 72.1 ms | 1.6 s |
| Count | text | — | 8 | p99 latency | 161 ms | 5.9 s |
| Count | text | — | 8 | throughput (QPS) | 106 | 4 |
| Faceting | text | terms | 1 | p50 latency | 44.6 ms | 884 ms |
| Faceting | text | terms | 1 | p99 latency | 93.4 ms | 3.2 s |
| Faceting | text | terms | 1 | throughput (QPS) | 22 | 1 |
| Faceting | text | terms | 4 | p50 latency | 44.9 ms | 2.1 s |
| Faceting | text | terms | 4 | p99 latency | 93.4 ms | 8.3 s |
| Faceting | text | terms | 4 | throughput (QPS) | 89 | 2 |
| Faceting | text | terms | 8 | p50 latency | 85.2 ms | 4.2 s |
| Faceting | text | terms | 8 | p99 latency | 204 ms | 27.9 s |
| Faceting | text | terms | 8 | throughput (QPS) | 89 | 2 |
| Faceting | text | histogram | 1 | p50 latency | 51.3 ms | 975 ms |
| Faceting | text | histogram | 1 | p99 latency | 106 ms | 3.2 s |
| Faceting | text | histogram | 1 | throughput (QPS) | 19 | 1 |
| Faceting | text | histogram | 4 | p50 latency | 56.3 ms | 1.5 s |
| Faceting | text | histogram | 4 | p99 latency | 110 ms | 13.7 s |
| Faceting | text | histogram | 4 | throughput (QPS) | 73 | 2 |
| Faceting | text | histogram | 8 | p50 latency | 93.5 ms | 4.0 s |
| Faceting | text | histogram | 8 | p99 latency | 235 ms | 16.0 s |
| Faceting | text | histogram | 8 | throughput (QPS) | 80 | 2 |
| Highlighting | title | — | 1 | p50 latency | 2.8 ms | 92.0 ms |
| Highlighting | title | — | 1 | p99 latency | 3.5 ms | 500 ms |
| Highlighting | title | — | 1 | throughput (QPS) | 367 | 7 |
| Highlighting | title | — | 4 | p50 latency | 3.2 ms | 106 ms |
| Highlighting | title | — | 4 | p99 latency | 4.1 ms | 636 ms |
| Highlighting | title | — | 4 | throughput (QPS) | 1261 | 26 |
| Highlighting | title | — | 8 | p50 latency | 5.4 ms | 171 ms |
| Highlighting | title | — | 8 | p99 latency | 8.3 ms | 964 ms |
| Highlighting | title | — | 8 | throughput (QPS) | 1523 | 34 |
| Highlighting | text | — | 1 | p50 latency | 3.2 ms | 729 ms |
| Highlighting | text | — | 1 | p99 latency | 3.9 ms | 2.9 s |
| Highlighting | text | — | 1 | throughput (QPS) | 307 | 1 |
| Highlighting | text | — | 4 | p50 latency | 3.8 ms | 2.2 s |
| Highlighting | text | — | 4 | p99 latency | 4.9 ms | 8.1 s |
| Highlighting | text | — | 4 | throughput (QPS) | 1040 | 2 |
| Highlighting | text | — | 8 | p50 latency | 6.8 ms | 5.0 s |
| Highlighting | text | — | 8 | p99 latency | 10.2 ms | 16.8 s |
| Highlighting | text | — | 8 | throughput (QPS) | 1220 | 1 |
How they differ
Stock FTS is real search: parsing, stemming, indexes. The gap opens at ranking quality, and at how each side executes Top K, filters, and counts once the corpus stops being small.
| ParadeDB | Postgres FTS | |
|---|---|---|
| Index structure | One index: a segmented inverted index (Tantivy), columnar storage, and a SPANN-style IVF vector index | GIN posting trees over tsvector, plus a pending list; vectors need a separate pgvector index |
| Relevance scoring | BM25: corpus-wide IDF, term saturation, length normalization | ts_rank / ts_rank_cd: local frequency only, no corpus statistics |
| Top K execution | Score-ordered iterator with block-max WAND: skips matches that can't reach the top k, so work stays sublinear in match count | No score order, so bitmap every match, ts_rank each from the heap, sort, then LIMIT: cost tracks match count |
| Semantic search | Dense and sparse vectors indexed and scored alongside BM25, on the same live rows | FTS is lexical only; pgvector is a separate extension and index to add, sync, and tune |
| Filtered search | Numbers, dates, and literals are stored columnar and queried in the same index scan | GIN and btree via BitmapAnd, then rank whatever survives |
| Hybrid ranking | Vector and BM25 scored in a single index pass, fused with RRF (native RRF coming soon) | pgvector and GIN as two separate index scans, then RRF-fused |
count(*) over a match | Answered from the index | No index-only scans on GIN: every counted row touches the heap |
| Tokenization | Per-field tokenizers: ICU, ngram, CJK, stemmers, custom | One parser; dictionaries for stemming, stopwords, synonyms |
| Query surface | Match, phrase, fuzzy, boost, regex, and range operators in SQL | tsquery: & | ! <-> plus four weight classes (A–D) |
| Highlighting | pdb.snippet() from indexed positions | ts_headline() re-parses each document at query time |
| Document limits | No practical caps | tsvector: 1MB per row, positions clamped at 16,383 |
| Write path | MVCC; segments merge in the background (LSM-style), with an optional in-memory mutable segment to absorb high write rates | MVCC; GIN fastupdate pending list, flushed on the unlucky write |
| Where it runs | ParadeDB Cloud, BYOC, or self-hosted extension | Everywhere: bundled in RDS, Cloud SQL, Azure, Supabase, Neon |