数优编码 All articles
编码原理

搜索召回率上不去?先看看你是不是把中文词切成了两半

数优编码
搜索召回率上不去?先看看你是不是把中文词切成了两半

Photo: Tore Danielsson (WMSE), CC0, via Wikimedia Commons

一个看起来无害的 substr

后端同学大概都写过这样的代码:字段太长,截取前 255 个字符入库。英文环境下它确实无害。可一旦字符串里是中文,而你的语言用的是「按字节截断」(比如某些老版本的字符串函数,或 Go 里对 []byte 的切片),问题就来了——一个汉字在 UTF-8 里占 3 个字节,你截到第 254 个字节,最后那个字就被劈成了两半,剩下一半是无效字节序列。

更隐蔽的是:即使你按「字符」截断,也可能出错。因为 Python、Java 里的「字符」在历史上是 UTF-16 code unit,而一个生僻字或 Emoji 需要两个 code unit 表示。截在代理对中间,同样得到一个坏字符。

码点、字节、字形簇:三层容易混淆的概念

处理多语言文本前,先把三个层次分清楚:

截断和搜索都必须在字形簇这一层操作,至少也要在码点层。在字节层操作,就是所有「搜不准」故事的起点。

为什么截断会毁掉搜索召回

中文没有空格分词,搜索引擎必须依赖分词器(如 IK、jieba)把「搜索引擎优化」切成词条。如果索引前某个字段被粗暴截断,可能出现两种灾难:

  1. 词被腰斩:「优化」只剩一个「优」,用户搜「优化」时这条文档永远召回不了。
  2. 坏字节污染分词器:无效的 UTF-8 序列会让部分分词器直接丢弃后续内容或抛异常,整条文档静默丢失。

这两种情况在监控里都不报错,只有用户投诉「搜不到」时才会暴露——非常典型的静默数据损坏。

Elasticsearch 中的正确姿势

ES 的 JVM 世界里字符串天然按 UTF-16 处理,所以 ES 本身不会截出半个字。真正的风险在写入链路:你的上游服务用某种语言的字节截断函数预处理过,或者 Logstash 的 truncate filter 配置成了按字节。检查三点:

PostgreSQL 全文检索的坑

PG 的 substring() 和 left() 都是按字符操作,这点对中文友好。但默认的全文检索配置基本不支持中文——to_tsvector('simple', ...) 会把整句中文当一个 token,等于搜什么都不命中。

实用方案:

陷阱清单

  1. 任何语言的「按字节截断」函数处理中文前,先确认字符边界。
  2. 截断后校验:text.encode('utf-8').decode('utf-8') 能否无损往返。
  3. 分词器版本和 ES 版本要匹配,IK 升级后记得重建索引。
  4. 别用 VARCHAR(n) 的字节数语义去套中文,PostgreSQL 的 n 是字符数,MySQL 的 n 在不同版本语义不同。
  5. 搜索召回异常时,先用 _analyze 或 ts_debug 看分词结果,别急着调 BM25 参数。

一句话总结:中文搜索不准,八成不是排序算法的问题,而是某个环节把字切坏了。先把字符边界守住,再谈调优。

All Articles

Related Articles

别再手动拼 URL 了:一次中文参数乱码背后的完整编码链路排查

别再手动拼 URL 了:一次中文参数乱码背后的完整编码链路排查

从「锟斤拷」到 Emoji 崩库:一场字符编码引发的血案复盘

从「锟斤拷」到 Emoji 崩库:一场字符编码引发的血案复盘

十种乱码长相速查表:从问号方块到「锟」字全家桶,对号入座就能修

十种乱码长相速查表:从问号方块到「锟」字全家桶,对号入座就能修