引言
「搜索需求不大,还要不要引 Elasticsearch?」——中小规模(千万级以内)场景,PostgreSQL 内置的**全文检索(Full-Text Search)**足够好用:语法简单、与业务数据同库(无数据同步问题)、事务一致。本文从分词原理讲起,覆盖 tsvector/tsquery、@@ 匹配、GIN 索引加速、排序与高亮、以及中文分词(zhparser)这一国内落地核心难点,最后给出「何时用内置 FTS、何时上 ES」的决策框架。
前置:/postgres-index-types/(GIN 索引原理)、/postgres-query-optimization/(执行计划与索引策略)。
目录
- 1. 全文搜索的本质:分词、词干与倒排
- 2. tsvector 文档向量
- 3. tsquery 查询语法
- 4. @@ 匹配与配置(文本搜索配置)
- 5. GIN 索引加速搜索
- 6. 排序与高亮
- 7. 中文分词:zhparser 落地
- 8. 全文搜索的工程架构
- 9. 与 Elasticsearch 的选型决策
- 10. 速查表与一句话记忆
- 延伸阅读
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_tsquery | to_tsquery('fox & dog') | 自动分词规范化(推荐) |
plainto_tsquery | plainto_tsquery('fox dog') | 把整串按空格拆成 AND |
websearch_to_tsquery | websearch_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 FTS | Elasticsearch |
|---|---|---|
| 数据量 | 千万级内良好 | 海量(亿级) |
| 一致性 | 与业务同库、事务一致 | 近实时,需同步 |
| 部署 | 零新增组件 | 独立集群、运维成本 |
| 复杂查询 | 相对简单 | 聚合/模糊/多语言强大 |
| 中文 | 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 数据库专题
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。