PostgreSQL 全文搜索:tsvector、tsquery 与 GIN 索引

PostgreSQL 内置全文检索能力,无需引入 Elasticsearch 即可支撑中小规模的搜索。本文系统讲解 tsvector 文档向量与 tsquery 查询语法、to_tsvector/to_tsquery 分词与词典、@@ 匹配操作符、GIN 索引加速、rank 排序、高亮与中文分词(zhparser/jieba),以及全文搜索与业务搜索的工程架构。

引言

「搜索需求不大,还要不要引 Elasticsearch?」——中小规模(千万级以内)场景,PostgreSQL 内置的**全文检索(Full-Text Search)**足够好用:语法简单、与业务数据同库(无数据同步问题)、事务一致。本文从分词原理讲起,覆盖 tsvector/tsquery、@@ 匹配、GIN 索引加速、排序与高亮、以及中文分词(zhparser)这一国内落地核心难点,最后给出「何时用内置 FTS、何时上 ES」的决策框架。

前置:/postgres-index-types/(GIN 索引原理)、/postgres-query-optimization/(执行计划与索引策略)。


目录


1. 全文搜索的本质:分词、词干与倒排

数据库 LIKE '%关键词%' 是全表扫描且无法用索引;全文搜索要做三件事:

1. 分词(Tokenization):把文档切成词
2. 规范化(Normalization):词干还原、小写、去停用词
3. 倒排索引(Inverted Index):词 → 包含它的文档列表
文档:"The quick brown fox jumps"
分词+规范化 → 词位(lexeme): 'brown' 'fox' 'jump' 'quick'
倒排索引: 'brown' → [doc1, doc3]  'fox' → [doc1]

PostgreSQL 用 tsvector 存文档向量、tsquery 写查询,@@ 做匹配。搜索性能的关键是为 tsvector 建 GIN 索引。


2. tsvector 文档向量

to_tsvector(config, text) 把文本转成文档向量:

SELECT to_tsvector('english', 'The quick brown fox jumps over the lazy dog');
-- 'brown':4 'dog':8 'fox':5 'jump':6 'lazy':7 'quick':2
-- 输出词位 + 出现位置(词干还原:jumps→jump;停用词 the/a 被剔除)

直接构造 tsvector:

-- 手动指定词与位置
SELECT 'hello:1 world:3'::tsvector;

多字段组合文档(商品搜索常用 title + description + tags):

CREATE TABLE products (
    id serial PRIMARY KEY,
    title text,
    description text,
    tags text[],
    search_vec tsvector
);

-- 组合生成 tsvector,写入专用列
UPDATE products SET search_vec =
    setweight(to_tsvector('english', title), 'A') ||          -- A 权重最高
    setweight(to_tsvector('english', description), 'B') ||
    setweight(to_tsvector('english', array_to_string(tags, ' ')), 'C');

记忆:tsvector 是「词 + 位置 + 权重」的压缩文档表示;组合多字段用 || 合并,权重 A/B/C/D 影响排序。


3. tsquery 查询语法

to_tsquery 把查询词转成 tsquery 表达式:

-- 与、或、非、短语
SELECT to_tsquery('english', 'fox & dog');    -- 同时含 fox 和 dog
SELECT to_tsquery('english', 'fox | cat');    -- 含任一
SELECT to_tsquery('english', 'fox & !cat');   -- 含 fox 不含 cat
SELECT to_tsquery('english', 'quick <-> fox'); -- 相邻词(短语匹配)
SELECT to_tsquery('english', 'fox:*');        -- 前缀匹配(fox 开头的词)

三种写法:

写法示例说明
to_tsqueryto_tsquery('fox & dog')自动分词规范化(推荐)
plainto_tsqueryplainto_tsquery('fox dog')把整串按空格拆成 AND
websearch_to_tsquerywebsearch_to_tsquery('"fox dog" -cat')类 Web 语法(引号短语、减号排除)

实战:用户输入直接喂 websearch_to_tsquery 最安全——它不做词法前缀通配、避免用户输入 &|! 造成解析异常。


4. @@ 匹配与配置(文本搜索配置)

匹配操作符 @@:

-- 文档向量 @@ 查询向量
SELECT * FROM products
WHERE search_vec @@ to_tsquery('english', 'fox & dog');

-- 也可以直接对原始文本列匹配(无索引时慢)
SELECT * FROM products
WHERE to_tsvector('english', title) @@ to_tsquery('english', 'fox');

文本搜索配置(Text Search Configuration):决定用什么分词器与词典。

-- 查看当前配置
SHOW default_text_search_config;   -- 通常是 pg_catalog.english

-- 查询时显式指定配置
SELECT to_tsvector('chinese', content) ...   -- 中文配置需装扩展

配置的作用:同一文本用不同配置得到不同向量(英文 english 做词干还原、simple 不还原)。必须保证建索引与查询用同一配置,否则匹配失效。


5. GIN 索引加速搜索

全文搜索的倒排结构天然适合 GIN(Generalized Inverted Index):

-- 为 tsvector 列建 GIN 索引
CREATE INDEX idx_products_search ON products USING GIN (search_vec);

-- 查询走索引
EXPLAIN ANALYZE SELECT * FROM products
WHERE search_vec @@ to_tsquery('english', 'fox & dog');
-- Bitmap Index Scan on idx_products_search ✓

GIN 索引要点:

要点说明
命中条件查询必须用 @@ 且左侧是索引的 tsvector 列
组合查询search_vec @@ q AND price < 100 可与普通索引 AND 组合
更新成本GIN 维护慢(倒排更新);高频写场景用 fastupdate 默认开启
列存用「生成列」+ 表达式索引可省一列

表达式索引(不额外建列):

CREATE INDEX idx_products_search_expr ON products
  USING GIN (to_tsvector('english', title || ' ' || description));

记忆:搜索加速 = tsvector 列 + GIN 索引 + @@ 查询——三者缺一,命中不了 Bitmap Index Scan。


6. 排序与高亮

相关性排序:ts_rank/ts_rank_cd 按词频、权重、距离打分:

SELECT title, ts_rank(search_vec, to_tsquery('english', 'fox & dog')) AS rank
FROM products
WHERE search_vec @@ to_tsquery('english', 'fox & dog')
ORDER BY rank DESC
LIMIT 20;

权重的影响:标题(A 权重)命中 > 描述(B)命中,ts_rank 天然体现。

高亮:ts_headline 返回带 <b> 标记的上下文片段:

SELECT ts_headline('english', description,
                   to_tsquery('english', 'fox & dog'),
                   'StartSel=<mark>, StopSel=</mark>, MaxWords=20')
FROM products
WHERE search_vec @@ to_tsquery('english', 'fox & dog');

分页与稳定性:rank 相同需加 tie-breaker(id);深分页用 keyset 游标。


7. 中文分词:zhparser 落地

英文靠空格分词,中文必须专用分词器。主流方案 zhparser(基于 SCWS):

# 安装扩展
CREATE EXTENSION zhparser;

-- 创建中文文本搜索配置
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple;
-- 中文分词测试
SELECT to_tsvector('chinese', 'PostgreSQL 全文搜索实战指南');
-- 'postgresql':1 '全文':2 '搜索':3 '实战':4 '指南':5

中文配置要点:

□ 装扩展后必须自己建 chinese 配置并 ADD MAPPING
□ 映射规则:n(名)/v(动)/a(形)/i(成语)/e(语气) 等词性用 simple 词典
□ 分词粒度:zhparser 支持两种模式(整词 vs 按字符)由配置开关控制
□ 建索引与查询必须都用 'chinese' 配置

备选方案:jieba(通过 plpython/plrust 接入)、pg_jieba。轻量场景也可用 simple 配置 + 字符 ngram。


8. 全文搜索的工程架构

表结构落地:

-- 生成列 + 表达式索引(推荐,无需手工维护 search_vec)
ALTER TABLE products ADD COLUMN search_vec tsvector
  GENERATED ALWAYS AS (
    setweight(to_tsvector('chinese', title), 'A') ||
    setweight(to_tsvector('chinese', description), 'B')
  ) STORED;

CREATE INDEX idx_products_search ON products USING GIN (search_vec);

查询封装(应用层):

-- 组合:关键词 + 业务过滤
SELECT id, title
FROM products
WHERE search_vec @@ websearch_to_tsquery('chinese', $kw)
  AND category_id = $cid
  AND status = 'active'
ORDER BY ts_rank(search_vec, websearch_to_tsquery('chinese', $kw)) DESC
LIMIT 20;

注意点:

□ 中文配置在每次 to_tsquery 都要显式传,别依赖 default
□ 用户输入先清理(长度限制、危险字符)
□ 高频更新表的 GIN 膨胀:定期 REINDEX
□ 结果二次加工:应用层做个性化排序/过滤

9. 与 Elasticsearch 的选型决策

维度PostgreSQL FTSElasticsearch
数据量千万级内良好海量(亿级)
一致性与业务同库、事务一致近实时,需同步
部署零新增组件独立集群、运维成本
复杂查询相对简单聚合/模糊/多语言强大
中文zhparser 可用生态更成熟

决策框架:

□ <500 万行、简单关键词搜索 → PostgreSQL FTS(零运维)
□ 需要聚合分析/模糊纠错/多语言高亮 → Elasticsearch
□ 已有 ES 集群 → 别为了省事迁回 PG
□ 先 PG 起步,数据量起来再平滑迁 ES(抽象搜索接口)

记忆:搜索的取舍是「功能 vs 运维」:PG 内置 FTS 覆盖 80% 中小需求、零组件;一旦需要聚合/纠错/海量,再上 ES——别一开始就为搜索引集群。


10. 速查表与一句话记忆

需求手段
文档向量to_tsvector(config, text)
查询表达式to_tsquery / websearch_to_tsquery
匹配search_vec @@ query
加速tsvector 列 + GIN 索引
相关性排序ts_rank(search_vec, query)
高亮ts_headline
权重setweight(vec, 'A'/'B') 再 `
中文分词zhparser + 自定义 chinese 配置
组合过滤@@ query AND category_id = ...
大规模迁移 Elasticsearch

一句话记忆:全文搜索 = to_tsvector 存向量 + tsquery 写查询 + @@ 匹配 + GIN 加速 + rank 排序;英文用 english 配置、中文上 zhparser;千万级以内让 PG 内置 FTS 干活,别轻易为搜索引集群。


延伸阅读

  • /postgres-index-types/ — GIN 索引原理与维护
  • /postgres-query-optimization/ — 执行计划与 Bitmap Index Scan
  • /postgres-jsonb-performance/ — JSONB 与 FTS 的配合
  • /postgres-sql-advanced/ — 正则与文本处理
  • [[postgresql]] — PostgreSQL 数据库专题

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 数据迁移实战:从 MySQL/MongoDB 迁到 PostgreSQL
  2. 云托管与 Serverless PostgreSQL:RDS/Aurora/Neon/Supabase 选型与实战
  3. PostgreSQL 数据库设计规范:范式、类型选择与迁移演进