15.3 模糊匹配与拼写容错
全文检索把不同词形归一到 lexeme,却不会自动把错拼词改成正确词。
pg_trgm 从另一个角度补召回:不先理解语言,而是比较字符三元组。
这使它很适合人名、标题、标识符和拼写容错,也意味着它可能找到“长得像” 但业务上完全不相关的字符串。
15.3.1 pg_trgm 相似度与距离
三元组如何形成
trigram 是三个连续字符。pg_trgm 忽略非单词字符;对每个词,左侧补两个
空格,右侧补一个空格。官方例子中 cat 产生:
" c", " ca", "cat", "at "两个字符串共享的 trigram 越多,similarity(a, b) 越高。它的取值在
0–1,距离运算符则是:
a <-> b = 1 - similarity(a, b)这不是 Levenshtein edit distance。字符插入、删除或换位会同时影响周围 多个 trigram;短字符串可用 trigram 更少,一个字符的影响会更大。
查看:
SELECT shop_ch14.show_trgm('wireless');
SELECT
shop_ch14.similarity(
'wireless headphones',
'wireles hedphones'
);本章扩展装在 shop_ch14,所以函数和 operator class 都全限定。普通安装
可能在 public;不要假设 search_path。
similarity、word_similarity 与 strict 版本
三个函数回答不同问题:
| 函数 | 比较范围 |
|---|---|
similarity(a,b) | 两个完整字符串的 trigram 集合 |
word_similarity(a,b) | a 与 b 中任一连续 trigram extent |
strict_word_similarity(a,b) | extent 还必须落在词边界 |
例如 query 是一个短词,title 是多个词时,完整字符串 similarity 会被 title
额外字符稀释;word_similarity(query, title) 更像是在 title 中寻找最相似
局部。strict 版本适合不希望跨词边界拼接的场景。
参数顺序很重要。官方把 word_similarity(first, second) 描述为 first 的
trigram 集与 second 某一连续 extent 的最大相似。对称函数 similarity
可以交换,word_similarity 的语义不要想当然地交换。
本章用:
greatest(
shop_ch14.similarity(lower(p.title), lower(q.raw_query)),
shop_ch14.word_similarity(
lower(q.raw_query),
lower(p.title)
)
) AS score这样同时保留整串与局部词候选。lower 使表达式与索引表达式一致;官方文档
还说明默认构建中的 trigram similarity 本身不区分大小写,但显式规范化能
让应用合同与表达式索引更清楚。
参见
pg_trgm Functions and Operators。
函数分数、布尔门槛与距离排序
pg_trgm 提供三组布尔操作符:
% uses pg_trgm.similarity_threshold
<% uses pg_trgm.word_similarity_threshold
<<% uses pg_trgm.strict_word_similarity_threshold默认门槛在本章 PostgreSQL 18 文档中分别为 0.3、0.6、0.5。它们是 session
GUC,可以 SET LOCAL,不应靠某个连接之前遗留的值:
BEGIN;
SET LOCAL pg_trgm.similarity_threshold = 0.35;
SELECT product_id, title
FROM shop_ch15.product_search
WHERE lower(title)
OPERATOR(shop_ch14.%)
lower('wireles hedphones')
ORDER BY
shop_ch14.similarity(
lower(title),
lower('wireles hedphones')
) DESC,
product_id;
COMMIT;门槛控制 candidate set,函数分数控制 set 内次序。若只写:
ORDER BY similarity(...) DESC
LIMIT 10就会永远返回十行,即使后几行分数是 0;也不一定能利用 GIN 做候选过滤。 这是“Top-K 必有结果”最常见的误判。
本章 fuzzy_ranking 故意对过滤后的全部小集合排名,不设阈值。它让评估
框架看到低分尾部,也展示为何生产必须选择门槛、最小结果可信度或 no-result
行为。
索引操作类决定可用路径
本章:
CREATE INDEX product_search_title_trgm_idx
ON shop_ch15.product_search
USING gin (
lower(title) shop_ch14.gin_trgm_ops
);强制 probe:
SET enable_seqscan = off;
EXPLAIN (COSTS OFF)
SELECT product_id
FROM shop_ch15.product_search
WHERE lower(title)
OPERATOR(shop_ch14.%)
lower('wireles hedphones');得到:
Bitmap Index Scan on product_search_title_trgm_idx目录同时验证:
access_method = gin
operator_class = shop_ch14.gin_trgm_ops
valid/ready/live = truePostgreSQL 官方 pg_trgm 提供 GiST 与 GIN opclass。两者都支持相似操作符,
也支持 trigram 可提取的 LIKE、ILIKE、正则与等号查询;普通等值查询
通常还是 B-tree 更合适。
若需求是:
ORDER BY title <-> :query
LIMIT KGiST 能提供 KNN distance 路径;官方文档明确指出某些 distance Top-N 形式 能高效使用 GiST,而不能用 GIN 直接完成同样的有序扫描。选择前先写清楚是 “按门槛过滤大量候选”还是“按距离取最近 K 个”。
15.3.2 前缀、包含、拼写错误与候选召回
先按意图选算子
| 用户意图 | 首选起点 |
|---|---|
| SKU 完全相等 | B-tree = |
| 规范化前缀补全 | B-tree/pattern opclass 或专门前缀设计 |
| 任意位置包含 | trigram LIKE '%term%' |
| 错拼容忍 | % / word similarity + threshold |
| 词形、布尔词、短语 | FTS |
| 模型表达的语义 | vector |
能被 trigram 索引支持,不等于它是所有文本谓词的首选。用 trigram 做每一次 等值查询,通常比普通唯一/B-tree 索引更大、更贵、语义也更弱。
短输入是天然边界
trigram 需要可提取的三字符片段。官方文档提醒:LIKE 或正则模式若没有
可提取 trigram,会退化为 full-index scan。对一两个字符的自动补全:
- trigram 区分力低;
- 候选可能巨大;
- 门槛对短词异常敏感;
- 用户每敲一个字符就查询会放大负载。
常见策略:
length < 3 -> no fuzzy query / curated prefix path
length 3..N -> prefix or stricter threshold
longer typo text -> trigram candidate generation长度规则要按语言与规范化后的 token 定义,不要只按 UTF-8 byte 数。
规范化必须与索引表达式相同
本章索引的是:
lower(title)所以 predicate 也写 lower(title)。生产可能还要处理:
- Unicode normalization;
- accent folding;
- 标点与空白;
- 全角/半角;
- SKU 中应保留的连字符;
- locale/collation;
- 同义缩写。
若规范化函数不 immutable,或查询表达式和索引表达式不同,表达式索引可能 无法使用。更重要的是:过度规范化会把本来不同的业务标识合并。先把规则做成 测试向量,再决定存储列或表达式索引。
门槛不是一次拍脑袋
对每个 query,按 score 取候选并与标注比较:
SELECT
q.query_id,
p.product_id,
greatest(
shop_ch14.similarity(lower(p.title), lower(q.raw_query)),
shop_ch14.word_similarity(lower(q.raw_query), lower(p.title))
) AS score,
j.grade
FROM shop_ch15.eval_query AS q
JOIN shop_ch15.product_search AS p
ON p.active
AND p.category = q.category_filter
LEFT JOIN shop_ch15.relevance_judgment AS j
USING (query_id, product_id)
ORDER BY q.query_id, score DESC, p.product_id;再画出或列出不同 threshold 下:
candidate count
precision/recall
latency
index/heap buffers
no-result rate门槛可能按字段、查询长度或语言分层,但每多一个规则就多一个需要版本化和
回归的参数。不要在应用各处散落 0.2、0.3、0.45。
召回池与展示结果要分开
模糊匹配很适合 召回候选,不一定适合最终排序:
typo query
-> trigram top-N candidate ids
-> exact filters
-> FTS/vector/business features
-> fusion or re-rank
-> final K如果 title 很短、候选同质,trigram 分数可以直接做主要排序;如果 document 很长、字段复杂或业务相关性与字面差别大,它更适合作为一项 feature。
本章 q04 coffee bean grinder 的模糊前三名包含商品 14(coffee scale),
却漏掉标注相关的 espresso machine,说明“字符串看起来像”会压过工作流上
相关但字面不同的对象。
15.3.3 与全文检索的互补和重复
用失败矩阵判断互补
本章观察:
| query | FTS matches | fuzzy top-3 relevant | 解释 |
|---|---|---|---|
| q01 exact words | 1 | 3 | FTS 精确,fuzzy 扩展同类 |
| q02 two typos | 0 | 3 | trigram 补错拼 |
| q03 descriptive phrase | 1 | 3 | 描述命中与标题相似都可工作 |
| q04 grinder | 1 | 2 | fuzzy 被字面相似干扰 |
| q05 espresso task | 1 | 3 | 两路互补 |
| q06 trail hydration | 2 | 2 | 两路都漏一个字面较远对象 |
| q07 technical typos | 0 | 3 | trigram 补技术词错拼 |
| q08 exact long-tail | 1 | 3 | FTS 精确,fuzzy 给邻近书籍 |
这比“FTS + trigram 效果更好”更有用。它指出:
- 哪些查询需要 fallback;
- 哪些查询适合 union;
- 哪些弱候选可能污染最终结果;
- 下一批标注应该补什么失败类型。
不要做脆弱的二选一 fallback
一种常见实现:
if FTS has results:
use FTS only
else:
use trigram它简单,但“FTS 有一条结果”不代表召回充分。q06 的 FTS 有两条,却漏掉 第三条相关对象;fallback 不会启动。
更稳健的做法是并行产生有限候选:
lexical top N
UNION ALL
fuzzy top N然后保留 source/rank,去重并融合。这样能看到一条对象由几路支持,也能在 延迟预算不足时按证据关闭某一路。
也不要无条件扩大候选
多一路候选会增加:
- index/heap 读取;
- SQL 排序与去重;
- 重排器输入;
- 解释复杂度;
- 弱信号把强结果挤下去的机会。
本章 RRF 的 q02 就是反例:纯 fuzzy 把最理想商品排第一;vector 与 lexical 信息加入后,混合把商品 3 排到第一,NDCG 下降。
候选源的采用条件应写成:
incremental recall gain
vs latency/resource cost
vs ranking degradation
on a frozen and a fresh evaluation set可解释证据
给每个最终结果至少保留:
{
"product_id": 3,
"sources": {
"fuzzy": {"rank": 1, "score": 0.7},
"vector": {"rank": 2, "distance": 0.06}
},
"fusion": {"method": "rrf", "k": 60, "rank": 1}
}不必把内部数值全部暴露给最终用户,但调试与审核必须能重建。
一条可操作的路由原则
先不要设计“智能路由器”。以简单证据开始:
exact identifier detected -> exact path first
normal natural-language query -> FTS + bounded vector
likely typo / sparse FTS -> add trigram candidates
very short query -> avoid broad fuzzy scan
authorization filters -> always mandatory每条规则都需要 query segment 指标,最终也可能被统一并行候选替代。路由器 本身是一个模型;如果没有独立评估,它只是在隐藏失败。
本节验收
本章同时固定两类证据:
behavior:
fuzzy quality and per-query ranks
mechanics:
GIN operator class and forced bitmap index plan行为证据回答“结果如何”;机制证据回答“数据库能否走这条路径”。二者不能 相互替代。
上一节:PostgreSQL 全文检索 · 返回本章目录 · 下一节:可复现的向量检索 · 查看全书目录 · 查看索引中心