# Comparing ParadeDB and Postgres Full-Text Search

> An honest comparison of ParadeDB and Postgres full-text search: how each one ranks, executes Top K, filters, and counts, and where stock FTS stops scaling.

Source: https://www.paradedb.com/compare/postgresql

---

ParadeDB is Postgres, so this comparison is really about the search machinery. Postgres ships [`tsvector` and GIN](https://www.postgresql.org/docs/current/textsearch.html), 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](https://www.paradedb.com/docs/concepts/architecture) 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.

Dataset: ParadeDB (pg_search 0.25.6, Postgres 18) versus Postgres 18, tsvector stored generated columns, GIN + btree. Example query terms: one term "rust", ten terms "rust arc clone memory safety borrow checker ownership lifetime rules". The full per-percentile data is downloadable from [the compare page](https://www.paradedb.com/compare/postgresql) and the harness is open source at [paradedb/benchmarker](https://github.com/paradedb/benchmarker).

One row per workload and concurrency. Latency is p50 and p99 in milliseconds; throughput is completed queries per second. A query that took longer than 30 seconds was cancelled and counts as timed out.

| Workload | Field | Detail | Clients | ParadeDB p50 | Postgres FTS p50 | ParadeDB p99 | Postgres FTS p99 | ParadeDB QPS | Postgres FTS QPS |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Top K | title | 1 term, top 10 | 1 | 2.5 ms | 160 ms | 3.1 ms | 1.1 s | 412 | 5 |
| Top K | title | 1 term, top 10 | 4 | 3.1 ms | 112 ms | 3.9 ms | 627 ms | 1354 | 24 |
| Top K | title | 1 term, top 10 | 8 | 4.9 ms | 148 ms | 7.6 ms | 978 ms | 1656 | 36 |
| Top K | text | 1 term, top 10 | 1 | 2.8 ms | 712 ms | 3.6 ms | 2.9 s | 348 | 1 |
| Top K | text | 1 term, top 10 | 4 | 3.3 ms | 3.3 s | 4.6 ms | 12.6 s | 1180 | 1 |
| Top K | text | 1 term, top 10 | 8 | 5.8 ms | 6.8 s | 9.2 ms | 20.2 s | 1401 | 1 |
| Top K | text | 10 terms, top 10 | 1 | 30.9 ms | timed out (>30 s) | 79.3 ms | timed out (>30 s) | 28 | 0 |
| Top K | text | 10 terms, top 10 | 4 | 32.1 ms | timed out (>30 s) | 79.0 ms | timed out (>30 s) | 113 | 0 |
| Top K | text | 10 terms, top 10 | 8 | 64.3 ms | timed out (>30 s) | 174 ms | timed out (>30 s) | 113 | 0 |
| Filtered | text | score > 10 | 1 | 18.0 ms | 161 ms | 33.5 ms | 351 ms | 58 | 6 |
| Filtered | text | score > 10 | 4 | 17.4 ms | 173 ms | 33.7 ms | 377 ms | 235 | 22 |
| Filtered | text | score > 10 | 8 | 30.3 ms | 354 ms | 79.3 ms | 805 ms | 239 | 22 |
| Filtered | text | type = 'story' | 1 | 9.0 ms | 686 ms | 27.7 ms | 923 ms | 98 | 2 |
| Filtered | text | type = 'story' | 4 | 9.3 ms | 735 ms | 28.7 ms | 1.1 s | 377 | 6 |
| Filtered | text | type = 'story' | 8 | 18.4 ms | 1.6 s | 57.1 ms | 2.4 s | 394 | 5 |
| Count | title |  | 1 | 17.2 ms | 67.9 ms | 37.7 ms | 529 ms | 56 | 9 |
| Count | title |  | 4 | 19.5 ms | 84.6 ms | 41.5 ms | 563 ms | 204 | 32 |
| Count | title |  | 8 | 32.3 ms | 99.8 ms | 84.4 ms | 649 ms | 218 | 56 |
| Count | text |  | 1 | 37.6 ms | 378 ms | 75.2 ms | 2.1 s | 27 | 2 |
| Count | text |  | 4 | 38.8 ms | 1.2 s | 75.7 ms | 4.9 s | 103 | 3 |
| Count | text |  | 8 | 72.1 ms | 1.6 s | 161 ms | 5.9 s | 106 | 4 |
| Faceting | text | terms | 1 | 44.6 ms | 884 ms | 93.4 ms | 3.2 s | 22 | 1 |
| Faceting | text | terms | 4 | 44.9 ms | 2.1 s | 93.4 ms | 8.3 s | 89 | 2 |
| Faceting | text | terms | 8 | 85.2 ms | 4.2 s | 204 ms | 27.9 s | 89 | 2 |
| Faceting | text | histogram | 1 | 51.3 ms | 975 ms | 106 ms | 3.2 s | 19 | 1 |
| Faceting | text | histogram | 4 | 56.3 ms | 1.5 s | 110 ms | 13.7 s | 73 | 2 |
| Faceting | text | histogram | 8 | 93.5 ms | 4.0 s | 235 ms | 16.0 s | 80 | 2 |
| Highlighting | title |  | 1 | 2.8 ms | 92.0 ms | 3.5 ms | 500 ms | 367 | 7 |
| Highlighting | title |  | 4 | 3.2 ms | 106 ms | 4.1 ms | 636 ms | 1261 | 26 |
| Highlighting | title |  | 8 | 5.4 ms | 171 ms | 8.3 ms | 964 ms | 1523 | 34 |
| Highlighting | text |  | 1 | 3.2 ms | 729 ms | 3.9 ms | 2.9 s | 307 | 1 |
| Highlighting | text |  | 4 | 3.8 ms | 2.2 s | 4.9 ms | 8.1 s | 1040 | 2 |
| Highlighting | text |  | 8 | 6.8 ms | 5.0 s | 10.2 ms | 16.8 s | 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.

| Feature                                                        | ParadeDB                                                                                                                                                                                                                                                                                                                        | Postgres FTS                                                                                                                                                                             |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Index structure                                                | One index: a segmented inverted index ([Tantivy](/learn/tantivy/introduction)), columnar storage, and a SPANN-style IVF vector index                                                                                                                                                                                            | [GIN posting trees](https://www.postgresql.org/docs/current/gin.html) over `tsvector`, plus a pending list; vectors need a separate [pgvector](/learn/postgresql/what-is-pgvector) index |
| Relevance scoring                                              | [BM25](/learn/search-concepts/bm25): corpus-wide IDF, term saturation, length normalization                                                                                                                                                                                                                                     | [`ts_rank` / `ts_rank_cd`](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING): local frequency only, no corpus statistics                              |
| Top K execution                                                | Score-ordered iterator with [block-max WAND](/learn/search-concepts/block-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](/learn/search-concepts/vector-search) search (beta) | Dense and sparse vectors indexed and scored alongside BM25, on the same live rows                                                                                                                                                                                                                                               | FTS is lexical only; [pgvector](/learn/postgresql/what-is-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](/learn/search-concepts/hybrid-search) ranking         | Vector and BM25 scored in a single index pass, fused with [RRF](/learn/search-concepts/reciprocal-rank-fusion) (native RRF coming soon)                                                                                                                                                                                         | [pgvector](/learn/postgresql/what-is-pgvector) and GIN as two separate index scans, then [RRF](/learn/search-concepts/reciprocal-rank-fusion)-fused                                      |
| `count(*)` over a match                                        | [Answered from the index](https://www.paradedb.com/docs/reference/aggregates/overview)                                                                                                                                                                                                                                          | No index-only scans on GIN: every counted row touches the heap                                                                                                                           |
| Tokenization                                                   | [Per-field tokenizers](https://www.paradedb.com/docs/reference/tokenizers/overview): ICU, ngram, CJK, stemmers, custom                                                                                                                                                                                                          | One parser; [dictionaries](https://www.postgresql.org/docs/current/textsearch-dictionaries.html) for stemming, stopwords, synonyms                                                       |
| Query surface                                                  | [Match, phrase, fuzzy, boost, regex, and range operators](https://www.paradedb.com/docs/concepts/full-text/overview) in SQL                                                                                                                                                                                                     | [`tsquery`](https://www.postgresql.org/docs/current/datatype-textsearch.html#DATATYPE-TSQUERY): `& \| ! <->` plus four weight classes (A–D)                                              |
| Highlighting                                                   | `pdb.snippet()` from indexed positions                                                                                                                                                                                                                                                                                          | [`ts_headline()`](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-HEADLINE) re-parses each document at query time                                            |
| Document limits                                                | No practical caps                                                                                                                                                                                                                                                                                                               | [`tsvector`: 1MB per row, positions clamped at 16,383](https://www.postgresql.org/docs/current/textsearch-limitations.html)                                                              |
| Write path                                                     | MVCC; segments merge in the background ([LSM-style](/blog/lsm-trees-in-postgres)), with an optional in-memory mutable segment to absorb high write rates                                                                                                                                                                        | MVCC; GIN [`fastupdate` pending list](https://www.postgresql.org/docs/current/gin.html#GIN-FAST-UPDATE), flushed on the unlucky write                                                    |
| Where it runs                                                  | [ParadeDB Cloud](https://app.paradedb.com) (private preview), BYOC, or [self-hosted extension](https://www.paradedb.com/docs/start/install). Managed services do not allow the extension; run ParadeDB beside them with [logical replication](https://www.paradedb.com/docs/operate/deploy/logical-replication/getting-started) | Everywhere: bundled in RDS, Cloud SQL, Azure, Supabase, Neon                                                                                                                             |

## Next steps

- [Install ParadeDB](https://www.paradedb.com/docs/start/install) and [create your first index](https://www.paradedb.com/docs/start/create-your-first-index).
- [Same Query, Three Results: Benchmarking ParadeDB and Postgres FTS](https://www.paradedb.com/blog/benchmarker-iteration) explains how the benchmark was built.
- The benchmark harness is open source at [paradedb/benchmarker](https://github.com/paradedb/benchmarker).
