从玩具到引擎
上一篇那张「词 → 商品」的手工表只解决了思想问题。真让它在十亿文档、百万词条下工作,还差三块核心零件:分词器、词典、倒排表。这一篇把每块零件拆开看。
零件一:分词器(Analyzer)
倒排的前提是分词——把「燕麦拿铁(大杯)」切成词条。ES 的分析器是三段式流水线:
原文「Oat Latte 大杯 #新品」
-> Character Filters:清洗字符(如去掉 HTML 标签)
-> Tokenizer:切段(按空格/规则切出 Oat、Latte、大杯、#新品)
-> Token Filters:加工(转小写、去停用词、同义词扩展)
最终词条:oat、latte、大杯、新品分词质量直接决定搜索质量:切得太粗,搜「拿铁」搜不到「生椰拿铁」;切得太碎,语义就散了。中文分词是重灾区,第 4 篇专门讲。
零件二:词典(Term Dictionary)
所有词条组织成一个有序词典。查词快不快,取决于词典用什么结构。Lucene 的答案是 FST(Finite State Transducer,有限状态转换器),理解它只需要抓住两个词:
- 前缀共享:latte、latteart、lattenut 三个词共用前缀 latte,重复的字只存一份——百万词条压缩后能整个塞进内存;
- 边查边走:查 lattern 不用从头比对,沿着 l-a-t-t-e-r-n 的路径走,走到头没有就是没有,还能顺路给出所有前缀为 lattern* 的词(前缀搜索白送的)。
MySQL 系列讲过的 B+ 树是「磁盘友好」的结构,FST 则是「内存友好」的结构——成本模型不同,选择就不同。
零件三:倒排表(Posting List)
词典里每个词条后面挂着文档号列表,还附带频率与位置信息:
拿铁 -> [doc1(出现3次,位置0,5,9), doc2(出现1次,位置2), doc4...]三样信息各有用途:docId 告诉你命中谁,频率(TF)参与算分,位置(Position)支撑短语查询(match_phrase 靠它判断「燕麦」和「拿铁」是否相邻)。存盘时倒排表还要做压缩:相邻 docId 的差值很小(增量编码),Frame of Reference 按块压缩,过滤缓存再用 Roaring Bitmap 按位运算加速。
多词条查询时,两个倒排表要合并:AND 取交集,OR 取并集。交集不逐个比对——docId 有序,配上跳表(Skip List),一方的游标可以大步跳着追赶另一方,交集合并不再是遍历。
评分的直觉:BM25 从哪来
倒排解决了「找到谁」,还要解决「谁排前面」。BM25 评分的三个直觉:
- 词频(TF):这个词在这篇文档里出现越多,文档越相关——但有饱和效应,出现 10 次不代表比 5 次相关一倍;
- 逆文档频率(IDF):全库只有两篇文档含「燕麦」,命中它的价值远高于人人都有的「咖啡」——稀缺的词更能代表意图;
- 文档长度归一:同样的词频,短文档比长文档更相关——两百字提三次,比两万字提三次分量重。
公式不用背,直觉记牢,第 8 篇还会回来调参。
倒排的另一面:doc values
倒排索引擅长「词找文档」,不擅长「文档找字段值」——排序、聚合、脚本取值要反复问「这一篇的 price 是多少」,倒排表干不了这个活。于是 ES 给每个字段配了doc values:建索引时就生成的一列式正排数据,按文档号顺序排列,天然适合扫描聚合。一句话分工:倒排管搜,doc values 管排和聚。
小结
一台搜索引擎的四大件:分词器切词,FST 词典查词,压缩倒排表管命中,doc values 管排序聚合。词频与稀缺度撑起评分直觉。零件都认识了,下一篇回到使用者的视角:Index、Document、Shard、Replica——ES 的世界观,以及它和 MySQL 概念的对应表。
评论 (0)