Skip to content
Discord Get Started

Full-Text Search

DB9 supports full-text search with language-specific tokenizers.

TokenizerAliasesDescription
jiebachinese, zhparserChinese word segmentation (jieba-rs)
chinese_ngramzhparser_ngramChinese + bigram overlay for multi-char words
simple-Whitespace tokenizer for English/Latin. No stemming, no stopword removal
english-Whitespace tokenizer plus a minimal stopword list (a, an, is, the). No stemming
english_stem-Snowball stemming plus the full PostgreSQL English stopword list (~174 words)
french-Snowball French stemming plus a French stopword list
ngram-Lowercased character bigrams, never spanning whitespace or punctuation — 数据库 indexes 数据 and 据库
ngram3trigramLowercased character trigrams, never spanning whitespace or punctuation
unigram-Lowercased single characters
SQL
SELECT
to_tsvector('french', 'les chats mangeaient les souris') AS french,
to_tsvector('ngram', 'database') AS ngram;

This returns 'chat':2 'mang':3 'sour':5 for french — the stopword les is dropped and the other words are stemmed — and 'ab':4 'as':6 'at':2 'ba':5 'da':1 'se':7 'ta':3 for ngram.

The default configuration is simple (SHOW default_text_search_config). Configuration names are matched exactly as written. Any other name — german or spanish, a differently-cased name such as English, or a schema-qualified name such as pg_catalog.english — fails with ERROR: internal error (XX000) in to_tsvector, to_tsquery, plainto_tsquery, phraseto_tsquery, websearch_to_tsquery and ts_headline. The error does not name the configuration, so check the configuration argument first when you see it.

GIN (Generalized Inverted Index) indexes are the standard way to accelerate full-text search queries. Create a GIN index on a tsvector column or expression to avoid sequential scans:

SQL
-- Index on a tsvector column
CREATE INDEX idx_tsv ON documents USING GIN (tsv);
-- Index on an expression
CREATE INDEX idx_content_fts ON documents
USING GIN (to_tsvector('simple', content));

The optimizer uses a GIN index when the @@ operator matches the indexed column or expression and the right-hand side is a to_tsquery / plainto_tsquery / websearch_to_tsquery call whose arguments are inlined literals. Two forms fall back to a Seq Scan over every row:

  • a driver-bound parameter (@@ websearch_to_tsquery('english', $1)), which is how most ORMs issue the query;
  • a tsquery literal — 'cat & dog'::tsquery, CAST('cat & dog' AS tsquery) and tsquery 'cat & dog' are not recognised as probes, even though they are inlined.

So holding a ready-made tsquery string and casting it is the one spelling that looks most direct and is never indexed. Results are identical either way, so nothing signals the fallback — check EXPLAIN ANALYZE before assuming the index is in play.

SQL
-- Create index
CREATE INDEX idx_content_fts ON documents
USING gin(to_tsvector('jieba', content));
-- Search
SELECT * FROM documents
WHERE to_tsvector('jieba', content) @@ plainto_tsquery('jieba', '关键词');

Use simple for exact-token matching, or english_stem when you want stemming and stopword removal (the closest match to PostgreSQL’s english configuration):

SQL
-- Exact tokens (no stemming)
CREATE INDEX idx_content_fts ON documents
USING gin(to_tsvector('simple', content));
SELECT * FROM documents
WHERE to_tsvector('simple', content) @@ to_tsquery('simple', 'keyword');
-- Stemmed search: a query for 'running' also matches 'runs'
CREATE INDEX idx_content_stem ON documents
USING gin(to_tsvector('english_stem', content));
SELECT * FROM documents
WHERE to_tsvector('english_stem', content) @@ plainto_tsquery('english_stem', 'running');

Use the same configuration on both sides of @@ and in the index expression. The configurations tokenize differently, so mixing them (for example indexing with simple but querying with english_stem) changes which rows match.

SQL
SELECT
content,
ts_rank(to_tsvector('jieba', content), plainto_tsquery('jieba', '搜索词')) as rank
FROM (VALUES
('全文搜索词法分析很重要'),
('数据库支持中文检索'),
('这段文字没有关键内容')
) AS d(content)
WHERE to_tsvector('jieba', content) @@ plainto_tsquery('jieba', '搜索词')
ORDER BY rank DESC
LIMIT 10;
FunctionDescriptionExample
plainto_tsquerySimple phraseplainto_tsquery('jieba', '人工智能')
to_tsqueryBoolean operatorsto_tsquery('simple', 'cat & dog')
phraseto_tsqueryExact phrasephraseto_tsquery('simple', 'hello world')
websearch_to_tsqueryGoogle-stylewebsearch_to_tsquery('simple', '"exact" -exclude')
SQL
-- AND: both terms must match
SELECT * FROM documents
WHERE tsv @@ to_tsquery('simple', 'database & performance');
-- OR: either term matches
SELECT * FROM documents
WHERE tsv @@ to_tsquery('simple', 'postgres | mysql');
-- NOT: exclude term
SELECT * FROM documents
WHERE tsv @@ to_tsquery('simple', 'database & !oracle');
-- Prefix matching
SELECT * FROM documents
WHERE tsv @@ to_tsquery('simple', 'data:*');
SQL
SELECT
ts_headline('jieba', content, plainto_tsquery('jieba', '数据库'),
'StartSel=<b>, StopSel=</b>, MaxWords=50'
) as highlighted
FROM (VALUES ('数据库支持中文全文检索')) AS d(content)
WHERE to_tsvector('jieba', content) @@ plainto_tsquery('jieba', '数据库');
SQL
-- Prioritize title matches over body
ALTER TABLE articles ADD COLUMN tsv tsvector;
UPDATE articles SET tsv =
setweight(to_tsvector('jieba', title), 'A') ||
setweight(to_tsvector('jieba', body), 'B');
-- Search with weighted ranking
WITH q AS (SELECT plainto_tsquery('jieba', '搜索词') AS query)
SELECT a.title, ts_rank(a.tsv, q.query) AS rank
FROM articles a, q
WHERE a.tsv @@ q.query
ORDER BY rank DESC;
LimitValue
Max tsvector value size1 MB
Max lexemes per tsvector~264,000
Supported text search configurationsjieba, chinese_ngram, simple, english, english_stem, french, ngram, ngram3, unigram, plus the aliases chinese, zhparser, zhparser_ngram, trigram
GIN index build strategySequential (full table scan)

See Limits and Quotas for the complete list.