We ran pg_search 0.25.11 and pg_textsearch 1.4.0 on PostgreSQL 18 against the 300 BEIR SciFact test queries, and relevance is a tie: 0.6839 and 0.6881 nDCG@10, against 0.3324 for native tsvector with ts_rank_cd. The differences that decide the choice sit elsewhere. pg_search's stemmed index answered top-10 at a 0.64 ms p50 and top-100 at 0.80 ms, while pg_textsearch went from 1.28 ms to 10.4 ms as k grew, which is the over-fetch every hybrid query does. pg_search has phrase, fuzzy and prefix queries today; pg_textsearch 1.4.0 silently scores a phrase as a bag of words. pg_textsearch is WAL-replicated to physical standbys and ships under the PostgreSQL license, while the pg_search Community index lives only on the primary and ships under AGPL-3.0, with HA and read replicas reserved for ParadeDB Enterprise. Both leaked RLS-hidden rows into BM25 scores in our probe: a tenant's score for a guessed term moved by a factor of 37 to 39 when another tenant's rows arrived. The two cannot share a database, because both register an access method named bm25. Start by listing your replica topology, your cloud's extension allow-list and your AGPL policy, because those three answers pick the extension before any feature does.
pg_search vs pg_textsearch is the choice you face once you decide keyword retrieval should live in the same Postgres as your pgvector embeddings instead of in a separate Elasticsearch or OpenSearch cluster. We ran both on PostgreSQL 18 against the 300 BEIR SciFact test queries, and on relevance they tie: pg_textsearch 1.4.0 scored 0.6881 nDCG@10 and pg_search 0.25.11 with English stemming scored 0.6839. Native tsvector with ts_rank_cd scored 0.3324 on the same queries.
So relevance will not decide this. Four other things will: whether your HA design depends on physical streaming replicas, which extensions your managed cloud allows, whether AGPL-3.0 clears your license review, and whether your users need exact phrases and typo tolerance this quarter. Each of those has a clear answer, and for a regulated team running Postgres with failover replicas, the replica question alone settles it before features come up.
What follows is written against the stable releases on 30 September 2026: pg_search v0.25.11 (published 29 September; v0.26.0-rc.2 is a prerelease and we ignore it) and pg_textsearch v1.4.0 (18 August). Every SQL statement below ran in our lab as printed, except the postgresql.conf lines in the install blocks, which are the documented settings (our containers set the preload list at startup), and the '<query embedding>' placeholder in the hybrid query, where we bound a real vector.
pg_search vs pg_textsearch: the short answer
Pick by topology, cloud and license first, features second. Both extensions give you real BM25 with IDF and length normalisation, and the relevance gap between them on our run was 0.004 nDCG@10, well inside what tokenizer differences explain.
Our default for the reader this post is written for, a platform team at a regulated company running Postgres with a failover standby, is pg_textsearch, with the phrase workaround below until its next release ships Boolean and phrase index scans. Switch to pg_search the moment exact-phrase or fuzzy matching is a hard requirement, and budget for Enterprise if that search has to survive a failover.
| Your situation | Pick | Why |
|---|---|---|
| Self-managed Postgres with physical replicas and automatic failover, no budget for a commercial search license | pg_textsearch | Index pages travel through the normal WAL stream; the pg_search Community index exists only on the primary |
| Google Cloud SQL, AlloyDB or Azure HorizonDB | pg_textsearch | The only one of the two those services document; pg_search there means running a separate ParadeDB subscriber |
| Users need exact phrases, typo tolerance, prefix search or a query-string syntax today | pg_search | pg_textsearch 1.4.0 has no positions, no fuzzy operator and ignores phrase syntax without an error |
| Still on PostgreSQL 15 or 16 | pg_search | pg_textsearch 1.4.0 supports only 17 and 18 |
| Hybrid search with large candidate pools, latency-sensitive | pg_search | Top-100 p50 stayed under 1 ms in our run; pg_textsearch grew to 10.4 ms |
| AGPL-3.0 is on your deny-list and ParadeDB Enterprise is not an option | pg_textsearch | PostgreSQL license |
| Single primary is acceptable, or you will buy ParadeDB Enterprise | pg_search | Richer query language and faster top-K; Enterprise adds HA and read replicas |
| RDS or Aurora primary, keyword search required in place | Neither, today | Neither extension appeared on AWS's extension lists when we checked; use native full-text search or a logical subscriber |
pg_search vs pg_textsearch feature comparison
pg_search comes from ParadeDB and embeds a Rust search library inside Postgres; the repository has roughly 9,300 stars. pg_textsearch comes from Tiger Data (formerly Timescale) and is written as a native access method that reuses Postgres text search configurations. The table is the whole comparison in one place; the sections after it show the parts that surprised us.
The table leaves performance out on purpose. The Tiger Data release notes claim pg_textsearch outperforms ParadeDB on filtered top-K and, in an earlier release, 3.5 times the query throughput of "the leading Postgres-based BM25 extension" on a 133M-document corpus; those are the vendor's own benchmarks and we found no independent reproduction. The one head-to-head table in those release notes is a write benchmark: at 8 concurrent clients inserting MSMARCO passages, pg_textsearch v1.1.0 did 11,593 TPS and ParadeDB did 12,620 TPS, with no ParadeDB version stated. Treat both as vendor-reported.
| pg_search (ParadeDB) | pg_textsearch (Tiger Data) | ||||
|---|---|---|---|---|---|
| License | AGPL-3.0; ParadeDB Enterprise is commercial and waives copyleft | PostgreSQL license | |||
| Latest stable | v0.25.11 (2026-09-29) | v1.4.0 (2026-08-18) | |||
| PostgreSQL versions | 15, 16, 17, 18 | 17, 18 | |||
| Preload and dependencies | shared_preload_libraries; requires pgvector since 0.25.0 | shared_preload_libraries; no dependency | |||
| Index DDL | USING paradedb (id, ...) WITH (key_field='id'); bm25 kept as a deprecated alias | USING bm25(col) WITH (text_config='english') | |||
| Indexes per table | One ParadeDB index per table, covering many columns | Many; one partial index per language is a documented pattern | |||
| Query syntax | `\ | \ | \ | any term, &&& all terms, === exact term, ### phrase, @@@` query builder | <@> ordering operator; to_bm25query(text, index) |
| Score | pdb.score(id), higher is better | <@> returns negative BM25, lower is better | |||
| Phrase | Yes, with slop | No; phrase syntax is ignored | |||
| Fuzzy / prefix | Yes; fuzzy edit distance up to 2, phrase prefix | No | |||
| Boolean | Yes, through the query builder and pdb.parse | Not in 1.4.0; @@ index scans merged on main, unreleased | |||
| Tokenizers and languages | Unicode default, ICU, Chinese and Japanese segmenters, ngram, literal, regex and others; stemmers for 20 languages | Any Postgres text search configuration (29 built in); Chinese added in 1.4.0 | |||
| Filters | Pushed into the index when the column is in the index; literal tokenizer for equality | Separate B-tree pre-filter or post-filter with over-fetch | |||
| RLS and corpus statistics | Hidden rows counted (our probe); undocumented | Hidden rows counted (our probe); documented on main | |||
| Partitions | Scored per partition, no cross-partition normalisation (open issue) | Partition-local statistics, documented | |||
| Physical replication | Community: not replicated. Enterprise: HA and read replicas | WAL-replicated since 1.2/1.3 | |||
| Managed services | None in place; ParadeDB as a logical subscriber | Cloud SQL (PG17+), AlloyDB preview, Azure HorizonDB preview |
How to install pg_search and pg_textsearch
Both extensions must be preloaded, which means a restart and a change window. These are the documented installs at the stable tags:
-- postgresql.conf, then restart shared_preload_libraries = 'pg_search' -- per database; CASCADE also creates the required vector extension CREATE EXTENSION pg_search CASCADE;
-- postgresql.conf, then restart shared_preload_libraries = 'pg_textsearch' -- per database CREATE EXTENSION pg_textsearch;
Without CASCADE, pg_search 0.25.11 fails with required extension "vector" is not installed. If this database already serves RAG, pgvector is there; if not, it has to go into the air-gapped package mirror too. pg_search publishes prebuilt packages for Debian, Ubuntu and RHEL on PostgreSQL 15 to 18; pg_textsearch publishes .deb zips and tarballs for PostgreSQL 17 and 18 on amd64 and arm64, and ours installed with dpkg -i on the stock postgres:18 image with no build step.
The indexes we benchmarked, on a docs table with a bigint primary key and a content text column:
CREATE INDEX docs_stem_bm25 ON docs_stem
USING paradedb (id, (content::pdb.simple('stemmer=english', 'stopwords_language=english')))
WITH (key_field='id');CREATE INDEX docs_bm25 ON docs USING bm25(content) WITH (text_config='english');
Configure the pg_search tokenizer explicitly. Its default tokenizer lowercases but neither stems nor drops stopwords, and on SciFact that cost 0.023 nDCG@10, doubled the index to 10.1 MB and made the median query 4.6 times slower (2.92 ms against 0.64 ms), because every query walked the posting lists of words like "the". The stemmed index turns Running studies of mice into {run,studi,mice}. pg_textsearch inherits whatever Postgres configuration you name, so english gets Postgres's stemmer and stopword list with no extra decision. If you index several languages, pg_textsearch's one-partial-index-per-language pattern maps directly onto one index per language for multilingual RAG; pg_search's one-index-per-table rule pushes you toward separate columns or tables instead, and a second ParadeDB index on the same table fails with a relation may only have one ParadeDB index.
ERROR: access method "bm25" already exists CONTEXT: SQL statement "CREATE ACCESS METHOD bm25 TYPE INDEX HANDLER public.tp_handler" extension script file "pg_textsearch--1.4.0.sql", near line 32
You cannot install both in one database
This blocks any attempt to A/B the two in one database. pg_search 0.25.11 registers its current paradedb access method and, for backward compatibility, a second one called bm25. pg_textsearch registers bm25 too. Access method names are unique per database, so whichever goes second fails. We ran it in both orders: The conflict is per database, not per server. With both libraries preloaded, one PostgreSQL 18 instance ran pg_search in one database and pg_textsearch in another, and each built its index and answered queries. One trap on the way: ALTER SYSTEM SET shared_preload_libraries with the whole list inside one quoted string stopped the server from starting, because Postgres looked for a single library named after the entire string. The unquoted, comma-separated form worked.
pg_search vs pg_textsearch benchmark on BEIR SciFact
The setup: BEIR SciFact, 5,183 documents (20 MB of heap) and its 300 test queries, loaded identically into three PostgreSQL 18 containers on a 10-CPU laptop. Each query ran once at LIMIT 100 for quality, then 300 queries were timed at LIMIT 10 after an untimed warm-up pass, from psql over the Unix socket with one client and a warm cache. Neither engine got any query rewriting.
Read it with the caveats attached. This is a small corpus that fits in shared buffers, one client, no concurrent writes; the latencies show relative behaviour, not capacity, and say nothing about 50 million rows. The two BM25 engines used different tokenizers (Postgres english against pg_search's stemmer and stopword filter), and a 0.004 nDCG@10 gap is inside that noise. Call it a tie. The two reruns of the top-10 latency pass were stable within 0.1 ms at p50 for both engines.
Three findings do survive the caveats. First, both BM25 engines roughly double native ts_rank_cd, which has no IDF and was planned as a sequential scan with a top-N sort; the AND variant returned zero rows for 274 of the 300 queries. Second, pg_textsearch's latency grows with k: 1.28 ms at top-10, 10.4 ms at top-100, while pg_search's stemmed index stayed under 1 ms at both. That matters because hybrid search over-fetches 100 or more candidates per branch. Third, check the plan. pg_search's fast path shows Custom Scan (ParadeDB Base Scan) with Exec Method: TopKScanExecState, and only engages with a LIMIT, indexed ORDER BY columns (three at most) and a ParadeDB operator at the same query level; a query with no ParadeDB operator runs as plain Postgres. pg_textsearch's shows Index Scan using docs_bm25 with an Order By on the bm25query.
SELECT id, round(pdb.score(id)::numeric, 4) AS score, content ILIKE '%cancer cells%' AS has_exact_phrase FROM docs_stem WHERE content ||| 'cancer cells' ORDER BY pdb.score(id) DESC, id LIMIT 10;
SELECT id, round((content <@> 'cancer cells')::numeric, 4) AS score, content ILIKE '%cancer cells%' AS has_exact_phrase FROM docs ORDER BY content <@> 'cancer cells' LIMIT 10;
Scores, LIMIT and a row you did not ask for
The two engines point their scores in opposite directions. pg_search's pdb.score(id) is higher-is-better; pg_textsearch's <@> returns the negative BM25 score, so you sort ascending. The same query in each, from our phrase probe: Always write the LIMIT. Without it, pg_textsearch scores up to its pg_textsearch.default_limit of 1,000 rows. And <@> is an ordering operator, not a filter. When the planner skipped the BM25 index scan on a freshly created 20-row table, pg_textsearch returned the one matching row followed by non-matching rows with a score of 0 to fill the LIMIT. If your application must never show a non-match, filter zero scores in an outer query. pg_search does not do this, because ||| is a WHERE predicate. Name the index explicitly with to_bm25query('terms', 'index_name') for partial indexes and inside PL/pgSQL, where the planner hook that infers it does not run.
| Engine / config | nDCG@10 | Recall@10 | Recall@100 | p50 ms top-10 | p95 ms top-10 | p50 ms top-100 |
|---|---|---|---|---|---|---|
pg_textsearch 1.4.0, english | 0.6881 | 0.8278 | 0.9182 | 1.276 | 1.990 | 10.438 |
| pg_search 0.25.11, English stemmer + stopwords | 0.6839 | 0.8127 | 0.9216 | 0.641 | 1.134 | 0.798 |
| pg_search 0.25.11, default tokenizer | 0.6606 | 0.7843 | 0.8826 | 2.921 | 5.543 | 2.951 |
Native tsvector, OR semantics, ts_rank_cd | 0.3324 | 0.5122 | 0.7623 | 16.678 | 42.224 | 16.840 |
Native websearch_to_tsquery (AND), ts_rank_cd | 0.0724 | 0.0728 | 0.0728 | 0.103 | 0.159 | n/a |
| Index | Build time | Size |
|---|---|---|
| pg_search, default tokenizer | 69 ms | 10.1 MB |
| pg_search, English stemmer + stopwords | 213 ms | 5.1 MB |
pg_textsearch, english | 515 ms | 3.8 MB |
Native GIN on a stored tsvector | 83 ms, plus 574 ms to add the generated column | 4.6 MB, plus the stored column |
Phrase search, fuzzy matching and Boolean queries
This is where pg_search earns its place. Its ### operator enforces word order and adjacency, and on our stemmed index it agreed exactly with a literal substring search:
SELECT count(*) AS ilike_phrase FROM docs_stem WHERE content ILIKE '%blood pressure%'; -- 81 SELECT count(*) AS ilike_reversed FROM docs_stem WHERE content ILIKE '%pressure blood%'; -- 0 SELECT count(*) AS phrase_hits FROM docs_stem WHERE content ### 'blood pressure'; -- 81 SELECT count(*) AS reversed_phrase_hits FROM docs_stem WHERE content ### 'pressure blood'; -- 0 SELECT count(*) AS conjunction_hits FROM docs_stem WHERE content &&& 'blood pressure'; -- 85
Stemming carries into phrases: ### 'cancer cells' matched 187 documents on the stemmed index, because "cancer cell" counts, against 143 on the unstemmed index and 142 for the literal ILIKE. For clause numbers and defined terms in contracts or regulations, index that column without a stemmer. pg_search also has slop on phrases (::pdb.slop(n)), fuzzy matching up to edit distance 2 (::pdb.fuzzy(n)), phrase prefix, and pdb.parse for user-typed query strings with AND and OR. One trap: quotes inside a ||| string do nothing, and '"blood pressure"' returned the same top 5 as 'pressure blood'.
pg_textsearch 1.4.0 stores term frequencies but not positions. It does not reject phrase syntax; it ignores it. 'blood pressure', '"blood pressure"', 'pressure blood' and 'blood <-> pressure' returned the same five rows with the same scores, and for "cancer cells" 3 of the top 10 did not contain the phrase. ### fails outright with operator does not exist. The README's workaround, over-fetching by BM25 and filtering on the text, worked:
SELECT id, round(score::numeric, 4) AS score FROM (
SELECT id, content, content <@> 'cancer cells' AS score
FROM docs
ORDER BY score
LIMIT 100
) sub
WHERE content ILIKE '%cancer cells%'
ORDER BY score
LIMIT 10;It returns only exact-phrase documents, in BM25 order, but a rare phrase can have fewer than 10 hits in the first 100 candidates and you get a short page. That is the same over-fetch problem that filtered vector search runs into at low selectivity, and the same fix: raise the inner limit and measure how often pages come back short. Native @@ with a <-> tsquery also works on the pg_textsearch table, but at 1.4.0 it runs as a sequential scan.
What is coming: Boolean tsquery index scans with &, |, !, <-> phrase and :* prefix were merged on 17 September 2026, and Boolean filtering combined with BM25 ranking was merged on 29 September, labelled "supported, but is not yet optimized". Neither is in a release. Plan against 1.4.0 behaviour until one ships.
Filtering follows the same split. pg_search pushes a WHERE into the index scan when the filter column is in the ParadeDB index, with the literal tokenizer for text equality. pg_textsearch 1.4.0 pre-filters through a separate B-tree or post-filters after the BM25 scan, and its release notes claim filtered top-K got up to 5 times faster in 1.4.0; post-filtering can still return fewer than LIMIT rows, so over-fetch.
Hybrid search with pgvector and reciprocal rank fusion
Neither extension ships a hybrid operator you can rely on. ParadeDB's README still lists native hybrid search as coming soon, and its in-index vector support added in 0.25.0 is beta, takes only the vector type (no halfvec, which matters if you use it to get past pgvector's 2,000-dimension index limit) and may need a reindex when the storage format changes. So hybrid is still a reciprocal rank fusion query you write, with the vector branch on a normal pgvector HNSW index. The reasons to fuse at all are covered in hybrid dense and sparse retrieval; here is the SQL that ran against pg_textsearch, with embeddings in a side table doc_emb and '<query embedding>' standing in for the vector your application binds:
SET hnsw.ef_search = 100;
WITH text AS (SELECT id, ROW_NUMBER() OVER (ORDER BY score) AS rank FROM (SELECT id, content <@> $q$0-dimensional biomaterials show inductive properties.$q$ AS score FROM docs ORDER BY content <@> $q$0-dimensional biomaterials show inductive properties.$q$ LIMIT 100) t),
vector AS (SELECT id, ROW_NUMBER() OVER (ORDER BY dist) AS rank FROM (SELECT id, embedding <=> '<query embedding>'::vector AS dist FROM doc_emb ORDER BY embedding <=> '<query embedding>'::vector LIMIT 100) v),
fused AS (
SELECT id, sum(weight) AS score
FROM (
SELECT id, 1.0 / (60 + rank) AS weight FROM text
UNION ALL
SELECT id, 1.0 / (60 + rank) AS weight FROM vector
) u
GROUP BY id
)
SELECT id, score FROM fused ORDER BY score DESC, id LIMIT 10;For pg_search, replace only the first CTE; the ranking is ascending in pg_textsearch and descending in pg_search, which is the bug to look for when porting:
WITH text AS (SELECT id, ROW_NUMBER() OVER (ORDER BY pdb.score(id) DESC, id) AS rank FROM docs_stem WHERE content ||| $q$0-dimensional biomaterials show inductive properties.$q$ ORDER BY pdb.score(id) DESC, id LIMIT 100),
Set hnsw.ef_search to at least the branch limit; at the default of 40 the vector branch cannot return 100 candidates. Tuning HNSW for millions of rows covers the rest of that branch.
Both plans used both indexes. The latency gap is the top-100 text branch: pg_textsearch's BM25 index scan took about 12 ms of a 14.9 ms execution. Do not read the quality column as "hybrid loses". Equal-weight RRF scored below BM25 alone here because the dense branch was weak: a 137M-parameter open embedding model at 0.49 nDCG@10, with 354 of the 5,183 documents embedded from truncated text after the local embedding server failed on them. The result proves the SQL works in both engines. Tune the fusion weights against your own labelled queries, and put a reranker after fusion only when your evals show it pays.
| Run | nDCG@10 | Recall@100 | p50 ms | p95 ms |
|---|---|---|---|---|
| Dense only, pgvector HNSW | 0.4924 | 0.7782 | 0.610 | 0.749 |
| RRF, pg_search text branch | 0.6198 | 0.9213 | 1.736 | 2.273 |
| RRF, pg_textsearch text branch | 0.6251 | 0.9197 | 11.381 | 14.118 |
Row-level security, partitions and replication
CREATE POLICY tenant_a_only ON rls_docs FOR SELECT TO tenant_a_reader USING (tenant = 'a');
SELECT id, content <@> to_bm25query('sanctions', 'rls_docs_bm25') AS score FROM rls_docs ORDER BY content <@> to_bm25query('sanctions', 'rls_docs_bm25') LIMIT 3;SELECT id, pdb.score(id) AS score FROM rls_docs WHERE content ||| 'sanctions' ORDER BY pdb.score(id) DESC LIMIT 3;
Neither engine returned a hidden row; pg_search applies the policy as a heap filter inside its custom scan, and pg_textsearch's plan shows Rows Removed by Filter: 200. But the score moved by a factor of about 37 in pg_textsearch and 39 in pg_search, because IDF and average length are computed over every indexed row. A tenant-A user who guesses a term (a counterparty name, a case code) can tell whether other tenants' documents contain it. pg_textsearch documents this on its main branch and adds a superuser setting, pg_textsearch.allow_rls, to refuse BM25 indexes on RLS tables; in 1.4.0 SHOW pg_textsearch.allow_rls fails as an unrecognized parameter. We found no mention of RLS in the ParadeDB docs we read.
The fix is structural: never let two clearance levels share an index's statistics. With pg_textsearch, a partial index per tenant or a partition per tenant keeps statistics separate, since both engines compute them per partition. With pg_search, one index per table rules out partial indexes per tenant, so partition or separate tables. That is the silo end of the silo, pool and bridge tenancy models, and it pairs with keeping document ACLs in sync with the source system, because a stale permission is the other way the wrong rows reach a scorer.
The partition side effect: scores from different partitions are not comparable in either engine. pg_textsearch documents partition-local statistics, and ParadeDB has an open issue for cross-partition normalisation. Do not merge top-K across partitions by raw score.
Row-level security does not hide corpus statistics
For regulated multi-tenant corpora this finding changes the design. We built a 20-row table for tenant A with an RLS policy, one document mentioning "sanctions", and a reader role that sees only tenant A: Then, as superuser, we inserted 200 rows for tenant B, each containing "sanctions", and ran the same query as the reader:
Writes and VACUUM
Both are LSM-style. pg_search writes a new segment for every INSERT, UPDATE or COPY statement and merges in the background; each writing statement wants at least 15 MB of work_mem, and scores can include dead rows until VACUUM runs. pg_textsearch 1.4.0 writes to a WAL-logged memtable that spills to segments, compacts synchronously during the spill (background compaction is on main, unreleased), says sustained write-heavy workloads are not yet fully optimized, and offers bm25_force_merge() after bulk loads. In both, an UPDATE was searchable immediately. In pg_textsearch, two updates left total_docs at 5,185 for 5,183 live rows even after VACUUM, a small drift in the statistics. Two-phase commit: 1.4.0 accepted CREATE INDEX ... USING bm25 inside a prepared transaction in our test, but main's README now says PREPARE TRANSACTION is not supported after creating a BM25 index. If your stack uses XA, test before upgrading.
Replication decides it for on-prem HA
This is where an on-prem deployment changes the answer. If your Postgres runs on your own hardware with a physical streaming standby and automatic failover, this paragraph decides the choice. ParadeDB documents that the Community index "does not get physically replicated and won't be available on other nodes in a high availability setup"; HA and read replicas are Enterprise features. After a failover, search is gone until you rebuild. pg_textsearch fixed physical replication in 1.2.0 and moved to a paged memtable in 1.3.0 so standbys reconstruct every page from the normal WAL stream, which is also what lets it run on Aurora-style and AlloyDB-style storage. The README on main adds that query-serving standbys need hot_standby_feedback = on. On managed clouds the list is short. Google Cloud SQL documents pg_textsearch for PostgreSQL 17+ behind the cloudsql.enable_pg_textsearch flag; AlloyDB and Azure HorizonDB (version 1.3.1, no phrase or fuzzy) list it as preview; Azure Flexible Server has said it is not on the roadmap. Neither extension appeared on AWS's RDS or Aurora extension lists. pg_search on any of these means running ParadeDB yourself as a logical replication subscriber, on PostgreSQL 17+, which is one more system to patch, secure and put in the audit scope. The RAG systems pillar covers where keyword retrieval sits in the wider stack.
| Engine | Visible rows | Doc 1 score before | Doc 1 score after 200 hidden rows |
|---|---|---|---|
| pg_textsearch 1.4.0 | 20 | -2.9270 | -0.0800 |
| pg_search 0.25.11 | 20 | 2.5175 | 0.0644 |
AGPL vs PostgreSQL license: procurement questions
pg_search is AGPL-3.0. ParadeDB Enterprise is commercially licensed, waives the copyleft provision and adds closed-source features. pg_textsearch uses the PostgreSQL license. None of this is legal advice; these are the questions procurement will ask, and it is faster to bring the answers than to wait for them:
For teams that need failover, the license question and the HA question tend to merge: the Enterprise license removes AGPL from the conversation and supplies replicas at the same time. For teams that cannot add a vendor contract, pg_textsearch clears both with no negotiation, and phrase support is the price until its next release.
This week, run one small test: load 1,000 of your own documents into two scratch databases on one server, one per extension, and run 20 real queries from your logs through each, including one exact phrase and one misspelling. Then run EXPLAIN on each and fail over your staging standby. The plans and the failover will settle the choice faster than any benchmark.
FAQ
Quick answers to the questions this post tends to raise.



