TechLead
Lesson 17 of 22
5 min read
PostgreSQL

Full-Text Search in PostgreSQL

Build powerful search with tsvector, tsquery, GIN indexes, ranking, and phrase search without Elasticsearch.

Why Full-Text Search in PostgreSQL?

PostgreSQL has built-in full-text search that handles stemming, ranking, phrase matching, and custom dictionaries. For many applications, it eliminates the need for Elasticsearch or Solr, keeping your stack simpler while providing powerful search capabilities.

tsvector & tsquery Basics

tsvector is a sorted list of normalized lexemes (words). tsquery is a search expression with boolean operators.

-- Convert text to tsvector (normalizes, stems, removes stop words)
SELECT to_tsvector('english', 'The quick brown foxes jumped over lazy dogs');
-- Result: 'brown':3 'dog':9 'fox':4 'jump':5 'lazi':8 'quick':2

-- Create a search query
SELECT to_tsquery('english', 'quick & fox');
-- Result: 'quick' & 'fox'

-- Match: does the document match the query?
SELECT to_tsvector('english', 'The quick brown fox')
    @@ to_tsquery('english', 'quick & fox');
-- Result: true

Adding Search to a Table

-- Option 1: Compute on the fly (simple but slower)
SELECT id, title FROM articles
WHERE to_tsvector('english', title || ' ' || body)
   @@ to_tsquery('english', 'postgresql & performance');

-- Option 2: Stored generated column + GIN index (recommended)
ALTER TABLE articles ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (
    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('english', coalesce(body, '')), 'B')
  ) STORED;

CREATE INDEX idx_articles_search ON articles USING GIN (search_vector);

-- Fast search using the index
SELECT id, title FROM articles
WHERE search_vector @@ to_tsquery('english', 'postgresql & performance')
ORDER BY ts_rank(search_vector, to_tsquery('english', 'postgresql & performance')) DESC
LIMIT 20;

Ranking & Highlighting

-- Rank results by relevance
SELECT
  id,
  title,
  ts_rank(search_vector, query) AS rank,
  ts_rank_cd(search_vector, query) AS rank_cd
FROM articles, to_tsquery('english', 'database & optimization') AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;

-- Highlight matching terms in results
SELECT
  id,
  ts_headline('english', body, to_tsquery('english', 'postgresql'),
    'StartSel=, StopSel=, MaxWords=50, MinWords=20'
  ) AS snippet
FROM articles
WHERE search_vector @@ to_tsquery('english', 'postgresql');

Phrase Search & Advanced Queries

-- Phrase search: words must appear adjacent
SELECT * FROM articles
WHERE search_vector @@ phraseto_tsquery('english', 'machine learning');

-- Prefix matching: find words starting with...
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english', 'post:*');

-- Boolean operators: & (AND), | (OR), ! (NOT), <-> (followed by)
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english',
  '(postgres | postgresql) & !mysql & performance'
);

-- Websearch-friendly syntax (PG11+)
SELECT * FROM articles
WHERE search_vector @@ websearch_to_tsquery('english',
  '"full text search" postgresql -mysql'
);

Full-Text Search Tips

  • • Use setweight() to boost title matches over body matches (A > B > C > D)
  • • Use pg_trgm extension alongside FTS for fuzzy / typo-tolerant search
  • • Generated columns keep the search vector automatically in sync with source columns
  • • For multilingual content, store the language per row and use it dynamically in to_tsvector
  • • Combine FTS with LIMIT and ts_rank for paginated, ranked results
  • • Consider unaccent dictionary for accent-insensitive search

Continue Learning