15.2 PostgreSQL 全文检索
PostgreSQL 全文检索不是 LIKE 的加速版。它把原文处理成带词位、权重的
lexeme 向量,把查询处理成逻辑表达式,再用 @@ 匹配、用 ranking function
排序。
本节先只讨论词法证据。拼写容错与向量语义分别留给后两节。
15.2.1 文档、词典、配置与 tsvector
文档是检索单位,不一定是一列
全文检索中的 document 是“一次返回和排序的单位”。它可以是:
- 一篇文章;
- 一个商品;
- 一封邮件;
- 多列拼接后的一个业务对象;
- 甚至跨表构造的投影。
本章把一个商品行视为 document,并只索引标题与描述。不要先把所有字符串 列都拼进去:SKU、类目、品牌、权限标签往往需要精确过滤或独立权重,盲目 拼接会同时损害相关性和索引体积。
从原文到 tsvector 的管线是:
text
-> parser: split and classify tokens
-> dictionary chain: recognize / normalize / discard
-> lexemes + positions + optional weights
-> tsvector四类 PostgreSQL 对象各有职责:
| 对象 | 职责 |
|---|---|
| parser | 切 token 并判定 token type |
| dictionary | 把 token 归一为 lexeme,或认定 stop word |
| template | 提供 dictionary 的底层行为 |
| configuration | 把 parser token type 映射到 dictionary 链 |
官方文档把 token 解释为原始片段,把 lexeme 解释为用于索引的归一化词。 例如英语配置会去掉常见 stop word,并把一些复数/词形归到词干。参见 Full Text Search Introduction。
配置必须是数据合同的一部分
下面写法依赖会话/数据库/集群默认值:
to_tsvector(title || ' ' || description)同一数据迁到另一个集群,default_text_search_config 不同,就可能生成不同
lexeme。更危险的是建索引和查询端使用了不同配置:SQL 都能执行,却永远
错过部分匹配。
本章始终显式写:
'pg_catalog.english'::pg_catalog.regconfig并用全限定名称避免受 search_path 影响。生产 ADR 至少记录:
configuration OID/name
dictionary files and versions
custom synonym/thesaurus source
language routing rule
reindex/rebuild procedureenglish 适合本章英语夹具,不适合中文照抄。语言不是一个装饰参数:中文
如何分词、多语言文档如何路由、专有名词是否要保留原形,都会改变 document
和 query 的共同词表。
检查当前配置:
SHOW default_text_search_config;
\dF
SELECT
cfgname,
pg_get_userbyid(cfgowner) AS owner,
cfgnamespace::regnamespace
FROM pg_ts_config
ORDER BY cfgnamespace::regnamespace::text, cfgname;调试某段文本:
SELECT *
FROM ts_debug(
'pg_catalog.english'::regconfig,
'Making espresso at home'
);ts_debug 能显示 token type、dictionary、lexeme 与是否被丢弃,是处理
“为什么没命中”的第一证据,而不是先调相关性权重。
多字段用权重表达结构
本章定义:
search_document tsvector
GENERATED ALWAYS AS (
pg_catalog.setweight(
pg_catalog.to_tsvector(
'pg_catalog.english'::pg_catalog.regconfig,
coalesce(title, '')
),
'A'
)
||
pg_catalog.setweight(
pg_catalog.to_tsvector(
'pg_catalog.english'::pg_catalog.regconfig,
coalesce(description, '')
),
'B'
)
) STORED三个细节都不能省:
coalesce防止一列NULL让整个 document 变成NULL;- title 用 A、description 用 B,保留字段结构给 ranking;
- configuration 显式固定,生成列和查询端一致。
默认 ts_rank/ts_rank_cd 权重按 D、C、B、A 分别为
0.1, 0.2, 0.4, 1.0。这只是默认,不是商品搜索的普遍最优值。
为什么用存储生成列
存储生成列在行写入/更新时计算,占用真实存储;读取时不必重新解析全文。
PostgreSQL 还保证它随基列变化自动更新,应用不能绕过表达式写入另一份
不一致的 tsvector。
官方限制生成表达式只能使用 immutable function,不能含子查询,也不能 引用当前行之外的数据。参见 Generated Columns。
因此它适合:
- 稳定、行内、确定性的文本预处理;
- 明确的 parser/dictionary 配置;
- 由数据库维护的一致索引列。
它不适合直接调用:
- 外部 embedding API;
- 会变化的跨表同义词;
- 当前时间;
- 随机或非确定函数。
外部模型输出应是普通列加模型身份与作业状态,由受控异步流程写入。
用目录证明生成关系
看值:
SELECT
product_id,
title,
search_document
FROM shop_ch15.product_search
ORDER BY product_id;看生成属性与表达式:
SELECT
a.attname,
a.attgenerated,
pg_get_expr(d.adbin, d.adrelid) AS expression
FROM pg_attribute AS a
JOIN pg_attrdef AS d
ON d.adrelid = a.attrelid
AND d.adnum = a.attnum
WHERE a.attrelid = 'shop_ch15.product_search'::regclass
AND a.attname = 'search_document';attgenerated = 's' 表示 stored。verify.sql 还会为 17 行重新计算表达式,
逐行与存储值比较,避免只有目录声明正确、数据却来自旧表达式。
15.2.2 查询语法、权重与相关性排序
tsquery 不是原始字符串
@@ 比较的是 document 与 query:
search_document @@ parsed_query常用构造函数:
| 函数 | 输入合同 | 典型用途 |
|---|---|---|
plainto_tsquery | 普通文本,存活词之间加 AND | 简单所有词查询 |
phraseto_tsquery | 普通文本,保留词位顺序/stop word 距离 | 短语 |
websearch_to_tsquery | web 风格文本、引号、OR、减号 | 面向原始用户输入 |
to_tsquery | 严格运算符表达式 | 受控高级查询语言 |
to_tsquery 能表达 &、|、!、<->、权重和前缀,但它要求输入已经
符合语法。把 raw user text 直接传入,标点或漏写运算符就可能报错。
本章选择:
websearch_to_tsquery('pg_catalog.english', query.raw_query)官方文档说明它不会抛出 syntax error,并支持:
unquoted words -> AND
"quoted phrase" -> FOLLOWED BY
OR -> OR
-term -> NOT“不会语法报错”并不等于“没有风险”。要处理:
SELECT
websearch_to_tsquery('pg_catalog.english', :raw) AS parsed,
numnode(websearch_to_tsquery('pg_catalog.english', :raw)) AS nodes;空 query、全部是 stop word、节点过多、负向条件过多或输入过长,都应有接口
级策略与 statement_timeout。
先用 @@ 缩小,再计算 rank
本章词法排名:
WITH scored AS (
SELECT
q.query_id,
p.product_id,
ts_rank_cd(
p.search_document,
websearch_to_tsquery(
'pg_catalog.english',
q.raw_query
),
32
)::double precision AS score
FROM shop_ch15.eval_query AS q
JOIN shop_ch15.product_search AS p
ON p.active
AND p.category = q.category_filter
WHERE p.search_document @@
websearch_to_tsquery(
'pg_catalog.english',
q.raw_query
)
)
SELECT ...;@@ 是候选条件;ts_rank_cd 只对命中 document 排序。若对所有行先算 rank
再筛选,会放大 CPU 与 I/O。
ts_rank 主要基于匹配 lexeme 频率;ts_rank_cd 还考虑 cover density,
也就是匹配词在文档中的接近程度,并依赖词位信息。对被 strip 掉词位的
tsvector,cover density 信息会丢失。
normalization 不是概率校准
本章给 ts_rank_cd 的第三个参数为 32:
rank / (rank + 1)它把正分数压进 0–1 范围,但官方文档特别说明,这只是 cosmetic scaling, 不会改变排序,也不能产生全局百分比。
所以这些写法没有理论依据:
-- 错:看到都在 0..1 就当同一量纲
0.5 * ts_rank_normalized
+ 0.5 * cosine_similarity全文 rank 的 0.7、余弦相似的 0.7、trigram similarity 的 0.7 来自不同 生成机制,含义不相同。要么用训练/标注数据做分数校准,要么像本章一样用 只依赖名次的 RRF。
权重是可验证假设
setweight(..., 'A') 只给 lexeme 标标签;最终权重由 ranking function 的
weight array 决定:
ts_rank_cd(
ARRAY[0.1, 0.2, 0.4, 1.0]::real[],
search_document,
parsed_query,
32
)数组顺序是 D、C、B、A,不是 A、B、C、D。调权重时应记录:
which fields map to which labels
weight vector
query set version
metric deltas per query segment不要因为一个标题命中的样例“看起来更好”就把 A 权重调到很大;它可能把描述 中的强意图全部压下去。
高亮不是匹配证据本身
ts_headline 可以生成带命中词的摘要,但它会重新处理原始 document,且
输出是展示材料,不应替代 @@ 与 rank 证据。生产还要:
- 对输出做 HTML escaping;
- 控制片段长度和调用成本;
- 不把未经授权的原文片段泄露给用户;
- 缓存时把语言、query 与权限身份纳入 key。
先返回稳定 document id 和 rank,再在安全边界内生成展示片段。
15.2.3 GIN/GiST 索引、更新与语言边界
GIN 是通常的全文首选
PostgreSQL 支持用 GIN 或 GiST 加速全文检索。官方文档把 GIN 称为 preferred text search index type:
CREATE INDEX product_search_fts_idx
ON shop_ch15.product_search
USING gin (search_document);GIN 为每个 lexeme 保存匹配位置列表,适合多词交集。它不在索引里保存
tsvector 的权重标签,所以带权查询需要回表 recheck。
GiST 用固定长度 signature 表示 document,是 lossy 的,可能产生 false
match,由 PostgreSQL 自动回表排除。GiST 可以 INCLUDE 其他列,而 GIN/
GiST 在写入、体积、构建和查询上的权衡应由目标数据测量。
参见 Preferred Index Types for Text Search。
本章不是 GIN/GiST benchmark。它选择 GIN 是因为普通 document FTS 的候选 生成合同明确,并用计划证明 opclass:
Bitmap Heap Scan on product_search
Recheck Cond: (search_document @@ ...)
-> Bitmap Index Scan on product_search_fts_idx由于只有 17 行,正常 planner 很可能觉得顺序扫描更便宜。实验临时:
SET enable_seqscan = off;只证明路径存在。真正上线要恢复默认 planner 设置,在代表性规模下看
EXPLAIN (ANALYZE, BUFFERS, WAL) 和延迟分布。
存储列、表达式索引与触发器
三种常见维护方式:
| 方式 | 优点 | 代价 |
|---|---|---|
tsvector 存储生成列 + GIN | 自动一致、值可见、查询清晰 | 行与 WAL 增大,表达式受 immutable 限制 |
GIN(to_tsvector(config, text)) 表达式索引 | 不存单独列 | 查询表达式必须匹配,rank 仍需计算 document |
普通 tsvector + trigger/job | 可跨更复杂来源 | 有漂移/失败状态,必须治理回填 |
如果 document 只来自当前行且表达式稳定,本章偏好存储生成列。若 document 跨表、依赖外部词典版本或异步 enrichment,就把刷新状态建成显式数据模型, 不要伪装成同步生成列。
每次内容更新都可能更新多个索引
一次 title/description UPDATE 可能同时:
- 生成新
tsvector; - 更新 FTS GIN;
- 更新标题 trigram GIN;
- 若重新生成 embedding,更新 HNSW;
- 产生 heap/index WAL;
- 在副本重放;
- 留下旧 tuple/index entry 等待 vacuum。
“搜索读得快”可能把成本转给写入、WAL、autovacuum 和副本延迟。生产实验要 分别测:
bulk initial build
steady inserts
document updates
embedding-only updates
concurrent index build
vacuum/reindex
standby replay不能用 pg_relation_size 的一个时间点代替整个生命周期。
GIN pending list 与维护预算
GIN 默认可用 fast update,把新条目先放入 pending list,之后批量合并。 这通常降低前台写入成本,却可能让后续查询或 cleanup 承担尖峰。检查:
SELECT *
FROM gin_metapage_info(
get_raw_page(
'shop_ch15.product_search_fts_idx',
0
)
);这需要 pageinspect 和相应权限,只应由运维角色执行。更通用的观测包括:
SELECT
pg_relation_size('shop_ch15.product_search_fts_idx') AS index_bytes,
last_autovacuum,
n_dead_tup
FROM pg_stat_user_tables
WHERE relid = 'shop_ch15.product_search'::regclass;参数选择应结合更新率、gin_pending_list_limit、autovacuum 与延迟尖峰,而
不是全局照抄一组值。
语言边界是功能边界
本章的英语结果:
wireless headphones -> 1 lexical match
wireles hedphones -> 0
music on the go -> 1
trail hydration -> 2
postgre databse tuning -> 0这证明:
- stemming/stop word 能处理词法变体与普通短语;
- 它不会自动修正
wireles、hedphones、databse; - AND 语义可能把只命中部分词的 document 排除。
对中文或混合语言,先回答:
how language is detected
how text is segmented
which dictionaries normalize which token types
how names/SKUs/code are preserved
how query and document choose the same config
how config changes trigger rebuild and re-evaluation如果需要外部分词扩展,回到第 14 章的扩展 ADR:软件包、权限、升级、备份、 主备节点和退出都要重新评审。
何时停止扩展 FTS
PostgreSQL FTS 很适合与事务数据同库、查询边界明确、规模和 ranking 需求 可控的场景。以下信号值得重新评估架构,而不是继续堆 SQL:
- 多语言/自定义分析链远超当前 parser/dictionary 能力;
- 需要复杂学习排序、拼写纠正、聚合/facet 与实时个性化;
- 搜索写入和主交易表索引维护互相伤害;
- 索引规模、构建窗口或副本恢复不满足 SLO;
- 需要独立扩缩容、故障域或跨多源索引。
重新评估不等于立刻外置。先用本章评估集和成本证据比较 PostgreSQL、扩展与 外部搜索系统,保持双写、回放、校验和退出路径。
上一节:先定义检索任务与评估集 · 返回本章目录 · 下一节:模糊匹配与拼写容错 · 查看全书目录 · 查看索引中心