全文搜索并不是给字符串加一个更快的 LIKE '%keyword%'。一个可用的搜索引擎至少要完成文本规范化、查询解析、倒排索引检索和相关性排序。PostgreSQL 把这些能力放进数据库,通过 tsvector、tsquery、GIN 索引和排名函数,提供了一套适合中小规模内容检索的完整链路。
搜索引擎实际上在处理什么
假设文档中有一句话:
PostgreSQL databases are running reliably.
搜索系统不会简单地保存整句文本并逐行扫描。它通常会经过以下处理:
- 切分 token:识别单词、数字、URL 等文本单元。
- 规范化词形:例如把
databases归一为databas,把running归一为run。具体结果由文本搜索配置和词典决定。 - 过滤停用词:部分配置会忽略
are之类缺乏检索价值的高频词。 - 记录位置:保存词元在文档中的位置,以支持短语搜索和覆盖密度排名。
- 建立倒排索引:从“文档包含哪些词”反转为“某个词出现在哪些文档中”。
在 PostgreSQL 中,文档侧的处理结果用 tsvector 表示:
SELECT to_tsvector(
'english',
'PostgreSQL databases are running reliably.'
);
结果会类似:
'databas':2 'postgresql':1 'reliabl':5 'run':4
查询侧则使用 tsquery。例如:
SELECT plainto_tsquery('english', 'running database');
它会把用户输入转成可匹配的查询表达式。真正的匹配由 @@ 运算符完成:
SELECT to_tsvector('english', 'PostgreSQL databases are running reliably.')
@@ plainto_tsquery('english', 'running database');
这里的关键不是字符串是否完全相同,而是规范化后的词元是否匹配。
tsvector 与 tsquery 如何配合
PostgreSQL 将文档和查询分开建模,这让不同输入方式可以共享同一份索引。
常用查询构造函数包括:
plainto_tsquery:把普通文本中的词默认用 AND 连接,适合简单搜索框。phraseto_tsquery:保留词语顺序和距离语义,适合短语搜索。websearch_to_tsquery:接受更接近搜索引擎的输入,例如引号短语、OR和减号排除。to_tsquery:允许直接编写&、|、!、<->等查询操作符,更灵活,但不适合直接接收未经处理的用户输入。
例如:
SELECT websearch_to_tsquery(
'english',
'postgres search -mysql'
);
SELECT phraseto_tsquery(
'english',
'full text search'
);
SELECT to_tsquery(
'english',
'postgr:* & search'
);
最后一个例子中的 :* 是词元前缀匹配,并不是任意位置的子字符串匹配。因此,它不能直接替代所有模糊搜索需求。
文本搜索配置也必须与内容语言匹配。english 配置包含英语词形处理;simple 通常更接近小写化后的原词,不执行英语词干提取。对于中文、日文等不以空格自然分词的语言,需要额外评估分词器、扩展或外部预处理方案,不能仅把配置名称从 english 换掉就期待相同效果。
一套可以直接运行的搜索表
下面的示例使用 PostgreSQL 16 容器,创建带有加权搜索向量和 GIN 索引的文档表。运行前需要安装 Docker,并确认本机的 5432 端口未被占用。
docker run --name pg-search-demo -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:16
docker exec -i pg-search-demo psql -U postgres <<'SQL'
CREATE TABLE documents (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
search_vector tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(body, '')), 'B')
) STORED
);
CREATE INDEX documents_search_vector_gin
ON documents USING GIN (search_vector);
INSERT INTO documents (title, body) VALUES
('PostgreSQL full-text search',
'Build a search engine with tsvector, tsquery, ranking, and a GIN index.'),
('Reliable database operations',
'Monitor backups, replication, query latency, and database capacity.'),
('Search service architecture',
'A search service needs document processing, retrieval, and ranking.'),
('MySQL indexing notes',
'An introduction to relational database indexes and query plans.');
SELECT
id,
title,
ts_rank_cd(
search_vector,
websearch_to_tsquery('english', 'postgres search -mysql')
) AS rank
FROM documents
WHERE search_vector @@ websearch_to_tsquery(
'english',
'postgres search -mysql'
)
ORDER BY rank DESC, id
LIMIT 10;
SQL
示例使用生成列,是为了让 title 或 body 更新时自动重建 search_vector。标题被赋予权重 A,正文使用权重 B,从而让标题命中通常比正文命中更重要。
如果 PostgreSQL 版本或现有表结构不适合使用生成列,也可以维护普通 tsvector 列,并通过触发器或应用写入流程同步更新。但无论使用哪种方式,都要避免索引使用一种配置、查询却使用另一种配置,否则词形规范化结果可能不同,导致意外漏检。
GIN 负责召回,排名函数负责排序
GIN 索引适合回答“哪些行包含这些词元”。执行计划理想情况下会出现 Bitmap Index Scan 或相关的 GIN 索引路径:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title
FROM documents
WHERE search_vector @@ websearch_to_tsquery(
'english',
'postgres search'
);
但索引命中并不等于相关性排序已经完成。ts_rank 和 ts_rank_cd 会根据匹配词、权重、频率及位置信息计算分数。尤其是 ts_rank_cd,会利用词元位置考虑匹配覆盖密度。
这意味着下面的查询通常仍需要对候选结果计算排名并排序:
WITH query AS (
SELECT websearch_to_tsquery('english', 'database search') AS q
)
SELECT
d.id,
d.title,
ts_rank_cd(d.search_vector, query.q) AS rank
FROM documents AS d
CROSS JOIN query
WHERE d.search_vector @@ query.q
ORDER BY rank DESC
LIMIT 20;
当匹配集合很大时,这一步可能成为延迟来源。可以通过更有选择性的过滤条件、分区、时间范围、分类字段,或者两阶段召回与排序来控制候选集。GIN 本身并不会把所有文档预先按某个动态查询的相关性排好序。
如果需要结果摘要,可以这样实践:
SELECT
title,
ts_headline(
'english',
body,
websearch_to_tsquery('english', 'search ranking'),
'MaxWords=20, MinWords=8, StartSel=<mark>, StopSel=</mark>'
) AS snippet
FROM documents
WHERE search_vector @@ websearch_to_tsquery(
'english',
'search ranking'
);
如果摘要将被放入网页,仍应在应用层执行可靠的 HTML 转义或清洗,不要把高亮标记当成完整的安全边界。
什么时候应该采用 PostgreSQL 全文搜索
PostgreSQL 全文搜索特别适合以下场景:
- 数据本来就在 PostgreSQL 中,希望减少同步链路和额外基础设施。
- 搜索需要与权限、租户、状态、时间等关系型条件一起过滤。
- 查询规模可控,需求主要是词法匹配、布尔查询、短语和基础排名。
- 团队希望先交付一个可靠版本,再根据实际负载决定是否拆出独立搜索系统。
上线前建议检查:
- 文档和查询是否使用同一文本搜索配置。
- 是否为实际查询表达式建立了正确的 GIN 索引。
- 更新频率是否会带来可接受的索引写入和膨胀成本。
- 排名是否符合产品目标,而不仅是“有一个分数”。
- 拼写纠错、同义词、自动补全、跨语言分词和语义检索是否属于核心需求。
- 是否使用
EXPLAIN (ANALYZE, BUFFERS)在接近真实的数据量上验证计划。
当需求发展到复杂同义词管理、大规模分布式检索、高级聚合、个性化学习排序或向量与词法混合召回时,独立搜索平台可能更合适。但在引入更多系统之前,PostgreSQL 已经能把全文搜索最关键的几块——分析、索引、匹配和排序——组合成一条清晰、可维护的链路。