跳至内容
15.3 模糊匹配与拼写容错

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

similarityword_similarity 与 strict 版本

三个函数回答不同问题:

函数比较范围
similarity(a,b)两个完整字符串的 trigram 集合
word_similarity(a,b)ab 中任一连续 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 = true

PostgreSQL 官方 pg_trgm 提供 GiST 与 GIN opclass。两者都支持相似操作符, 也支持 trigram 可提取的 LIKEILIKE、正则与等号查询;普通等值查询 通常还是 B-tree 更合适。

若需求是:

ORDER BY title <-> :query
LIMIT K

GiST 能提供 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.20.30.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 与全文检索的互补和重复

用失败矩阵判断互补

本章观察:

queryFTS matchesfuzzy top-3 relevant解释
q01 exact words13FTS 精确,fuzzy 扩展同类
q02 two typos03trigram 补错拼
q03 descriptive phrase13描述命中与标题相似都可工作
q04 grinder12fuzzy 被字面相似干扰
q05 espresso task13两路互补
q06 trail hydration22两路都漏一个字面较远对象
q07 technical typos03trigram 补技术词错拼
q08 exact long-tail13FTS 精确,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 全文检索 · 返回本章目录 · 下一节:可复现的向量检索 · 查看全书目录 · 查看索引中心

最后更新于