假设你是新上任的图书馆管理员,书架上堆着几十万本书。某天一位研究员跑来问:“我想借所有提到'量子计算'的书,能帮我找出来吗?”如果按书架一本本翻,你得加班到天亮。聪明的做法是什么呢?——下班前你连夜做了一套“索引卡片柜”:把书里出现过的关键词抄成一张张卡片,按字母排序,每张卡片背后写着书单、出现次数和页码。从此再有人问“量子计算”,你抽出卡片,秒回书单。 倒排索引(Inverted Index)就是 Elasticsearch 的“卡片柜”。它不像关系型数据库那样“先找书、再翻内容”,而是提前把“每个词出现在哪些文档”登记成表。配合分段存储、FST 压缩、近实时刷新等机制,ES 才能在万亿级文档中毫秒级返回全文检索结果。这篇文章就用图书馆的故事,带你拆解这套索引的全部秘密。
对搜索工程师而言,倒排索引是 ES 性能的根基——理解了它,才能解释“为什么写入要等一秒”,为查询优化、集群调优和故障排障找到正确方向。
目录
可视化演示
展示ES倒排索引的Term→Document映射结构、Segment不可变段与Commit Point机制、以及倒排索引与B+树索引的结构对比,切换模式查看不同层面。
本节配有交互动效(进入视口后自动加载,约 3KB)。
倒排索引:词项到文档的映射 文档分词后建立Term→DocList映射 Posting List记录包含该词项的文档ID Term → Posting List → Doc IDs 倒排索引实现O(1)级别的全文检索。
索引概念:从翻书到翻卡片
你接手图书馆第一天,就收到一个刁钻需求:把所有提到“数据仓库”的书找出来。书架是按编号排的,编号和内容毫无关系,你只能拿起第一本,从第一页翻到最后一页,做满记号再放下——几十万本翻完,天都亮了。你意识到:书架适合“按编号找书”,却回答不了“按内容找书”。
于是你想了个聪明办法:与其每次翻书,不如提前把“书里有哪些词”登记起来。正着登记,以每本书为开头,记“《数据库大全》包含:数据库、索引、查询……”;另一种做法是倒过来,以每个词为开头,记“数据库:出现在《数据库大全》《存储漫谈》……”。第二种,就是倒排索引。
在 Elasticsearch 中,文档(documents)先交给分词器切成词条(terms),系统为每个词条维护一个倒排表。查询时只需在词条字典里定位关键词,再取出倒排表,就能瞬间知道命中哪些文档,而不用逐篇全文匹配。这就是“反向映射”的精髓:把“文档→词”的对应翻转成“词→文档”。
现在你再遇到“找数据仓库相关的书”这种需求,只要翻开那张写着“数据仓库”的卡片,背后就是完整书单。从“一本本翻书”到“一翻卡片就有”,这就是倒排索引带来的质变。
- Posting(t) 表示倒排表词条
t对应的倒排表,也就是“关键词 t 的书单”,列出所有讲过这个词的文档编号 - d 表示一篇文档文档的编号,对应图书馆里的一本书
- Terms(d) 表示文档词条集合文档 d 分词后得到的全部词条集合,相当于一本书的“全书词汇表”
- t ∈ Terms(d) 判定收录条件词条 t 出现在文档 d 中,这是书单收录这本书的唯一条件
倒排结构:一张卡片,三重信息
卡片柜搭好后,你发现光有“书单”还不够。读者常追问:“这几本书里,哪本讲'数据库'讲得最细?”如果卡片只写书名,你还是答不上来。于是你给每张卡片背面的每本书旁,记上两个数字:这个词在这本书里出现了几次、分别在第几章第几页。
这就是倒排结构的三要素:Term Dictionary 是卡片的“标签区”,所有关键词按字典序排列,方便快速定位;Posting List 是卡片背面的“书单区”,记录文档编号;Posting 里还带词频与位置。三者合起来,才构成一张完整的倒排卡片。
技术上,ES 的 Posting List 不止记录 docID,还记录 term frequency(词频,用于相关度打分)和 positions(词位置,用于短语查询与高亮)。Lucene 还进一步用跳表、位图等结构压缩倒排表,让“书单”更省空间、遍历更快。
现在卡片柜每一格都能回答三个层次的问题:这本书有没有提到(编号)、讲得多不多(词频)、讲到哪(位置)。读者再刁钻的“细节追问”,你翻翻卡片背面就有答案。
- d 记录文档编号书单上的第一列,指明词条出现在哪本书
- f 表示词频词条 t 在文档 d 中出现的次数,出现越多通常相关度越高
- pos 表示位置列表词条 t 在文档 d 中出现的具体词位置,短语查询与高亮都靠它
- 三元组构成完整档案每个三元组 (d, f, pos) 是卡片背面的一条记录,三者缺一不可
核心流程:分三步走完一次搜索
一个读者走到柜台:“我想找同时讲'搜索引擎'和'倒排索引'的书。”你抽出两张卡片:一张写着“搜索引擎”,一张写着“倒排索引”。把两张书单摆在一起,找出都出现的编号——这就是查询的“与”运算。
一次完整搜索有四步:分词(把 query 切成词条)、构建(写入时已经建好索引)、匹配(查词典→取倒排表→求交集/并集)、排序(按相关性打分返回 Top N)。对用户来说这只是眨眼之间,对系统来说是一场流水线作业。
布尔查询是基础。多个词条按 AND 处理时,就是对各倒排表做集合交集;OR 则是并集。实际生产环境还会用 BM25 打分排序:词频越高、文档越短,得分越高,这就是“结果排序”环节的职责。
有了卡片柜,任何复杂问题都被拆成“翻哪几张卡片、怎么合并书单”的小事。图书馆的服务台从此再也不怕刁钻提问——分词、查卡、合并、排序,四步流水线各司其职。
- q 表示查询用户输入经过分词后的词条集合,相当于读者报出的一组关键词
- Posting(t) 取出每张书单为查询中的每个词条取倒排表,就像抽出对应卡片
- ⋂ 表示集合交集多个书单取共同出现的文档,对应 AND 语义
- R(q) 得到查询结果集合所有词条都出现的文档才是最终结果
Segment机制:不可变的“新书册子”
图书馆每天都有新书到货。最初你把新书硬塞进旧册子重新编目,结果每加一本书,几十万张卡片就要重写,图书馆直接瘫痪。你痛定思痛:新书不再改旧册,而是装订成一本新册子,单独上架。
ES 的每个分片(shard)由多个 Segment 组成。Segment 一旦生成就不可修改(immutable),这带来三个好处:无需加锁,并发读安全;文件缓存友好,热数据命中率高;崩溃恢复简单,已落盘段不用重写。代价是多次写入会产生很多小段,查询时要跨段汇总结果。
后台的 merge 线程会定期把小段合并成大段,减少段数量,同时真正清理被标记删除的文档。合并是纯后台操作,但会占用 IO,所以 ES 提供了按段数量、按段大小等触发策略,让管理员能控制“多久合并一次、合并到什么规模”。
就像图书馆每天闭馆后把散落的小册子并成大册、顺手清掉破损书页,合并让书架永远保持“恰到好处的整洁”,查询效率也稳定在最佳状态。
- S1 到 Sn 表示多个分段查询时并列存在的若干 Segment,每个都有一份自己的倒排表
- ∪ 表示跨段汇总同一词条的书单分布在各段中,查询需要合并所有段的命中
- merged 表示合并后的大段后台把小段合并为一个新段,覆盖旧的多个段
- ⇒ 表示合并的方向段数从多到少,查询效率随之提升
提交与更新:登记簿与流水账
一次断电事故让管理员惊出一身冷汗:临时架上几十本新书“凭空消失”,因为它们只存在于记忆里,没有登记。为了让数据万无一失,你立了两本账:一本“入库登记簿”,记录哪些册子正式盖章入库(已落盘);一本“流水账”,记住每次“书到了”但还没来得及盖章的操作。
对应 ES:Commit Point 是登记簿,记录所有已落盘的 Segment 及文件信息;写入先进内存 buffer 和 translog(流水账),当触发 flush 时,buffer 生成新 Segment 并 fsync 落盘,同时更新 Commit Point。translog 本身也会定时 fsync 到磁盘,防止内存丢失。
崩溃恢复时,ES 先根据最近一次 Commit Point 找到已落盘的数据,再重放 translog 里尚未提交的写入,从而做到不丢数据。这正是“先写日志、再改数据”的 WAL(Write-Ahead Logging)思想的体现:流水账永远是第一道保险。
有了登记簿和流水账,哪怕半夜突然断电,第二天你也能把书一本不差“找回来”。对数据密集型系统来说,这种双账本机制就是持久化的定心丸。
- D_commit 表示已提交数据最近一次 Commit Point 之后已落盘的全部数据
- Translog 表示流水账记录尚未提交的写操作序列,可追加、可重放
- ⊕ 表示重放操作把流水账中的写操作按顺序重新执行一遍
- D_恢复 表示最终数据崩溃重启后恢复出的完整数据,写入一条不少
FST压缩:把卡片标签折叠起来
卡片柜越来越大,几十万张卡片堆满了三个房间。管理员想省钱省空间,仔细观察发现很多卡片标签重叠:“数据库”、“数据仓库”、“数据结构”都共享前缀“数据”。与其每张卡片都完整写一遍,不如做一个“树状折叠柜”:公共前缀只写一次,后面的分支各自展开。
ES 的词典(Term Dictionary)必须常驻内存才能快速定位词条,而海量词条会让内存爆炸。Lucene 用 FST(Finite State Transducer,有限状态转换器)来压缩词典:它把词条集合编码成一张有向无环图,共享前缀与后缀,既大幅节省内存,又能在图上做精确查找和前缀查询。
FST 的构建是贪心算法:按字典序插入词条,尽量复用已有路径,最终得到最小化的状态机。查询时从起点沿着字符边走,走完即找到词条及其对应的 value(如 docID 区间)。它是无损的,不会因为压缩丢掉任何一个词,这也是“快速查找”的底气所在。
换用折叠柜后,房间空出一大半,找卡片的速度反而更快——因为路径更短了。内存省了、查询快了,FST 是 ES 在万亿词条下依然轻盈的秘密武器。
- T 表示全部词条集合词典中需要存储的所有词条
- |t| 表示词条字节数单个词条在压缩前占用的存储大小
- |FST(T)| 表示压缩后大小整棵 FST 状态图的总存储量
- Saved 表示节省空间压缩前后的差值,也是内存压力的缓解量
近实时性:先上架,后编目
新书上架流程很长:编目、盖章、入库、登记,一套走完往往要等很久。但读者等不及。于是你开了一个“新书快借区”:新书到货,先在内存里整理成临时小册子,每隔一秒统一摆上快借区,读者立刻能借到;至于盖章入库、登记账本,那是后台慢慢做的事。
ES 的写入默认先进内存 buffer,每 refresh(默认 1 秒)把 buffer 生成一个新 Segment,并让这段数据对搜索可见。所以“写入到可搜”的时延约为一个 refresh 周期,这就是近实时(Near Real-Time)的含义——不是实时,但足够快。
如果业务要求“写入后立刻可见”,可以把 refresh_interval 调小甚至手动 refresh,但这会频繁生成小 Segment,拖累写入吞吐。反之,批量导入时把 refresh 调大(如 30 秒),写入性能显著提升。真正的持久化仍要靠 translog fsync 与 flush 落盘完成。
“先上架,后编目”的策略让图书馆在“及时可用”与“严谨归档”之间找到了平衡——你还可以根据客流(写入压力)灵活调整快借区的刷新频率,这正是 ES 近实时设计的精髓。
- t_可见 表示可见延迟从写入到可被搜索的等待时间
- refresh_interval 表示刷新间隔定期把内存 buffer 变成可搜索 Segment 的周期,默认 1 秒
- ≈ 表示近似相等实际延迟还受 buffer 大小、线程调度等因素影响
- 权衡点间隔越小越实时,但小 Segment 越多;间隔越大写入越快,但可见越慢
B+树对比:书架与卡片柜各有所长
你发现隔壁老图书馆用的是另一种体系:每本书有固定编号,按编号顺序排满书架,想找编号 1000 到 2000 之间的书,沿着书架走一趟就有;但想找“提到某词的书”,依然得一本本翻。而你的卡片柜呢?找关键词秒回,却回答不了“编号 1000 到 2000 之间有哪些书”这种范围问题。
关系型数据库(如 MySQL InnoDB)用 B+树做主索引:叶子节点有序存放行数据,擅长范围查询、排序、精确匹配;ES 的倒排索引则把“词→文档”映射前置,擅长全文检索,“哪个词出现在哪些文档”是它的主场。两者解决的其实是不同维度的问题,这就是“原理差异”的本质。
B+树查询复杂度是 O(log n + k),n 是记录数、k 是结果数;倒排索引查询要先在 FST 词典中定位词条,再取倒排表,复杂度与命中结果规模相关,与文档总数基本无关。所以文档越多,倒排索引的“免扫描”优势越明显。
书架与卡片柜并不冲突:成熟系统往往两者并用——MySQL 用 B+树管行、ES 用倒排管词,再由业务层协调。理解了各自的边界,你才能在设计检索系统时让它们各司其职。
- log n 表示查找层数B+树从根到叶的层数,与记录总数 n 的对数成正比
- k 表示结果记录数命中结果的数量,输出阶段线性扫描不可避免
- L 表示词典查找开销倒排查询先查词典的成本,FST 下近似于词条长度
- R 表示倒排表规模命中的倒排表大小,决定倒排查询的快慢
AI 应用场景
电商商品搜索
阶段:数据索引与检索
解决的问题:用户在电商平台输入“白色 连衣裙 夏季”,传统数据库用 LIKE '%连衣裙%' 做全表扫描,数据量过千万时耗时长且无法评分排序。AI 搜索系统先用分词器切出词条,再走倒排索引查词典、取倒排表、按 BM25 打分,毫秒级返回并排序。
具体案例:某服饰电商每秒处理 3000 次搜索请求,商品文档每天更新 200 万条。用 ES 倒排索引后,搜索平均响应从 1.8 秒降到 45 毫秒,同时利用 Segment 合并策略保障写入吞吐,大促期间稳定扛住流量高峰。
日志检索与可观测性平台
阶段:海量日志的实时索引与检索
解决的问题:传统方式按行存日志文件,排查“某个服务在 10:00 到 10:05 之间报了多少次 OOM”需要 grep 全部日志,效率极低。ELK 技术栈用 Logstash 采集日志,ES 为每条日志建立倒排索引,字段如 level、message 均可快速过滤。
具体案例:某金融公司每天产生 50 TB 日志,使用 ES 按天建索引、配合近实时 refresh 让故障日志秒级可查。SRE 通过关键字 + 时间范围组合查询,几分钟内定位线上故障根因。
企业知识库与合同全文检索
阶段:非结构化文档的语义检索前置
解决的问题:法律合同、规章制度等 PDF 文档散落在共享盘里,员工检索全靠文件名。ES 把文档解析成文本并构建倒排索引,配合 FST 压缩让百万级词条的词典常驻内存,实现“全文任意词秒级定位”。
具体案例:某大型企业将 80 万份合同导入 ES,法务人员输入“违约责任 提前终止”,系统分词后走倒排索引联合查询,不仅返回相关合同编号,还能利用词位置高亮命中片段,找合同从半天缩短到几秒钟。
Python 演示
# -*- coding: utf-8 -*-# 迷你倒排索引: 用 Python 字典实现 "词条 -> 文档" 的翻转映射import refrom collections import defaultdict class MiniInvertedIndex(object): def __init__(self): self.doc_texts = {} # doc_id -> 原文 self.term_docs = defaultdict(set) # term -> 命中的 doc_id 集合 self.term_total = defaultdict(int) # term -> 全局出现次数 def add_doc(self, doc_id, text): self.doc_texts[doc_id] = text tokens = re.findall(r"\w+", text.lower()) local_count = defaultdict(int) for token in tokens: local_count[token] += 1 for token, freq in local_count.items(): self.term_docs[token].add(doc_id) self.term_total[token] += freq def query_and(self, query_text): tokens = re.findall(r"\w+", query_text.lower()) hit = None for token in tokens: cur = self.term_docs.get(token, set()) hit = cur if hit is None else hit & cur return sorted(hit) if hit else [] def top_terms(self, limit=5): items = sorted(self.term_total.items(), key=lambda kv: kv[1], reverse=True) return [(t, c) for t, c in items[:limit]] if __name__ == "__main__": idx = MiniInvertedIndex() docs = { 1: "Elasticsearch is a distributed search engine", 2: "A search engine builds an inverted index", 3: "The inverted index powers full text search", } for did, content in docs.items(): idx.add_doc(did, content) print("query 'search engine':", idx.query_and("search engine")) print("query 'inverted index':", idx.query_and("inverted index")) print("query 'database':", idx.query_and("database")) print("top terms:", idx.top_terms())
self.term_docs[token].add(doc_id)核心一步:把词条 token 映射到文档 doc_id,这就是“倒排”关系的建立。re.findall(r"\w+", text.lower())简易分词:按单词切分并统一小写,模拟 ES 的 analyzer 分词器。local_count[token] += 1统计词频,对应正文里“卡片背面的次数记录”。hit = cur if hit is None else hit & cur多个词条做集合交集,实现布尔 AND 查询。idx.query_and("inverted index")复现“翻两张卡片、合并书单”的完整查询过程。
常见误区
误区我以为倒排索引就是把文档内容反转过来存放,就像把句子从后往前写一遍,这样就能加快搜索。
正解倒排索引翻转的是“映射方向”,不是文档内容。文档原文始终原样存储;变的是索引结构从“文档→包含哪些词”翻转为“词→出现在哪些文档”。就像图书馆不会把书倒着放,只是新增了一套按关键词组织的卡片柜。
为何常见:名字里的“倒”字极具迷惑性,初学者容易望文生义,把“方向倒转”误解成“内容倒置”。
误区我以为像 MySQL 提交事务一样,ES 写入成功后马上就能被搜索到,毕竟接口都返回成功了。
正解ES 是近实时(NRT)系统,写入先进内存 buffer,要等下一个 refresh 周期(默认约 1 秒)生成新 Segment 后才对搜索可见。接口返回成功只代表写入已接收,不代表已可见。需要即时可见可调小 refresh_interval 或调用 refresh API,但要付出写入性能代价。
为何常见:大多数人对数据库的“提交即见”习以为常,没有意识到内存 buffer 与可搜索索引之间还隔着一层“刷新”。
误区执行 delete 后,磁盘占用应该立刻下降,就像删文件一样干脆。可我用 df 一看,空间一点没少。
正解ES 的删除是“标记删除”:先在文档上打删除标记,查询时过滤掉,真正的物理清除要等后台 Segment 合并时才会发生,磁盘空间也随之释放。所以删除后空间不降是正常现象。
为何常见:delete API 立即返回成功,误导使用者以为物理删除已完成,忽略了 Segment 不可变这个前提。
误区我以为 Segment 越多,数据划分越细,并行查询越快,所以写入频繁时小段多也没关系。
正解恰恰相反,查询要扫描所有 Segment 再跨段汇总,段数越多汇总开销越大,查询会变慢。后台 merge 线程把小段合并成大段正是为了减少段数。合并虽然耗 IO,但从整体上让查询更稳定高效。
为何常见:看不到“段”这个中间层,只看到写入量增长,容易把并发写与查询性能画上错误等号。
误区一听“压缩”我就联想到图片压缩会损失画质,担心 FST 压缩后某些词条就搜不到了。
正解FST 是无损压缩,它只是把词条集合编码成共享前缀/后缀的状态图,所有词条都能精确检索,还天然支持前缀查询。它压缩的是“存储冗余”,不是“词条内容”。
为何常见:日常接触的 JPEG、MP3 等有损压缩先入为主,让人对一切“压缩”都产生信息丢失的担忧。
面试题
Q1. 倒排索引的核心作用是把下面哪种关系翻转?
- A.文档到词条的映射关系← 正确答案
- B.词条到文档的映射关系
- C.数据库到表的映射关系
- D.表到字段的映射关系
答案:A。倒排索引的精髓是把“文档→包含哪些词”翻转成“词→出现在哪些文档”。选 A。B 是翻转后的结果方向而非翻转动作本身;C、D 属于数据库编目范畴,与倒排索引的“词文档映射”无关。
Q2. 词条“数据库”的倒排表中不会记录下列哪项信息?
- A.包含该词的文档编号
- B.该词在文档中的词频
- C.该词在文档中的位置
- D.该词的字面长度← 正确答案
答案:D。倒排表(Posting List)记录文档编号、词频、位置,分别用于定位、打分和高亮。词条的字面长度属于词典管理的元信息,不属于倒排表内容,故选 D。
Q3. ES 中 Segment 具有不可变性,这一设计带来的最大好处是?
- A.可以支持原地更新词条
- B.无需加锁并发安全读← 正确答案
- C.减少磁盘空间占用
- D.加快数据写入速度
答案:B。Segment 不可变意味着没有并发写冲突,读取无需加锁、缓存稳定,这是 ES 高并发读的基石。A 与不可变性相悖;C、D 是不可变性之外的间接收益,并非最大好处。
Q4. 关于 ES 删除文档的说法,正确的是?
- A.删除后磁盘空间立即释放
- B.删除只打标记合并时清除← 正确答案
- C.删除会重写整个倒排索引
- D.删除后数据立刻不可恢复
答案:B。ES 的 delete 是延迟删除——先加删除标记,查询时过滤,段合并时才物理清除并释放空间。A 是常见误解;C 代价过大不可行;D 错误,合并前数据仍保留。
Q5. FST 压缩能大幅节省内存,主要利用了词条的哪些特点?
- A.存在公共前缀可以共享← 正确答案
- B.存在公共后缀可以合并← 正确答案
- C.所有词条长度完全相同
- D.词条出现频率完全相同
答案:AB。FST 把词典编码成有向无环图,公共前缀共享路径、公共后缀合并状态,从而实现无损压缩。C、D 不是词条的客观规律,也不构成压缩依据。
Q6. 默认配置下,ES 写入一条新文档后大约多久可被搜索到?
- A.毫秒级实时可见
- B.大约 1 秒后刷新可见← 正确答案
- C.必须等 flush 落盘
- D.必须等段合并完成
答案:B。默认 refresh_interval 为 1 秒,写入先进内存 buffer,refresh 时生成新段并对搜索可见。A 太快不可能;C 是 flush 的职责;D 合并与可见性无关。
Q7. 关于 translog 的作用,下列说法最准确的是?
- A.记录已提交段的完整内容
- B.在崩溃后重放未提交写入← 正确答案
- C.压缩内存中的倒排索引
- D.加速桶聚合查询的速度
答案:B。translog 是写前日志(WAL),记录尚未提交的写入,崩溃后重放它补齐数据。A 是 Segment 的职责;C、D 与 translog 毫无关系。
Q8. 与 B+树相比,倒排索引在处理哪类查询时更有优势?
- A.主键值等于某值的点查
- B.对某字段做范围查询排序
- C.关键词全文检索与统计← 正确答案
- D.多表连接与事务回滚
答案:C。倒排索引的“词→文档”映射天然适合全文检索与词频统计,文档总量越大优势越明显。A、B 是 B+树的强项;D 属于数据库事务层,与索引类型关系不大。
Q9. 下列哪些属于倒排索引的组成部分?
- A.词条字典 Term Dictionary← 正确答案
- B.倒排表 Posting List← 正确答案
- C.事务日志 Redo Log
- D.主键索引 B+树
答案:AB。倒排索引两大核心组件是词典(Term Dictionary)和倒排表(Posting List)。C 是数据库日志,D 是关系库索引,均不属于 ES 倒排索引的构成。
Q10. 下列关于 Segment 合并的说法,正确的有哪些?
- A.合并由后台线程自动执行← 正确答案
- B.合并后仍必须保留所有小段
- C.合并时真正清除被删文档← 正确答案
- D.合并一定会拖慢查询速度
答案:AC。合并由后台线程自动进行,合并时真正清除删除标记并释放磁盘,选 A、C。B 相反——合并的目的就是减少段数,无需保留小段;D 错误——合并完成后查询更快,虽然合并过程会消耗 IO。