1.1 一次搜索的幕后:倒排索引的直觉 本节摘要:倒排索引(inverted index)是 Elasticsearch 搜得快的根本原因——它把"从文档找词"的正排思路反转成"从词找文档"。本节用一次真实的搜索事故讲清这个结构的方向差异,手算一张迷你倒排表,并解释为什么分词质量决定了搜索质量。这是 doc-1001 旅程的第一站,也是后面所有章节的解释起点。 从一次搜索事故说起 去年冬天,一个工单系统的值班群炸了。运营反馈"搜索退款两个字要等十几秒",DBA 查了慢查询日志,罪魁是这条 SQL: 问题不在 SQL 写得差,而在数据结构本身。B+ 树索引擅长的是"按有序值快速定位"——前缀匹配能用上索引,夹在中间的百分号让它彻底失效,只能退化为逐行扫描。
本节摘要:倒排索引(inverted index)是 Elasticsearch 搜得快的根本原因——它把"从文档找词"的正排思路反转成"从词找文档"。本节用一次真实的搜索事故讲清这个结构的方向差异,手算一张迷你倒排表,并解释为什么分词质量决定了搜索质量。这是 doc-1001 旅程的第一站,也是后面所有章节的解释起点。
去年冬天,一个工单系统的值班群炸了。运营反馈"搜索退款两个字要等十几秒",DBA 查了慢查询日志,罪魁是这条 SQL:
-- 模糊匹配无法使用 B+ 树索引,只能全表扫描 SELECT id, title FROM ticket WHERE content LIKE '%退款%'; -- 80 万行数据,执行计划显示 rows=800000,耗时 11.4 秒
问题不在 SQL 写得差,而在数据结构本身。B+ 树索引擅长的是"按有序值快速定位"——前缀匹配能用上索引,夹在中间的百分号让它彻底失效,只能退化为逐行扫描。数据库的行存储是正排结构:先有文档,文档里包含若干词。拿着词去找文档,等于把图书馆里每本书逐页翻开检查。
真正的搜索引擎走了另一条路:提前把"哪个词出现在哪些文档里"整理成一张表。读者找"退款",管理员不翻书,直接抽出"退款"这张卡片,卡片背面写着所有出现过的书架号。这就是倒排索引——方向反过来,从词指向文档。
光说方向反转还是抽象,我们把三条工单亲手拆一遍。doc-1001 的内容是"用户申请退款,处理速度太慢",doc-1002 是"退款已到账",doc-1003 是"发票抬头开错了"。中文按最粗粒度切词后,得到下面这张表(真实引擎还会记录词频与位置,这里先省略):
{ "词典与倒排表(概念性简化)": { "用户": { "文档": ["doc-1001"] }, "申请": { "文档": ["doc-1001"] }, "退款": { "文档": ["doc-1001", "doc-1002"] }, "处理": { "文档": ["doc-1001"] }, "速度": { "文档": ["doc-1001"] }, "太慢": { "文档": ["doc-1001"] }, "到账": { "文档": ["doc-1002"] }, "发票": { "文档": ["doc-1003"] }, "抬头": { "文档": ["doc-1003"] } } }
搜"退款"时引擎做的事:查词典定位"退款",读出文档列表,完事。两次内存跳转,与库存了多少文档无关——这就是它和逐行扫描的本质差别。如果查询两个词,比如"退款 到账",就取两个列表做交集或并集,再按匹配程度排序。规模越大,差距越夸张。
| 维度 | 正排(行存储) | 倒排(词到文档) |
|---|---|---|
| 回答的问题 | 这条文档包含哪些词 | 这个词出现在哪些文档 |
| 擅长的操作 | 按主键取整行、事务、范围扫描 | 全文匹配、多词组合、相关性排序 |
| 模糊匹配代价 | 逐行扫描,随数据量线性变慢 | 查表,基本与数据量无关 |
| 典型代表 | 关系型数据库 | Lucene、Elasticsearch |
值得注意的细节是:倒排表里存的是切出来的词,不是原文。doc-1001 写的是"处理速度太慢",如果切词时切成"处理、速度、太慢",那么搜"太慢了"切出"太慢、了"仍能命中"太慢";可要是切词器压根不认识"太慢"这个词,把它切碎成单字,搜"速度慢"就永远命中不了。搜索质量的上限,在写入时就被分词器锁定了——这句话是第 3 章整章的伏笔。

引擎还会给每个词记录词频(出现次数)和位置(出现在第几个词位)。词频高的文档通常更相关——"退款"出现五次的工单,大概率比出现一次的更对题。第 5 章讲打分公式时,这两个数字会再次出场。
💡 关键直觉:把倒排索引想成图书馆的卡片目录。书是文档,卡片是词条,卡片背后的书架号是文档编号。管理员从不翻书找词,只翻卡片。
| 考核点 | 达标标准 |
|---|---|
| 方向反转 | 用一句话说清正排与倒排各自回答的问题 |
| 手算倒排表 | 给三条文档与切词结果,写出词典与倒排列表 |
| 查表过程 | 说出从查询词到命中文档列表的两跳:定位词典、读出列表 |
| 性能边界 | 解释为什么查表代价与文档总量基本无关 |
| 附带信息 | 说出词频与位置分别供打分与短语匹配使用 |
| 分词上限 | 举一个切词不当导致永远搜不到的具体例子 |
第 2 章的环境搭好后,词条向量接口能把一个字段的倒排登记直接打印出来。写入样例文档再执行:
GET tickets/_termvectors/doc-1001?fields=title
响应的 terms 一节列出每个词条的词频与位置——本节手算的那张表,就是引擎替你记录的版本。换一个切词粒度不同的分析器重写文档,再查一次,词条列表随之变化:"倒排表里只有切出来的词"从论断变成眼见为实,第 3 章的分词台也因此有了预习材料。
下一节把镜头拉远:这张倒排索引住在哪里、由谁保管——集群、节点、分片与副本的地图就在眼前。