ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【DIY系列:自己动手写分布式搜索引擎】第20篇:模糊搜索与拼写纠错实战

【DIY系列:自己动手写分布式搜索引擎】第20篇:模糊搜索与拼写纠错实战

上一篇【第19篇】编辑距离有限状态机:拼写纠错的底层武器
下一篇【第21篇】迷你检索引擎完整跑通:Hello Search!


摘要

上篇把编辑距离和纠错 FSM 这把武器锻好了,这一篇真刀真枪把它装到引擎上。你会看到纠错怎么无缝融进查询流程:用户打错时,引擎先精确查、查不到再走容错召回;"你是不是想搜:无线耳机"这条 did you mean 提示是怎么生成的;同音字、形近字这些现实坑怎么填;以及中英文纠错为什么完全是两码事。一篇把"纠错"从纸面原理变成你引擎里的一个实打实功能。

一、把纠错接进查询流程

纠错不该是独立功能,而要融进现有查询链路。最自然的顺序是:

  1. 用户提交 query,先正常分词、查倒排。
  2. 如果命中结果足够(比如命中数 ≥ 阈值),直接返回,不纠错,避免画蛇添足。
  3. 如果结果太少或为零,对每个词跑一遍纠错,召回候选,用候选再去查,并把"你是不是想搜:XXX"提示出来。
【带纠错的查询链路】 用户输入 "无限耳机" │ 精确查询 ▼ 命中数 = 0 (词典里没有"无限") │ 触发纠错 ▼ 对每个词跑容错: "无限"→["无线"], "耳机"→["耳机"] │ 用候选再查 ▼ 命中 "无线耳机" 的文档, 并提示 did you mean: 无线耳机

这个"先精确后纠错"的顺序很关键:绝大多数查询是对的,别为了那百分之几的错词让所有人都付出纠错开销。只在确实查空时才兜底,体验和性能都照顾到。

二、did you mean:提示怎么来

"你是不是想搜"这条提示,本质是"选一个最像用户输入的候选词"展示。选哪个?用前面编辑距离当相似度,再叠加一点业务权重(比如词的文档频率越高越可能是本意)。

importjava.util.*;publicclassDidYouMean{privatefinalSet<String>dictionary;privatefinalMap<String,Integer>docFreq;// 词 -> 命中文档数publicDidYouMean(Set<String>dictionary,Map<String,Integer>docFreq){this.dictionary=dictionary;this.docFreq=docFreq;}/** 返回最可能的纠错建议, 没有则返回 null */publicStringsuggest(Stringwrong,intmaxEdit){if(dictionary.contains(wrong))returnnull;// 本来就对Stringbest=null;intbestScore=-1;for(Stringword:dictionary){if(Math.abs(word.length()-wrong.length())>maxEdit)continue;intdist=EditDistance.distance(wrong,word);if(dist>maxEdit)continue;// 分数: 距离越近越好, 文档频率越高越可能intscore=(maxEdit-dist)*1000+docFreq.getOrDefault(word,0);if(score>bestScore){bestScore=score;best=word;}}returnbest;}}

注意分数里(maxEdit - dist) * 1000把距离放在主导位:优先选改得最少的;距离相同时又文档频率定胜负,比如"无限"既能纠成"无线"也能纠成生僻词,明显选高频的"无线"。这就是 did you mean 从不胡乱推荐的底气。

三、常见纠错坑:同音字与形近字

真做纠错,你会发现编辑距离 alone 不够用,现实里有两类典型坑。

同音字(中文特有问题)。“无线"和"无限"拼音都是 wuxian,用户用拼音输入时极易混。纯字形编辑距离认为它们差 1,但用户本意其实是拼音错了。更好的做法是加一层"拼音容错”:把词转拼音,对拼音做编辑距离容错,能抓住 wuxian→wuxian 这类同音错误。

形近字。“己”“已”"巳"三个字长得像,手写的或者 OCR 识别的文档里常混。编辑距离把它们当不同字,但用户眼里差不多。这类可以维护一张"形近字映射表"做预处理归一。

【两类坑与对策】 同音: 无线(wuxian) vs 无限(wuxian) → 加拼音层容错 形近: 己 vs 已 vs 巳 → 形近字归一表 英文: recieve vs receive → 编辑距离即可抓

重点:编辑距离是地基,但别迷信它。中文纠错真正难的是语义和拼音层,单纯数"改了几个字"会漏掉同音错误。我们手写版先上编辑距离,知道它的边界就够了,真做中文搜索时再叠拼音层。

四、中英文纠错为什么是两码事

英文纠错相对舒服:字母表小(26 个)、词有空格边界、拼写错误大多是键盘相邻键打错(teh→the)。编辑距离 + 词典频率就够覆盖大部分。

中文完全另一个世界:

  • 没有词边界,先得分词,分错了后面全错。纠错和分词耦合。
  • 同音字爆炸,拼音输入法让"无线/无限/无线"同音,靠字形距离分不清。
  • 简繁、多音字("重"有 zhong/chong)增加复杂度。
【中英文纠错差异速览】 英文: 有空格边界 + 字母表小 + 键盘错为主 → 编辑距离好使 中文: 无边界 + 同音爆炸 + 分词耦合 → 需拼音层 + 分词协同

我们的玩具引擎目前按空格切词,更贴近英文场景。真做中文,得接专业分词 + 拼音容错,那是后面 Lucene 中文篇的戏。这一篇你只要建立"中文纠错比英文难一个量级"的直觉即可。

五、把所有件拼起来:一个会纠错的 Searcher

把纠错塞进上篇的MiniSearcher,逻辑只是加一层"查空兜底"。

publicclassCorrectingSearcher{privatefinalMiniSearchersearcher;privatefinalDidYouMeandym;privatefinalintmaxEdit=1;publicCorrectingSearcher(MiniSearchersearcher,DidYouMeandym){this.searcher=searcher;this.dym=dym;}publicSearchResultsearch(Stringquery)throwsIOException{List<MiniSearcher.SearchHit>hits=searcher.search(query);if(!hits.isEmpty()){returnnewSearchResult(hits,null);// 查到了, 不纠错}// 查空, 逐词纠错生成建议 queryStringBuilderfixed=newStringBuilder();for(Stringterm:newSimpleAnalyzer().analyze(query)){Stringsug=dym.suggest(term,maxEdit);fixed.append(sug!=null?sug:term).append(" ");}List<MiniSearcher.SearchHit>fixedHits=searcher.search(fixed.toString().trim());Stringhint=fixedHits.isEmpty()?null:fixed.toString().trim();returnnewSearchResult(fixedHits,hint);// hint 即 did you mean}publicstaticclassSearchResult{publicfinalList<MiniSearcher.SearchHit>hits;publicfinalStringdidYouMean;// 非空则提示publicSearchResult(List<MiniSearcher.SearchHit>h,Stringd){hits=h;didYouMean=d;}}}

跑一下:search("无限耳机")精确查为空 → 触发纠错 → 建议"无线耳机" → 用建议查到文档,并带上didYouMean="无线耳机"。前端看到这个字段,就能显示"你是不是想搜:无线耳机"了。

【一次纠错查询的输出】 输入: "无限耳机" 输出: hits=[doc=1,...], didYouMean="无线耳机" 前端: (空结果区) 你是不是想搜:无线耳机 [点一下用建议重查]

六、纠错这一关,你拿到了什么

到这,你的引擎有了"容错"这只眼睛:用户打错字不再直接吃空结果,而是被温柔地引回正确查询。底层是编辑距离 + 自动机思路,表层是"先精确后兜底"的查询顺序和 did you mean 提示。你也看清了同音字、形近字、中英文差异这些真实坑。

下一篇,我们要把建索引、落盘、读取、查询、纠错这些零件全部串成一条线,喂一份真实小语料跑通整个引擎,验证性能和正确性,给第二模块画上句号。

七、拼音容错:中文纠错的隐藏主线

前面说同音字是中文纠错的大坑,这里给个能落地的做法。维护一张"词 → 拼音"的映射(可用开源拼音库批量生成),纠错时同时计算"字形编辑距离"和"拼音编辑距离",取两者较小者当相似度。比如"无限"和"无线"拼音都是 wuxian,拼音距离 0,一眼就纠得出来;而"己"和"已"拼音不同(ji / yi),靠字形距离兜住。

【拼音层容错示意】 输入 "wuxian耳机"(拼音输入手误) 拼音距离: wuxian vs 无线(wuxian) = 0 → 强烈候选 字形距离: 无限 vs 无线 = 1 → 也候选 综合: 无线 胜出

这层不复杂,但能把中文纠错从"勉强能用"拉到"真能救场"。代价是多存一份拼音索引,空间换体验,做中文搜索基本都值得。

八、上线的考量:纠错也有延迟预算

纠错虽好,不能无节制。线上搜索的延迟预算通常就几十毫秒,纠错若每次都遍历词典算距离,很容易超预算。工程上常见三招压成本:

  • 只在"精确查询命中太少"时触发,绝大多数正常查询零开销。
  • 词典不大时直接遍历算距离(我们玩具版就是);词典大时换自动机求交。
  • 把热门纠错对(用户输入 → 正确词)缓存起来,重复错词直接命中缓存。

这和我们前面"先精确后兜底"的顺序是一脉相承的:把贵操作锁在最小触发面里。

九、纠错之外:联想词与 did you mean 的关系

did you mean 是"你打错了,我猜你想搜这个";联想词(搜索框下拉)是"你还没打完,我先猜你要搜啥"。两者底层都依赖"字符串相似 + 频率",技术上同源。我们这篇做了 did you mean,等后面讲搜索界面体验模块,会把它和联想词一起系统做一遍。现在你至少知道,它们都不是魔法,而是编辑距离加频率表的组合拳。

本篇小结

纠错要融进查询链路:先精确查,结果太少才走容错召回,避免无谓开销。did you mean 用编辑距离挑"改得最少、文档频率最高"的候选展示。现实坑是同音字(加拼音层)和形近字(归一表),中文纠错比英文难一个量级,因为无词边界且同音爆炸。我们把纠错接进 Searcher,跑通了"无限耳机→无线耳机"的兜底。下一篇,把全部零件串起来端到端跑通。

上一篇【第19篇】编辑距离有限状态机:拼写纠错的底层武器
下一篇【第21篇】迷你检索引擎完整跑通:Hello Search!


RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进