52 Full-Text Search
PostgreSQL has a competent, built-in full-text search engine — no Elasticsearch, no Solr needed for most apps. The core abstractions are tsvector (a parsed-and-normalized representation of a document) and tsquery (a parsed search expression). With a GIN index on a tsvector, you get fast text search with stemming, ranking, and multi-language support, all in plain SQL.
What you’ll learn
to_tsvectorandto_tsquery- The
@@operator that tests a vector against a query - Configuring language (English, simple, etc.)
- Indexing with GIN for fast search
- Ranking results with
ts_rank
Example
to_tsvector('english', text) tokenizes the input, lowercases it, applies the English stemmer (so running and runs map to the same root), removes stop words (the, a, is), and returns a tsvector — a sorted list of normalized tokens with positions. Different configurations (english, simple, german, russian, …) implement different stemming and stop-word rules.
to_tsquery parses the expression: & (and), | (or), ! (not), <-> (followed by). websearch_to_tsquery accepts a friendlier syntax — double-quoted phrases, OR keyword, - to exclude. The latter is what you want behind a user-facing search box.
The @@ operator tests whether a tsvector matches a tsquery. With a GIN index on the tsvector, this is the fast path PostgreSQL uses for text search. The pattern above uses a generated column that always reflects title || ' ' || body — no triggers needed, no risk of forgetting to update.
Run the example
To run:
Common pitfalls
- Calling
to_tsvector('english', ...)in theWHEREclause without an expression index defeats indexing. Either index the expression or use a generated column. - Choosing the wrong language:
to_tsvector('english', russian_text)produces meaningless stems. Match the configuration to the data. LIKE '%foo%'is not full-text search. It is substring matching, slow without trigram indexes, and does not understand stemming or stop words. Use full-text search for natural-language queries.- For multi-language data, you may need to store the language alongside the text and parameterize the configuration.
For substring or fuzzy matching that does not need stemming, the pg_trgm extension provides trigram-based similarity (similarity(a, b)) and an index that supports LIKE '%foo%' efficiently. Often used alongside full-text search.
Run this before the final DROP TABLE docs_fts, or recreate the demo table first. Add more documents and search for them. Use <-> for phrase queries: to_tsquery('english', 'full <-> text') matches “full text” specifically. Add ts_headline('english', body, query) to highlight the matching snippet of each result.
Downloads
SQL file: full-text-search.sql · All lesson files · Setup guide