添加模糊匹配前,先确定搜索匹配规则

使用 AI 辅助编写与复核。

本文目录

请智能体让搜索“更宽容”之前,先决定哪些差异应该被视为相等。两个标签看起来可能相同,却使用不同的 Unicode 序列。另一些标签则在大小写、重音或字符宽度上不同。把这些差异都混成一个问题,会让搜索需求难以复核。

第一版契约可以有意保持简单:把派生的键规范化为 NFC,再比较整个标签,同时保留大小写和重音差异。这个虚构示例采用的就是这项策略。它是一种产品选择,并不表示这种行为适合所有语言或搜索任务。

技术依据是 Unicode 17.0.0 的 Unicode 标准附录 #15,第 57 次修订版。它区分规范等价(canonical equivalence)与兼容等价(compatibility equivalence),并定义了各类规范化形式。JavaScript normalize() 参考文档 则说明了下文小型模型所用的字符串操作。

先确定相等,再讨论相关性

这里的搜索会把整个查询与整个标签进行比较。它不对局部匹配排序,不做分词,也不猜测输入错误。

选定规则如下:

  • 将查询和标签的派生键都规范化为 NFC。
  • 精确比较所得字符串,保留大小写和重音差异。
  • 空查询不返回任何记录。
  • 不去除首尾空格,也不合并空白字符。
  • 按给定记录的顺序返回匹配项。
  • 即使两个标签得到相同的规范化键,也保留各自的原始标签和 ID。

NFC 先进行规范分解,再进行规范组合。它使本示例中预组合的 é,与 e 加组合尖音符的序列可以比较为相等。它不会把 Café 变成 Cafe、把 CAFÉ 变成 Café,也不会把半角字符变成对应的全角字符。

NFKC 还会使用兼容映射。如果另行选定的策略需要,这些映射可能有用;但它们会有意消除 NFC 保留的某些区别。因此,改用 NFKC 会改变这个示例的预期匹配结果。它不是修复搜索的通用方案。

把可见标签与其精确序列放在一起

下面十条记录是原创虚构教学数据。即使字体让两个标签看起来一样,码点列仍能明确其实际内容。s2 包含一个组合重音符;它在 JavaScript 中的转义写法是 "Cafe\u0301"。s1 的预组合写法是 "Caf\u00E9"。

ID 原始标签 精确码点
s1 Café U+0043 U+0061 U+0066 U+00E9
s2 Café U+0043 U+0061 U+0066 U+0065 U+0301
s3 Cafe U+0043 U+0061 U+0066 U+0065
s4 CAFÉ U+0043 U+0041 U+0046 U+00C9
s5 カフェ U+30AB U+30D5 U+30A7
s6 カフェ U+FF76 U+FF8C U+FF6A
s7 ABC U+FF21 U+FF22 U+FF23
s8 ABC U+0041 U+0042 U+0043
s9 工作 U+5DE5 U+4F5C
s10 Work U+0057 U+006F U+0072 U+006B

不要仅仅为了方便比较,就改写已存储的显示标签。应从标签派生搜索键,并保留原记录作为显示来源。s1 与 s2 匹配,并不能证明它们是同一个人、对象或数据库行,所以本组示例数据会返回两个 ID。

日文和拉丁字符的宽度对照,是为了显露一项策略选择。在选定的 NFC 相等规则下,它们保持分离。这并不能证明日文读者在更广泛的信息查找界面中会偏好什么;后者需要另外的需求和证据。

写出完整的匹配与不匹配矩阵

下面每一行都规定了该查询针对全部十条记录的结果。“无”表示空结果集。查询 q13 在 Café 前后各有一个 U+0020 空格;q12 是空字符串。明确写出序列,可以防止外观相近的查询在审阅过程中被悄悄当作可互换的输入。

ID 查询 精确码点 匹配的 ID 不匹配的 ID
q01 "Café" U+0043 U+0061 U+0066 U+00E9 s1, s2 s3, s4, s5, s6, s7, s8, s9, s10
q02 "Café" U+0043 U+0061 U+0066 U+0065 U+0301 s1, s2 s3, s4, s5, s6, s7, s8, s9, s10
q03 "Cafe" U+0043 U+0061 U+0066 U+0065 s3 s1, s2, s4, s5, s6, s7, s8, s9, s10
q04 "CAFÉ" U+0043 U+0041 U+0046 U+00C9 s4 s1, s2, s3, s5, s6, s7, s8, s9, s10
q05 "cafe" U+0063 U+0061 U+0066 U+0065 无 s1, s2, s3, s4, s5, s6, s7, s8, s9, s10
q06 "カフェ" U+30AB U+30D5 U+30A7 s5 s1, s2, s3, s4, s6, s7, s8, s9, s10
q07 "カフェ" U+FF76 U+FF8C U+FF6A s6 s1, s2, s3, s4, s5, s7, s8, s9, s10
q08 "ABC" U+0041 U+0042 U+0043 s8 s1, s2, s3, s4, s5, s6, s7, s9, s10
q09 "ABC" U+FF21 U+FF22 U+FF23 s7 s1, s2, s3, s4, s5, s6, s8, s9, s10
q10 "工作" U+5DE5 U+4F5C s9 s1, s2, s3, s4, s5, s6, s7, s8, s10
q11 "Work" U+0057 U+006F U+0072 U+006B s10 s1, s2, s3, s4, s5, s6, s7, s8, s9
q12 "" (空序列) 无 s1, s2, s3, s4, s5, s6, s7, s8, s9, s10
q13 " Café " U+0020 U+0043 U+0061 U+0066 U+00E9 U+0020 无 s1, s2, s3, s4, s5, s6, s7, s8, s9, s10
q14 "Caf" U+0043 U+0061 U+0066 无 s1, s2, s3, s4, s5, s6, s7, s8, s9, s10
q15 "work" U+0077 U+006F U+0072 U+006B 无 s1, s2, s3, s4, s5, s6, s7, s8, s9, s10

两个规范等价的查询 q01 和 q02,返回相同的两条记录。去掉重音后,结果变成 s3。改成大写后,结果变成 s4,而小写的 cafe 不匹配任何记录。宽度对照仍然保持分离;q14 拒绝仅前缀匹配;带首尾空格的示例则显露了“不去除首尾空格”的选择。

这份矩阵是一份匹配契约。未来修改排序或分页时,不应悄悄重新定义它。如果产品以后引入大小写折叠、忽略重音的匹配或子串搜索,应为修改后的策略分别给出预期结果。

用小型模型检查策略

这个 JavaScript 函数只实现选定的相等规则:

function matchLabels(records, query) {
  if (query === "") return [];
  const queryKey = query.normalize("NFC");
  return records
    .filter(record => record.label.normalize("NFC") === queryKey)
    .map(record => record.id);
}

函数在比较时派生键,并按输入顺序返回 ID。它不修改标签、不对记录去重,也不新增排序。它要求输入是示例所展示的字符串和记录结构;非字符串输入的处理属于另一份输入契约。

为这篇文章进行的一次离线运行,将函数结果与十条记录上的全部 15 组人工查询预期进行了比较:150 个记录与查询组合的比较结果一致。该次运行还检查了记录的码点、规范等价对、保留的重音与宽度区别,以及输入记录是否保持不变。观察到的运行时是 Node v24.18.0,其报告的 Unicode 版本为 17.0,ICU 版本为 78.3。

这些检查针对的是这个教学模型及其示例数据。它们不是对应用搜索服务、数据库排序规则(collation)、浏览器渲染或已部署索引的测试。真实实现可能把匹配交给行为不同的其他组件,因此要验证实际决定匹配结果的组件。

把后续问题分开

“不区分大小写”仍然需要一条具体规则。把一个字符串转成小写,并不是语言相关比较的完整规范;本文模型既不转小写,也不做大小写折叠。同样,移除组合标记来近似实现忽略重音的匹配,也属于另一种转换,需要考虑其自身后果。

允许输入错误的搜索,还需要决定候选项、距离度量、限制和顺序。音译与分词也会引入新行为。不要因为任务说明用了“模糊”一词,就让这些行为意外进入实现。先确定相等规则的基线,再展示哪些额外输入应该匹配,以及结果应如何排序。

身份与安全比较应排除在这项搜索策略之外。返回两个规范等价的标签,对这个搜索示例有用;它不授权合并账号,也不表示可以把外观相似的标识符视为可互换。

实施时,要盘点实际的比较路径:键在哪里派生、现有索引中的标签如何更新,以及其他层是否还会应用自己的规范化或排序规则。确认实际使用的运行时和数据版本。模型中观察到的运行时,并不能证明另一个服务安装了什么。

复制这份搜索调查请求

为给定的十条虚构记录和十五个查询,定义整个标签的搜索匹配。
保留所有原始标签和 ID。
不要连接应用、数据库、线上索引或外部 API。

选定策略:
- 为标签和查询派生 NFC 键,然后精确比较字符串。
- 保留大小写和重音差异。不使用兼容规范化。
- 空查询不返回任何记录。
- 不去除首尾空格,也不合并空白字符。
- 按原始记录顺序返回匹配项;不去重。
- 不做子串搜索、大小写折叠、音译或模糊匹配。

同时列出可见标签和精确 Unicode 码点。保留 Café 与
Cafe 加组合重音符这两条独立记录;任一规范等价查询
都应返回两者。保持 Cafe 和 CAFÉ 相互独立。
在 NFC 下保持半角与全角片假名、拉丁字符相互独立。
展示空查询、首尾带空格、仅前缀和小写查询不匹配的情况。

为每个查询同时返回匹配与不匹配的 ID。保留原始
显示序列;派生键不能替换它。排序和分页不属于本次匹配变更。

如果检查一个小型离线模型,记录实际运行时、Unicode
版本、示例数据,以及实际结果与预期结果的对照。
不要把这类检查描述为生产搜索或数据库结果。指出仍缺少的
输入校验、索引迁移和真实比较层证据。
不要把这条搜索规则用于账号或安全身份的相等判断。

精确的搜索请求可以范围不大,同时依然有用。一旦基线能解释每组输入为什么匹配或保持分离,额外的匹配行为就会成为一项有示例可审阅、清楚可见的产品决定。