读者在站内搜索“事务”,可能希望找到标题里包含这两个字的文章;输入“transactoin”,可能希望系统理解拼写错误;搜索“数据库提交后还能撤回吗”,又期待系统找到相关主题。三种需求看起来都叫搜索,所需的匹配能力却不同。
PostgreSQL 的全文检索和 pg_trgm 分别处理词项、相似字符与连续子串。下面的 SQL 以 PostgreSQL 17 为基线,所有数字和表结构只用于演示。
全文检索需要可用的词项
全文检索先将文档转成 tsvector,再用 tsquery 表达查询。它可以处理词项组合、权重和位置关系。索引只能加速对已有词项的查找;如果分词结果不符合读者的语言习惯,建立 GIN 索引也不会让检索突然理解中文。
simple 配置不是中文词法切分方案。应先观察部署环境的解析结果:
SELECT alias, token, lexemes
FROM ts_debug('simple', '数据库事务隔离级别');
运行这段 SQL,先检查输入产生了哪些词项。默认解析器、词典和配置分别承担不同职责,见 PostgreSQL 解析器文档。中文按词搜索需要经过验证的切词流程,写入和查询两侧必须采用同一套预处理。
下面用英文标题与正文演示字段权重。假设 posts 已有 title、body、status 和唯一 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 将字符串表示为字符三元组,可用于相似度比较,也可辅助部分 LIKE 和 ILIKE 查询。拼写相近的标识符适合从这条路径寻找候选;同义词或不同措辞未必共享足够字符。
若目标是标题里的字面包含,可以评估以下索引:
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_cd 与 similarity 相加,分值可能没有可比较的含义。更容易解释的初始方案是分层排序或基于名次融合,并在每层保留稳定的 ID 作为并列排序条件。它不一定给出最佳相关性,但能帮助定位错误属于匹配还是排序。
不要给所有字段都建立相似度索引。正文很长时,索引体积与更新成本可能不适合仅用于纠错的需求。先针对标题、术语或独立词表做候选,更便于控制成本。
高亮与短词都需要产品边界
检索系统返回的高亮片段仍然包含源内容。ts_headline 并不保证输出适合直接插入网页;正文可能带有不可信 HTML。应对输出做适当清洗,或者返回纯文本和受控高亮片段结构,不能因为内容来自数据库就跳过渲染安全。
对极短查询,产品需要决定是否要求更多字符、只搜索标题,或接受一个有上限的扫描范围。应用不得偷偷把一部分候选截掉,却继续宣称结果完整。较长正文、常见中文词和空查询也要明确预算。
选型前准备一组覆盖词项、错拼和子串的查询,记录期望结果与实际词项。数据库内搜索的维护链路较短;中文分词、跨字段相关性或排序超出可维护范围时,再评估独立搜索服务。迁移时保留这组查询,后续版本用同一组结果做回归。
