Module P-7·23 min read

tsvector, tsquery, GIN indexes, relevance ranking, and generated tsvector columns — search without Elasticsearch.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

P-7 — Full-Text Search

The standard answer to "we need search" is Elasticsearch. Spin up a cluster, sync your data, write query DSL, manage index mappings. For large-scale dedicated search, that trade-off sometimes makes sense. For most application search — finding documents, filtering content, search-as-you-type boxes — you already have everything you need inside PostgreSQL.

This module covers PostgreSQL's full-text search system: how it represents and indexes text, how queries work, how to rank results by relevance, and how to set it up so searches stay fast at scale.


The Problem With LIKE

The first instinct for search is LIKE:

sql

This works. It also requires a full sequential scan of the table for every search — no index can help with a leading wildcard. At a thousand rows it is fast. At a million rows it is a problem.

ILIKE (case-insensitive) is slower still. And neither handles linguistic variations: a search for "running" will not find articles containing "runs" or "ran."

Full-text search solves both problems: it is indexable and it understands language.


The Two Core Types

PostgreSQL full-text search is built around two data types:

tsvector — a preprocessed representation of a document. It is a sorted list of lexemes (normalized word stems) with their positions in the original text. When you convert text to tsvector, PostgreSQL normalizes words (removing stop words like "the", stemming "running" → "run"), and records where each term appeared.

tsquery — a search query. It is a boolean expression of lexemes that gets matched against a tsvector.

sql

The @@ operator is the match operator. It returns true if the tsvector satisfies the tsquery.


Building Queries with tsquery

to_tsquery

The most explicit form. Uses boolean operators:

sql

to_tsquery requires valid tsquery syntax. If the user types bare text, it will error.

plainto_tsquery

Takes plain text and converts it to an AND query. Safe for direct user input.

sql

websearch_to_tsquery

Understands Google-style search syntax. The best choice for search boxes.

sql

websearch_to_tsquery is the right function for production search boxes. It handles messy user input gracefully and supports the syntax users expect.


Searching a Table

Given a table of articles:

sql

A basic full-text search:

sql

This works but is slow — it calls to_tsvector on every row for every query. No index is used.


GIN Indexes: Making Search Fast

The solution is to pre-compute the tsvector and index it. The index type for tsvector is GIN (Generalized Inverted Index). A GIN index maps each lexeme to the set of rows that contain it — exactly like the index at the back of a book.

Option 1: Index on an Expression

sql

The query must use the exact same expression for the index to be used:

sql

Expression indexes work but have a maintenance overhead: the expression is recomputed on every INSERT and UPDATE.

A better pattern in PostgreSQL 12+: store the tsvector as a generated column and index that.

sql

Now queries are clean and simple:

sql

The search_vector column is maintained by PostgreSQL automatically on INSERT and UPDATE. The GIN index is built on the stored column. Query performance is fast.


Weighting Multiple Columns

Different columns have different importance — a match in the title should rank higher than a match in the body. setweight assigns a weight label (A, B, C, D — in descending importance) to a tsvector.

sql

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.