首页 / PostgreSQL 入门教程 / 全文检索(Full-Text Search)入门

PostgreSQL 入门教程

全文检索(Full-Text Search)入门

本教程共 50 篇 · 第 50 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

PostgreSQLPostgreSQL 入门教程全文检索tsvectortsqueryGIN 索引

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 变成 fatRats 变成 ratrunning 变成 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 @@ tsquerytsquery @@ 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 表演示,把 usernameemail 拼起来做关键词搜索(长文本列写法完全一样)。

-- 假设 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_tsqueryto_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');
Note

GIN(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;

常用的是 englishsimple;其它语言(如法语、德语)PG 自带对应配置。处理中文等没有空格分词的语言,需要额外装分词插件(如 zhparser),这部份超出入门范围,记住「配置选错,搜索结果就错」即可。

-- 法语文本用 french 配置
postgres=# SELECT to_tsvector('french', 'Le chat noir dort');
Warning

同一列里的 tsvectortsquery 必须用同一种配置,否则对不上。建生成列和写查询时,配置名要保持一致。比如生成列用 '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 做精确等于:它算的是相关度,精确匹配请用 =
上一篇
角色与权限(ROLE/GRANT/REVOKE)
下一篇
已经是最后一篇啦