# 同一个 é 在库里存了两种写法，搜索总漏结果，Unicode 规范化放哪一层合适？

**URL:** <https://www.shiqidu.com/t/topic/1880>\
**Category:** 开发调优\
**Tags:** JavaScript, 编程, 数据库\
**Created:** [2026 年11 月 1 日 07:15 UTC](https://www.shiqidu.com/t/topic/1880 "2026-11-01T07:15:45Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2026 年11 月 1 日 07:15 UTC](https://www.shiqidu.com/t/topic/1880/1 "2026-11-01T07:15:45Z")

</div>

用户敲进搜索框的字，来路很杂：不同的输入法、从网页或 PDF 里复制的片段、别的系统同步过来的文件名、App 端和 Web 端各自提交的表单。屏幕上都显示成 café，传到后端的码点却未必一样。英文和数字测得好好的，一碰到带重音的拉丁字母、全角字符、带圈数字，搜索就开始漏结果。这类问题往往要等用户反馈了才会发现。

## 一个字，两种存法

é 在 Unicode 里有两种合法写法：一种是预组合字符 U+00E9，另一种是普通的 e 后面跟一个组合重音符 U+0301。两者显示一样，按码点比较却不相等，连长度都不同：

```js
const a = 'caf\u00e9'; // 预组合
const b = 'cafe\u0301'; // e + 组合符

console.log(a === b); // false
console.log(a.length, b.length); // 4 5
console.log(a.normalize() === b.normalize()); // true

```

`normalize()` 不传参数时默认用 NFC，MDN 上有写。参数一共可以填四个值：

- NFC：先做规范分解，再规范组合，é 会合成一个码点。
- NFD：只做规范分解，é 拆成 e 加组合符。
- NFKC：先做兼容分解，再规范组合。
- NFKD：只做兼容分解。

传别的字符串会抛 `RangeError`。

UAX #15 里还有一句对排查很有用：只含 ASCII 的文本不受任何一种规范化影响，只含 Latin-1 的文本本身就已经是 NFC。所以拿纯英文用例去测，这个坑是测不出来的。

## 带 K 的两种要小心用

NFC / NFD 处理的是规范等价，前后意思完全不变。NFKC / NFKD 多做了一步兼容映射，会把一批「长得不同但常被当成一回事」的字符抹平：全角的 ＡＢＣ１２３ 变成半角，① 变成 1，ﬁ 连字拆成 f 和 i。做搜索要的正是这个效果。

代价是一些本来有含义的区别也被抹掉了。UAX #15 举了上标 i⁹ 变成 i9、¼ 变成 1/4 的例子。文档还明确说，NFKC / NFKD 不能随便套在所有文本上，最好把这两种形式看成和大小写转换差不多的操作。放到业务里，m² 规范化完会变成 m2，商品规格、化学式、数学公式里这种变化是不能接受的。

所以 NFKC 适合生成「比较用的键」，不适合覆盖原文。

## 各层怎么分工

比较稳的做法是原文照存、比较时统一规范化：

- 存储层保留用户提交的原文，顶多统一成 NFC。W3C 的字符模型建议 Web 内容一律用 NFC，UAX #15 里也引用了这条。NFC 不改变字义，拿来存原文问题不大。
- 单独加一列放搜索键，由后端在写入时生成，规则写死在一个函数里。前端顺手做一遍也行，只是 App、脚本、第三方导入都可能不经过前端，不能指望这一层。
- 查询时用同一个函数处理关键词，再去匹配搜索键那一列。写入和查询只要有一边漏了规范化，前面全白做。

```js
function searchKey(s) {
  return s.normalize('NFKC').toLowerCase().trim();
}

searchKey('ＡＢＣ１２３'); // 'abc123'
searchKey('m²'); // 'm2'
searchKey(' Café '); // 'café'

```

还有一点容易漏。UAX #15 提到，不管用哪种规范化形式，都不能保证拼接后还是规范的：两个各自规范化过的串，拼起来可能就不是了。所以搜索键要对拼好的整串算，别各字段算完再拼。

数据库这一层，有的自带规范化函数，有的排序规则能处理一部分等价字符。具体行为各数据库不一样，查自己的文档，再拿上面这几组例子实测。老数据可以写个一次性脚本把搜索键补齐，补完抽查一批带重音、全角的记录。

你们项目里的搜索键是在应用层算的，还是交给数据库的函数或排序规则去处理？
