全文检索(Full-Text Search)入门
本教程共 50 篇 · 第 50 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
50. 全文检索(Full-Text Search)入门
本节目标:学完你能在 PostgreSQL 里做真正的「关键词搜索」,而不只是用 LIKE 做笨拙的模糊匹配,并会给搜索字段建 GIN 索引提速。
用 LIKE '%猫%' 找文章里的「猫」,有三个毛病:它不认词、大小写麻烦、而且没法用普通索引加速。PostgreSQL 自带全文检索(Full-Text Search),专门解决「在大量自然语言文本里找相关文档」这件事。它先把文本拆成「词(lexeme)」并归一化,再用倒排索引快速匹配。
Note全文检索不是用来做「精确等于」的,而是做「相关性搜索」。要精确匹配还是用
=或LIKE。
两个核心类型
全文检索靠两个专用类型配合:
tsvector:文档。存的是「归一化后的词 + 位置」,不是原文。tsquery:查询。存的是「要找的词 + 布尔关系(与/或/非)」。
先看最直观的例子,直接把一句话转成 tsvector:
postgres=# SELECT '猫 坐在 地毯 上 吃了 一条 鱼'::tsvector;
-- 输出:'上' '吃了' '坐' '在' '一条' '猫' '地毯' '鱼'
Note
::tsvector只是把空格当分隔的粗暴转换,它会去重、排序,但不做词形归一化。原始文本要先过to_tsvector才会真正「分词、归并词形」。
to_tsvector:把文本变成可搜的文档
to_tsvector 需要指定一个「文本搜索配置(text search configuration)」,它决定怎么分词、怎么归并词形。英文用 'english',不处理词形就用 'simple'。
postgres=# SELECT to_tsvector('english', 'The Fat Rats are running');
-- 输出:'fat':2 'rat':3 'run':5
注意 Fat 变成 fat、Rats 变成 rat、running 变成 run——这就是词干归一化(stemming)。所以搜 run 也能匹配 running,这是 LIKE 做不到的。后面跟着的 :2 :3 是词在原文里的位置编号,用于后面的「短语搜索」。
tsquery 与 @@ 匹配
tsquery 用 &(与)、|(或)、!(非)组合词。最方便的写法是 to_tsquery,它会顺手帮你归一化:
postgres=# SELECT to_tsquery('english', 'fat & rat');
-- 输出:'fat' & 'rat'
postgres=# SELECT to_tsquery('english', 'fat | cat');
-- 输出:'fat' | 'cat'
postgres=# SELECT to_tsquery('english', 'fat & !cat');
-- 输出:'fat' & !'cat'
匹配用 @@ 运算符,返回真/假:
postgres=# SELECT to_tsvector('english', 'The Fat Rats are running')
@@ to_tsquery('english', 'fat & rat');
-- 输出:t(true,匹配上了)
反过来不匹配的例子:
postgres=# SELECT to_tsvector('english', '安静的猫')
@@ to_tsquery('english', 'dog');
-- 输出:f
Tip
@@两边顺序可换:tsvector @@ tsquery或tsquery @@ tsvector都行。text @@ tsquery也能用,PG 会隐式帮你to_tsvector(用默认配置)。
短语搜索:<-> 运算符
<-> 表示「后面紧跟着的词」,用于短语匹配。比如想找 “cat” 后面紧跟 “dog”:
postgres=# SELECT to_tsvector('english', 'cat dog mouse')
@@ to_tsquery('english', 'cat <-> dog');
-- 输出:t(cat 紧跟着 dog)
postgres=# SELECT to_tsvector('english', 'cat mouse dog')
@@ to_tsquery('english', 'cat <-> dog');
-- 输出:f(中间隔了 mouse)
<-> 里的距离还能写成 <N> 表示「相距 N 个词位」(即中间隔 N-1 个词,因为 <-> 等价于 <1>);比如 <2> 表示中间隔一个词。
在表里真正搜:用 users 表
我们用前面统一的 users 表演示,把 username 和 email 拼起来做关键词搜索(长文本列写法完全一样)。
-- 假设 users 里已有数据
postgres=# SELECT username
FROM users
WHERE to_tsvector('english', username || ' ' || email)
@@ to_tsquery('english', 'john & gmail');
-- 找出用户名或邮箱里同时含 john 和 gmail 的用户
这里用 || 把两列拼起来一起建 tsvector,再用 @@ 比对查询。
Warning每次查询都现场
to_tsvector,数据量大了会慢,还用不上索引。生产环境应当把 tsvector 存成生成列(见下节)。
websearch_to_tsquery:贴近网页搜索习惯
如果你要让用户直接输入搜索框那种「free text」,用 websearch_to_tsquery 比 to_tsquery 友好——它容忍空格、支持 OR、-词 排除、"短语" 等:
postgres=# SELECT websearch_to_tsquery('english', 'john OR alice -test');
-- 解析成 'john' | 'alice' & !'test'
plainto_tsquery 更省心,把整段输入当成「与」关系的关键词,连 & 都不用写,最适合「用户随便输入一段词」:
postgres=# SELECT plainto_tsquery('english', 'running cats');
-- 输出:'run' & 'cat'
Tip用户输入千万别直接拼进
to_tsquery,语法错就整个查询崩。websearch_to_tsquery/plainto_tsquery更容错,适合接搜索框。
用 GIN 索引加速
正确做法是把 tsvector 存成「生成列(generated column)」,再在上面建 GIN 索引。
-- 加一个永远自动生成的搜索向量列
ALTER TABLE users
ADD COLUMN search_vec tsvector
GENERATED ALWAYS AS (to_tsvector('english', username || ' ' || email)) STORED;
-- 在生成列上建 GIN 索引(全文检索专用索引类型)
CREATE INDEX idx_users_search ON users USING GIN (search_vec);
有了这列和索引,查询就清爽又高效:
postgres=# SELECT username
FROM users
WHERE search_vec @@ to_tsquery('english', 'john & gmail');
NoteGIN(Generalized Inverted Index,通用倒排索引)是全文检索的标准索引。它和咱们第 39 章讲的 B-tree 不是一回事,专为「一个词出现在哪些文档」这种反向查找设计。
按相关性排序:ts_rank
光匹配还不够,搜索结果通常要按「相关度」排前面。用 ts_rank 算一个分数,再 ORDER BY:
postgres=# SELECT username,
ts_rank(search_vec, to_tsquery('english', 'john')) AS score
FROM users
WHERE search_vec @@ to_tsquery('english', 'john')
ORDER BY score DESC;
ts_rank 还有第二个参数(归一化标志),比如 ts_rank(search_vec, q, 2) 表示按文档长度归一化,长文档不会被天然抬高分数。真实搜索一般是「匹配且取最相关的前几条」:
postgres=# SELECT username,
ts_rank(search_vec, to_tsquery('english', 'john')) AS score
FROM users
WHERE search_vec @@ to_tsquery('english', 'john')
ORDER BY score DESC
LIMIT 10;
如果想让某些列权重更高(比如标题比正文重要),用 setweight 标记权重 A/B/C/D,再拼成 tsvector:
-- 演示:把 username 标为 A 权重,email 标为 B 权重
SELECT setweight(to_tsvector('english', username), 'A') ||
setweight(to_tsvector('english', email), 'B') AS weighted_vec
FROM users;
ts_headline:给结果加高亮
搜索结果常要标出命中词。用 ts_headline 从原文里截取并加 <b> 标签高亮:
postgres=# SELECT ts_headline('english', 'The fat rat is running',
to_tsquery('english', 'rat'),
'StartSel=<b>, StopSel=</b>');
-- 输出:The fat <b>rat</b> is running
高亮标签的起止标记可以自定义,方便前端渲染。
多语言配置
不同语言的分词规则不同。to_tsvector 第一个参数就是配置名。查一下系统里有哪些:
postgres=# SELECT cfgname FROM pg_ts_config;
常用的是 english、simple;其它语言(如法语、德语)PG 自带对应配置。处理中文等没有空格分词的语言,需要额外装分词插件(如 zhparser),这部份超出入门范围,记住「配置选错,搜索结果就错」即可。
-- 法语文本用 french 配置
postgres=# SELECT to_tsvector('french', 'Le chat noir dort');
Warning同一列里的
tsvector和tsquery必须用同一种配置,否则对不上。建生成列和写查询时,配置名要保持一致。比如生成列用'english',查询却用'simple',就很可能搜不到。
停用词与词典
全文检索会自动丢弃「停用词(stop words)」——像英文的 the、a、and 这类太常见、没检索价值的词,既不进 tsvector 也不参与匹配。这套规则由「词典(dictionary)」和「配置(configuration)」决定。你甚至可以自定义词典,把业务里的黑话加进去归一化。入门阶段只要知道「默认就会过滤停用词、所以搜 the 搜不到」即可,不必深究。
什么时候不该用全文检索
- 数据量很小、只是精确匹配:直接用
=或LIKE更简单。 - 只需要前缀匹配(如「以 abc 开头」):用普通 B-tree 索引 +
LIKE 'abc%'就够了。 - 中文等需要分词插件的语言,环境没装分词器时效果会很差,得先装
zhparser并建对应配置。
Tip全文检索和 LIKE 不互斥。可以
WHERE search_vec @@ q AND username LIKE 'A%'组合,既用 FTS 提速又加精确约束。
常见误区
- 用
::tsvector当归一化:它不做词干处理,搜run匹配不到running,要用to_tsvector。 - 直接把搜索框输入丢进
to_tsquery:用户输个标点就报错,改用websearch_to_tsquery。 - 查询配置和索引配置不一致:两边配置名必须相同。
- 以为 LIKE 和 FTS 一样快:LIKE ‘%x%’ 一般走不了索引,FTS + GIN 才能扛大量文本。
- 忘了给长文本建 GIN:只建生成列不建索引,查询还是全表扫描。
- 用 FTS 做精确等于:它算的是相关度,精确匹配请用
=。