The real argument is not whether PostgreSQL can search text or whether Elasticsearch can store documents.
Both can.
The interesting question is where search complexity should live.
PostgreSQL full text search lives inside a transactional relational database with tsvector, tsquery, dictionaries, ranking, and GIN indexes. Elasticsearch is a distributed search and analytics engine built on Lucene, with analyzers, BM25 scoring, shard-based scale, aggregations, and near-real-time indexing.
Those are different operational philosophies before they are different feature lists.
If you are mapping this choice to storage, pipelines, and operations, this data infrastructure overview gives the wider system context.
What this comparison is actually about
At a low level, both systems rely on inverted-index ideas, but they package them very differently. PostgreSQL recommends GIN as the preferred text-search index type and describes it as an inverted index over lexemes in tsvector values. Elasticsearch analyzes text fields and indexes them for full-text search, then distributes those indexes across shards and nodes for scale. In practice, PostgreSQL feels like search embedded in your application database, while Elasticsearch feels like a dedicated search platform with its own runtime, lifecycle, and scaling model.
This comparison is mostly about native PostgreSQL full text search plus the very common pg_trgm helper for fuzzy-ish matching. That scope matters because the broader PostgreSQL ecosystem is getting more search-heavy over time. Extensions such as RUM add richer index behavior for phrase search and ranking-oriented scans, while PGroonga extends PostgreSQL with another full-text indexing path. That does not make native PostgreSQL equal to Elasticsearch, but it does mean the boundary is less static than many old comparisons assume.
My opinionated framing is simple. Search is usually a feature until it becomes a product surface. PostgreSQL tends to win while search is still a feature. Elasticsearch tends to win when search becomes the thing users judge first. That is less about brand names and more about where relevance logic, indexing policy, and operational pain are allowed to live.
How PostgreSQL full text search works
PostgreSQL full text search starts by turning raw text into lexemes. to_tsvector tokenizes text, normalizes it through the configured dictionaries, drops stop words, and stores surviving lexemes with positions. setweight lets you label lexemes from different parts of the document, such as title, abstract, and body, so those parts can influence ranking differently. PostgreSQL also supports multiple predefined language configurations and lets you build custom configurations with parsers and dictionaries.
If you want a compact SQL reference while implementing these patterns, this PostgreSQL cheatsheet and this SQL cheatsheet with the most useful SQL commands are practical companions.
A typical production pattern is a stored generated tsvector column plus a GIN index. PostgreSQL's documentation is blunt that practical text search usually requires an index, and it explicitly shows a stored generated column feeding a GIN index. That pattern avoids recomputing to_tsvector during verification and keeps the query surface clean.
alter table posts
add column search_vector tsvector
generated always as (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(summary, '')), 'B') ||
setweight(to_tsvector('english', coalesce(body, '')), 'D')
) stored;
create index posts_search_idx
on posts using gin (search_vector);
select id,
title,
ts_rank_cd(
search_vector,
websearch_to_tsquery('english', '"query planner" -mysql')
) as rank
from posts
where search_vector @@ websearch_to_tsquery('english', '"query planner" -mysql')
order by rank desc
limit 20;
On the query side, PostgreSQL gives you several parsers because user input is messier than engineering blogs admit. to_tsquery is explicit and powerful. phraseto_tsquery preserves word order with the <-> operator. websearch_to_tsquery accepts search-engine-like input, understands quoted phrases, OR, and - negation, and never raises syntax errors on raw user input. PostgreSQL also supports prefix matching by attaching * to a lexeme in to_tsquery.
Ranking is where native PostgreSQL shows both its strength and its ceiling. ts_rank and ts_rank_cd can use frequency, proximity, and structural weights, and the weighting model is surprisingly good for many application search tasks. At the same time, PostgreSQL's own docs note that ranking can be expensive and that the built-in ranking functions do not use global information. That is the quiet but important limit of native PostgreSQL full text search. It can rank, but relevance is not the center of gravity of the engine.
When PostgreSQL is enough for full text search
PostgreSQL is enough more often than dedicated search vendors would like. It is particularly compelling when search stays very close to transactional rows, joins, permissions, and fresh writes. PostgreSQL's MVCC model provides transactional consistency and snapshot-based reads, so the same database that accepts the write can answer the search without an Elasticsearch-style refresh window. When a search box is really "find records inside the app I just edited," that property matters more than glossy relevance demos.
It is also enough when SQL filtering is half the feature. Status filters, tenant isolation, publication states, timestamps, and relational joins often matter just as much as keyword relevance in line-of-business systems. In those cases, PostgreSQL full text search behaves like another indexed predicate in a relational query plan, not like a separate platform that needs to be fed and kept warm. That is a boring architecture, and boring is often the right kind of fast.
How Elasticsearch works as a search engine
Elasticsearch presents itself very differently. Its own docs define it as a distributed search and analytics engine, scalable data store, and vector database built on Apache Lucene, optimized for speed and relevance at production scale and operating in near real time. Elasticsearch splits each index into shards, replicates those shards, and distributes them across nodes to increase indexing and query capacity. This is why Elasticsearch is rarely "just an index." It is a cluster architecture.
Under the hood, analyzers do most of the heavy lifting. An Elasticsearch analyzer is a composition of character filters, tokenizers, and token filters. There are built-in analyzers, language analyzers, and custom analyzers, and synonym handling is a first-class part of analysis. That means search behavior is not only about the query. It is also about how both documents and queries are normalized before scoring even begins.
For a hands-on API reference while implementing these patterns, this Elasticsearch cheatsheet collects essential commands and operational shortcuts.
PUT posts
{
"mappings": {
"properties": {
"title": { "type": "text" },
"summary": { "type": "text" },
"body": { "type": "text" },
"tags": { "type": "keyword" }
}
}
}
GET posts/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "query planner",
"fields": ["title^3", "summary^2", "body"],
"type": "best_fields"
}
}
],
"must_not": [
{ "match": { "body": "mysql" } }
]
}
},
"aggs": {
"by_tag": {
"terms": {
"field": "tags"
}
}
},
"highlight": {
"fields": {
"body": {}
}
}
}
The Query DSL is where Elasticsearch starts to feel search-native rather than database-like. bool combines clauses with must, should, filter, and must_not. multi_match can search across many fields with field boosts and different execution modes such as best_fields, most_fields, cross_fields, phrase, and bool_prefix. Aggregations, highlights, and filters can all sit alongside the main query in the same request. BM25 is the default similarity model.
The freshness model is also explicit. Elasticsearch is near real time, not immediately search-consistent. Recent operations become visible to search when a refresh opens a new segment, and by default that refresh happens every second on indices that have been searched recently. Elastic's docs also warn that refreshes are resource-intensive and recommend waiting for periodic refreshes or using refresh=wait_for when a workflow needs read-after-write search visibility. That is a very different contract from PostgreSQL.
Why Elasticsearch usually ranks complex search better
This is the deepest technical reason many teams eventually move from PostgreSQL full text search to Elasticsearch. PostgreSQL's built-in ranking functions do not use global information, while Elasticsearch uses BM25 by default and exposes field-specific similarity settings, analyzers, multi-field query forms, and a search DSL designed around relevance tuning. Once search becomes less about "did it match" and more about "why did these ten results win," Elasticsearch usually has more expressive room.
Elasticsearch also has a clear bias toward denormalized documents. Its join field documentation explicitly warns against modeling multiple levels of relations to replicate a relational schema and recommends denormalization for better search performance. That design choice explains a lot of Elasticsearch's strengths and frustrations. It is not trying to be PostgreSQL with a faster LIKE. It is trying to be a search engine that can score and retrieve large document collections quickly.
PostgreSQL full text search vs Elasticsearch on real features
Typo tolerance is where the two systems diverge sharply. Elasticsearch provides fuzzy queries based on Levenshtein edit distance and also offers dedicated suggestion and as-you-type field types. PostgreSQL native full text search is not typo tolerant by itself. The usual PostgreSQL answer is pg_trgm, which adds similarity operators and index support for trigram similarity, LIKE, and ILIKE. That works well, but it is a composition strategy rather than one integrated search engine feature set.
Highlighting exists in both stacks, but the implementation details tell a story. PostgreSQL uses ts_headline, which can return useful snippets, yet the docs note that it uses the original document, can be slow, and is not guaranteed safe for direct insertion into web pages. Elasticsearch highlighting can use postings offsets or term vectors, which is especially valuable on large fields because it avoids reanalyzing the full text for every highlight request. In short, PostgreSQL can highlight, while Elasticsearch is built to highlight at scale.
Facets and search analytics are another fault line. Elasticsearch treats aggregations as a first-class part of the search model, with metric, bucket, and pipeline aggregations available directly in the search response. PostgreSQL can obviously aggregate because it is SQL, but once counted buckets, histograms, and composable search analytics become part of the search product itself, Elasticsearch feels much more native. The difference is not capability in principle. It is how much query ergonomics and performance policy the engine dedicates to that workload.
Autocomplete follows the same pattern. PostgreSQL can do prefix matching in to_tsquery, which is useful and often enough for internal tools. Elasticsearch goes further with search_as_you_type fields that automatically build multiple analyzed subfields for prefix and infix completion, plus completion suggesters that are purpose-built for fast suggestions. That gap is minor on an admin panel and major on a user-facing discovery surface.
Operational cost matters more than benchmark screenshots
The tempting search-engine question is "Is Elasticsearch faster than PostgreSQL for search?" The honest answer is "for what shape of search?" Elasticsearch is engineered around shards, replicas, bulk indexing, refresh policy, and lifecycle management. Elastic's own production docs go deep on shard strategy, bulk request sizing, indexing throughput, refresh intervals, and ILM. PostgreSQL avoids a second cluster, but GIN maintenance is not free. PostgreSQL's docs warn that GIN inserts can be slow, that pending-list cleanup can cause response-time fluctuations, and that autovacuum strategy matters if the index is updated heavily.
That makes the performance story more nuanced than most comparison posts admit. Elasticsearch usually has more headroom for large top-N lexical search, faceting, autocomplete, and distributed read volume because its architecture is dedicated to those tasks. PostgreSQL often feels faster for relational application queries with strict freshness requirements because there is no second datastore, no refresh boundary, and no sync path to debug. The winner is usually the workload shape, not the benchmark screenshot. That is partly an inference, but it follows directly from PostgreSQL's transactional MVCC model and Elasticsearch's near-real-time shard-based design.
Should transactional data and search indexes live in the same system? When search relevance is modest but freshness, permissions, and transactional truth are critical, the same-system design has obvious advantages. When search quality, faceting, synonym policy, typo tolerance, and horizontal search scale become first-class product concerns, a second system starts to look justified. Elasticsearch's own shard-sizing guidance says there is no one-size-fits-all strategy and recommends benchmarking production data on production hardware. That sentence captures the trade perfectly. Elasticsearch buys headroom by asking you to operate more search-specific architecture.
The practical verdict
PostgreSQL full text search wins the first 80 percent surprisingly often. It supports tokenization, stop words, stemming, phrase queries, weights, ranking, highlighting, generated search vectors, GIN indexes, and trigram-based similarity helpers. Combined with PostgreSQL's transactional semantics, it gives many applications a search stack that is simple, current, and close to the data. For SaaS back offices, internal tools, moderate content sites, and app-native search, that combination is hard to dismiss.
Elasticsearch becomes persuasive when search is not merely a filter but a product surface. BM25 by default, custom analyzers, synonym filters, fuzzy queries, multi-field ranking, aggregations, dedicated autocomplete options, large-field highlighting strategies, and distributed shard-based scaling are not side features. They are the reason the engine exists. That is why Elasticsearch comparisons that focus only on raw latency usually miss the point. The bigger difference is how much search product logic the engine is willing to own.
The cleanest mental model is this. PostgreSQL full text search is excellent when search belongs to the database. Elasticsearch is excellent when the database must feed a search platform. Most teams over-focus on speed and under-focus on failure modes. The real trade is where relevance tuning, data freshness, and operational complexity are allowed to reside.
Top comments (0)