PostgreSQL 站内搜索:全文检索与 pg_trgm 各自解决什么

区分词项检索、字符相似度和字面包含,给出 PostgreSQL 17 的索引与查询示例,说明中文分词、短词、排序和高亮的限制。

·6 min
左侧词项索引与右侧重叠三字符片段对照,分别连接各自的搜索结构。

读者在站内搜索“事务”,可能希望找到标题里包含这两个字的文章;输入“transactoin”,可能希望系统理解拼写错误;搜索“数据库提交后还能撤回吗”,又期待系统找到相关主题。三种需求看起来都叫搜索,所需的匹配能力却不同。

PostgreSQL 的全文检索和 pg_trgm 分别处理词项、相似字符与连续子串。下面的 SQL 以 PostgreSQL 17 为基线,所有数字和表结构只用于演示。

全文检索需要可用的词项

全文检索先将文档转成 tsvector,再用 tsquery 表达查询。它可以处理词项组合、权重和位置关系。索引只能加速对已有词项的查找;如果分词结果不符合读者的语言习惯,建立 GIN 索引也不会让检索突然理解中文。

simple 配置不是中文词法切分方案。应先观察部署环境的解析结果:

SELECT alias, token, lexemes
FROM ts_debug('simple', '数据库事务隔离级别');

运行这段 SQL,先检查输入产生了哪些词项。默认解析器、词典和配置分别承担不同职责,见 PostgreSQL 解析器文档。中文按词搜索需要经过验证的切词流程,写入和查询两侧必须采用同一套预处理。

下面用英文标题与正文演示字段权重。假设 posts 已有 titlebodystatus 和唯一 id,DDL 仅供独立实验库参考:

ALTER TABLE posts ADD COLUMN search_en tsvector
GENERATED ALWAYS AS (
  setweight(
    to_tsvector('english'::regconfig, coalesce(title, '')), 'A'
  ) ||
  setweight(
    to_tsvector('english'::regconfig, coalesce(body, '')), 'B'
  )
) STORED;

CREATE INDEX posts_search_en_published
ON posts USING GIN (search_en)
WHERE status = 'PUBLISHED';

标题与正文分别带权,方便排名函数区别处理。这里明确使用 english,只展示英文 FTS 的流程,不把它包装成中文搜索配置。

查询可用参数绑定:

WITH q AS (
  SELECT websearch_to_tsquery('english'::regconfig, $1) AS query
)
SELECT p.id, p.title,
       ts_rank_cd(p.search_en, q.query) AS rank
FROM posts AS p
CROSS JOIN q
WHERE p.status = 'PUBLISHED'
  AND p.search_en @@ q.query
ORDER BY rank DESC, p.id DESC
LIMIT 20;

websearch_to_tsquery 接受面向搜索的输入格式;它不是 SQL 参数转义机制,$1 仍应由驱动绑定。权重也不意味着标题必定排在所有正文命中之前,最终排名还受匹配位置等因素影响。相关行为可查 全文检索控制文档

pg_trgm 处理字符相似,不能代替语义判断

pg_trgm 将字符串表示为字符三元组,可用于相似度比较,也可辅助部分 LIKEILIKE 查询。拼写相近的标识符适合从这条路径寻找候选;同义词或不同措辞未必共享足够字符。

若目标是标题里的字面包含,可以评估以下索引:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX posts_title_trgm_published
ON posts USING GIN (title gin_trgm_ops)
WHERE status = 'PUBLISHED';

扩展安装权限及可用性由数据库环境决定。查询示例如下:

SELECT id, title
FROM posts
WHERE status = 'PUBLISHED'
  AND title ILIKE $1 ESCAPE '!'
ORDER BY id DESC
LIMIT 20;

应用把用户输入中的 !%_ 分别转义为 !!!%!_,再在两端加上 %,通过参数传入。参数绑定负责 SQL 安全,转义负责让用户输入保持字面含义,两者不能混为一谈。

如果用户输入本来就是一个搜索模式,产品也可以允许通配符,但要明确性能预算。能建立 trigram 索引,不代表每个模式都有可利用的三元组。无法提取有效三元组的模式可能退化为全索引扫描;中文一两个字的包含搜索尤其需要独立评估。详见 pg_trgm 索引说明

需要拼写相似查询时,可以用 % 相似度操作符筛选,再以 similarity 排序。它与 SQL LIKE 字符串中的百分号不是同一种用法。默认阈值也只是数据库设置,应该按实际标题长度和查询样本调整。

此外,GIN 不会自动让任意相似度排序变便宜。GiST 可以支持相应的距离近邻排序,GIN 更适合另一些过滤路径。选择索引前,应区分“先筛选再排序”与“按距离取最近若干项”的访问需求。

不同召回路径先保持结果可解释

对一个规模不大的技术博客,可以先把标题精确匹配、标题包含、拼写相似与正文词项匹配分开评估。记录每条结果来自哪种匹配,观察读者是否得到预期文章。

如果直接把 ts_rank_cdsimilarity 相加,分值可能没有可比较的含义。更容易解释的初始方案是分层排序或基于名次融合,并在每层保留稳定的 ID 作为并列排序条件。它不一定给出最佳相关性,但能帮助定位错误属于匹配还是排序。

不要给所有字段都建立相似度索引。正文很长时,索引体积与更新成本可能不适合仅用于纠错的需求。先针对标题、术语或独立词表做候选,更便于控制成本。

高亮与短词都需要产品边界

检索系统返回的高亮片段仍然包含源内容。ts_headline 并不保证输出适合直接插入网页;正文可能带有不可信 HTML。应对输出做适当清洗,或者返回纯文本和受控高亮片段结构,不能因为内容来自数据库就跳过渲染安全。

对极短查询,产品需要决定是否要求更多字符、只搜索标题,或接受一个有上限的扫描范围。应用不得偷偷把一部分候选截掉,却继续宣称结果完整。较长正文、常见中文词和空查询也要明确预算。

选型前准备一组覆盖词项、错拼和子串的查询,记录期望结果与实际词项。数据库内搜索的维护链路较短;中文分词、跨字段相关性或排序超出可维护范围时,再评估独立搜索服务。迁移时保留这组查询,后续版本用同一组结果做回归。