【ElasticSearch 基础】倒排索引、IK 分词、BM25 一次搞懂
读这篇,你可以带走什么
| # | 你会学到 | 对应概念 |
|---|---|---|
| 1 | ES 是什么,和 MySQL 的本质区别 | 正向索引 vs 倒排索引 |
| 2 | 为什么有了 Milvus 向量检索还需要 ES | 语义检索 vs 词条检索 |
| 3 | IK 分词器是什么,两种模式怎么选 | 中文分词 |
| 4 | BM25 是什么,为什么比 TF-IDF 更公平 | 相关性打分 |
| 5 | 三者串起来的 RAG 完整通路 | 倒排 + 分词 + 排序 + 混合检索 |
写在前面
做 AI 应用之前,我以为 ES 是”后端才需要关心的东西”。
直到我自己搭 RAG 系统,才发现:ES 是 RAG 的基础设施之一,绕不开。
这篇从前端工程师的视角出发,把 ES 的核心概念讲清楚——不是为了让你背概念,而是为了让你真正理解:为什么 RAG 需要 ES,ES 在里面扮演什么角色,以及它和 Milvus 向量检索的边界在哪里。
一、ES 是什么
ES,全称 ElasticSearch,是一个基于 Lucene 构建的分布式全文搜索引擎。
它不是数据库,不是缓存,也不是消息队列。它只做一件事:在海量文本中,快速找到你想要的内容,并按相关性排序返回。
用前端能理解的话说:
MySQL 是 Excel 表格,按行存数据,按列查数据。 ES 是书末的索引页,按关键词查文档,毫秒级返回。
这个比喻不是随便说的,它指向了两者最本质的区别——索引结构不同。
二、正向索引 vs 倒排索引
正向索引:MySQL 的方式
假设你有一张笔记表:
| id | title | content ||----|-------------|-----------------------------|| 1 | 今天早上跑步 | 沿江慢跑,状态不错 || 2 | 今天晚上跑步 | 夜跑 5 公里,拉伸后恢复很快 || 3 | 今天早上骑车 | 绕西湖骑行 20 公里 |你要搜「跑步」,MySQL 的做法是:
SELECT * FROM notes WHERE content LIKE '%跑步%';它从第 1 行开始,逐行扫描 content 字段,看有没有”跑步”这两个字。
1000 行还好,100 万行就崩了。
这就是正向索引的本质:文档 → 关键词。存档的思路来查,越查越慢。
倒排索引:ES 的方式
ES 在写入文档时,会先把每篇文档的内容按词拆开,建一张反过来的表:
词条 文档 ID 列表─────────────────────────"今天" -> [1, 2, 3]"早上" -> [1, 3]"跑步" -> [1, 2] ← 搜"跑步",直接在这里拿结果"骑车" -> [3]"晚上" -> [2]这就是倒排索引:关键词 → 文档。搜什么词,直接翻表,不用遍历。
用更直观的比喻:
正向索引 = 图书馆的书架 → 找包含"跑步"的书,要从第 1 排翻到第 100 排,一本一本看
倒排索引 = 书店的索引卡片柜 → 拉开"跑步"的抽屉,卡片上写着 [书1, 书2, 书99],直接去拿O(1) 找到关键词的位置,再按相关性排序返回 Top K。 这就是 ES 能在海量文本下做到毫秒级检索的根本原因。
三、ES 和 MySQL 的完整对比
在开始用 ES 之前,先建立一个概念映射表,不然很容易被术语绕晕:
| MySQL 概念 | ES 概念 | 说明 |
|---|---|---|
| Database | —— | ES 没有 database 层级 |
| Table | Index(索引) | 建一张表 = 建一个 index |
| Row | Document(文档) | 一行记录 = 一个 JSON document,每个有 _id |
| Column | Field(字段) | 有 text、keyword、integer、date 等类型 |
| Schema | Mapping | 定义字段类型,类似建表语句 |
| SQL | REST API / DSL | GET /index/_search 代替 SELECT |
LIKE '%跑步%' | match 查询 | MySQL 全表扫描,ES 走倒排索引 |
其中有一个字段类型的区别值得单独说:
text:会被分词,用于全文搜索。比如文章标题、内容。keyword:不分词,用于精确匹配、过滤、聚合。比如用户 ID、状态枚举、标签。
这个区别在实际建 Mapping 时很重要,用错了要么搜不到,要么排序乱。
四、有了 Milvus 向量检索,为什么还需要 ES
这是很多前端同学在做 AI 应用时会产生的困惑:
“我已经用 Milvus 做语义检索了,为什么还要再搭一个 ES?”
答案是:它们解决的是两类完全不同的问题。
语义检索 vs 词条检索
| Milvus(向量 / 语义检索) | ES(倒排 / 词条检索) | |
|---|---|---|
| 匹配逻辑 | 意思相近 | 字面相同 |
| 底层原理 | 向量空间距离(余弦相似度) | 倒排索引 + BM25 打分 |
| 适合输入 | 自然语言问句 | 精确实体、术语、代码、编号 |
| 成功案例 | 搜「杭州旅游」命中「西湖攻略」 | 搜 errorCode=5001 只命中 5001 |
| 失败案例 | 搜 X-XXXXA 可能漂到 X-XXXXB | 搜「西湖游玩」未必命中「杭州旅游」 |
这两个失败案例说明了边界:
- 字面相近但实际不同(如产品型号、错误码、合同编号)→ 走 ES 词条检索
- 字面不同但语义相近(如自然语言问句、同义词、近义词)→ 走 Milvus 语义检索
向量检索是怎么做的
Milvus 的核心是把文本转成向量(一个高维数字数组),然后在向量空间里找”距离最近”的几个向量。
"杭州旅游" → [0.12, 0.87, 0.34, ...] ← 这两个向量在空间里距离很近"西湖攻略" → [0.13, 0.85, 0.36, ...] ← 所以语义检索能命中
"errorCode=5001" → [0.91, 0.02, 0.67, ...] ← 这两个向量距离很远"errorCode=5002" → [0.90, 0.02, 0.68, ...] ← 但字面上只差一个数字向量是由 Embedding 模型(如 text-embedding-ada-002)生成的,它能捕捉语义,但对精确字符不敏感。这就是为什么向量检索在处理精确实体时会”漂移”。
为什么 RAG 需要两者都有
实际生产中的 RAG 系统,基本都是混合检索:
用户输入 ├─ ES 词条检索 → 召回精确匹配的文档 └─ Milvus 语义检索 → 召回语义相近的文档 ↓ 结果合并去重 ↓ Rerank 精排 ↓ Top K 文档 → 拼 Prompt → LLM 回答只用向量检索:精确实体、代码、编号容易漂移,召回结果不可靠。 只用词条检索:自然语言问句、同义词、近义词容易漏掉,召回率低。 两者结合:互补覆盖,召回质量显著提升。
五、IK 分词器 — 它到底做了什么
ES 默认不认识中文词条。
如果你不安装 IK 分词器,“全文检索”会被拆成 全 / 文 / 检 / 索——四个单字。倒排索引表建出来是这样的:
"全" -> [1, 2, 3]"文" -> [1, 2, 3]"检" -> [1, 2, 3]"索" -> [1, 2, 3]用户搜”全文检索”,ES 会把它也拆成四个单字去查,召回一堆无关文档,精度极差。
IK 分词器专门解决中文分词问题。 它内置了一个词典,能识别中文词语的边界,把句子切成有意义的词条。
两种模式
IK 提供两种分词模式,适用于不同场景:
ik_max_word — 最细粒度,写入时用
输入:"Elasticsearch全文检索入门"
输出:Elasticsearch / 全文检索 / 全文 / 文检 / 检索 / 入门穷举所有可能的词语组合。“全文检索”会被拆成 全文检索、全文、检索 三个词条,全部建入倒排索引。
目的:追求高召回率。 用户不管搜”检索”还是”全文检索”,都能命中这篇文档。
ik_smart — 最粗粒度,搜索时用
输入:"Elasticsearch全文检索入门"
输出:Elasticsearch / 全文检索 / 入门按最合理的语义单元切割,不重叠,不多拆。
目的:追求高精确率。 搜索”全文检索”直接匹配词条,不会因为多个重叠组合而引入噪音。
为什么写入和搜索用不同的模式
这是一个很经典的设计思路:
- 写入时用 ik_max_word:把所有可能的词条都建进倒排索引,保证任何搜法都能命中(高召回)
- 搜索时用 ik_smart:把用户输入按语义单元切割,精准匹配词条,不引入噪音(高精确)
两者配合,才能在召回率和精确率之间取得平衡。
在 Kibana Dev Tools 里跑两行就能直观感受区别:
POST /_analyze{ "analyzer": "ik_max_word", "text": "Elasticsearch全文检索入门"}
POST /_analyze{ "analyzer": "ik_smart", "text": "Elasticsearch全文检索入门"}自定义词典
IK 内置词典是静态的,遇到新词、专有名词(比如产品名、人名、行业术语)可能切错。
ES 支持挂载自定义词典文件,把你的专有词汇加进去:
ElasticSearch向量检索大语言模型RAG这在做垂直领域的搜索系统时非常重要——通用词典认不出你的行业术语,召回结果会很差。
六、BM25 — 相关性是怎么打分的
倒排索引解决了”有没有”,但搜到 10 条文档,谁排第一,靠的是 BM25。
先说 TF-IDF 的问题
BM25 是 TF-IDF 的改进版,先理解 TF-IDF 的局限,才能理解 BM25 为什么更好。
TF-IDF 由两部分组成:
- TF(词频,Term Frequency):一个词在文档中出现的次数越多,相关性越高
- IDF(逆文档频率,Inverse Document Frequency):一个词在所有文档中越少见,权重越高
TF-IDF 的问题是线性思维:
一篇文档里出现 100 次”跑步” = 相关性是出现 1 次的 100 倍
这不合理。一篇文章堆砌了 100 次同一个词,不见得比认真写了 10 次的更相关——它更可能是在刷排名。
另一个问题是没有考虑文档长度:一篇 10 万字的书里出现 10 次”跑步”,和一篇 100 字的笔记里出现 10 次”跑步”,TF-IDF 给的分数一样。但显然后者更相关。
BM25 的三个核心策略
BM25(Best Match 25,第 25 次迭代的最佳匹配算法)用三个策略解决了上面的问题:
策略 1:词频饱和(TF Saturation)
BM25 对词频做了非线性处理——词频增加到一定程度后,分数增长趋于平缓,不再线性增长。
TF-IDF:出现 100 次 = 出现 1 次的 100 倍分数BM25: 出现 100 次 ≈ 出现 10 次的分数(边际递减)防止通过堆砌关键词刷排名。
策略 2:文档长度归一化(Length Normalization)
BM25 引入了文档长度参数,对长文档的词频做惩罚:
100 字笔记出现 5 次"跑步" vs 10 万字书出现 5 次"跑步"→ 笔记的分数更高(密度更大,更相关)让长文档和短文档在同一个公平基准上比较。
策略 3:稀有词权重更高(IDF 加权)
这一点和 TF-IDF 类似,但计算方式更精确:
"的" 出现在 99% 的文档里 → IDF 极低,几乎不贡献分数"IK分词器" 只出现在 0.1% 的文档里 → IDF 极高,大幅提升相关性区分信息含量,高频停用词不会污染排序结果。
一句话记住 BM25
BM25 = 词频减速 + 文档长度纠正 + 稀有词加权
不是”词出现多少就加多少分”,而是一套非线性的公平打分体系。
ES 默认使用 BM25,不需要手动配置任何参数。大多数场景静默生效。
七、串起来:RAG 的完整通路
现在把所有概念串起来,看 ES 在一个完整 RAG 系统里扮演的角色。
RAG 是什么
RAG(Retrieval-Augmented Generation,检索增强生成)的核心思路是:
不让 LLM 凭记忆回答,而是先从知识库里检索相关内容,再把检索结果拼进 Prompt,让 LLM 基于真实资料回答。
这解决了 LLM 的两个核心问题:
- 幻觉:LLM 不知道的事情会编造答案
- 时效性:LLM 的训练数据有截止日期,无法回答最新信息
完整的 RAG 通路
┌─────────────────────────────────┐ │ 知识库构建阶段 │ └─────────────────────────────────┘原始文档(PDF/MD/网页) ↓ 文本切片(Chunking) ┌───────────────────────────────────────────────┐ │ 将长文档切成适合检索的片段(通常 200-500 字) │ └───────────────────────────────────────────────┘ ↓ ↓ ES 写入 Milvus 写入 ik_max_word 分词 Embedding 模型生成向量 建倒排索引 建向量索引 ↓ ↓ 词条检索库 语义检索库
┌─────────────────────────────────┐ │ 在线检索阶段 │ └─────────────────────────────────┘用户输入:"ES 的 BM25 算法是什么" ↓ ┌───────────────────────────────────────────────┐ │ 并行发起两路检索 │ └───────────────────────────────────────────────┘ ↓ ↓ ES 词条检索 Milvus 语义检索 ik_smart 分词查询 Embedding 生成查询向量 BM25 打分排序 余弦相似度排序 召回 Top 20 召回 Top 20 ↓ ↓ ┌───────────────────────────────────────────────┐ │ 结果合并去重(RRF 或加权融合) │ └───────────────────────────────────────────────┘ ↓ Rerank 精排(Cross-Encoder 模型) ┌───────────────────────────────────────────────┐ │ 对合并后的候选文档重新打分,选出最相关的 Top 5 │ └───────────────────────────────────────────────┘ ↓ Top 5 文档片段 ↓ 拼入 Prompt ┌───────────────────────────────────────────────┐ │ System: 你是一个技术助手,基于以下资料回答问题 │ │ Context: [Top 5 文档片段] │ │ User: ES 的 BM25 算法是什么 │ └───────────────────────────────────────────────┘ ↓ LLM 生成回答每个环节的作用
| 环节 | 技术 | 作用 |
|---|---|---|
| 文本切片 | LangChain / 自定义 | 把长文档切成合适大小的片段,太长超 token,太短丢上下文 |
| ES 词条检索 | 倒排索引 + IK + BM25 | 精确匹配关键词、术语、编号,高精确率 |
| Milvus 语义检索 | Embedding + 向量索引 | 捕捉语义相似性,高召回率 |
| 结果合并 | RRF(倒数排名融合) | 把两路结果融合成一个统一排名 |
| Rerank 精排 | Cross-Encoder 模型 | 对候选文档重新精排,比双塔模型更准确 |
| Prompt 拼接 | 模板引擎 | 把检索结果注入 Prompt,让 LLM 有据可依 |
为什么 Rerank 是必要的
ES 和 Milvus 各自召回 20 条,合并后有 30-40 条候选文档(去重后)。
直接把 40 条都塞进 Prompt 会超 token 限制,而且 LLM 处理太长的上下文时注意力会分散,回答质量下降。
Rerank 模型(Cross-Encoder)会对每一对「查询 + 文档」重新打分,选出最相关的 Top 5。
它比 ES 的 BM25 和 Milvus 的余弦相似度都更准确,因为它能同时看到查询和文档的完整内容,而不是分别编码后再比较。
代价是速度慢——所以只用在最后的精排阶段,不用在初始召回阶段。
RAG 的真正价值
RAG 不只是”给 LLM 加了个搜索”,它解决的是 LLM 在生产环境中最核心的可靠性问题:
没有 RAG:LLM 凭记忆回答 → 幻觉、过时信息、无法溯源有了 RAG:LLM 基于检索到的真实文档回答 → 可溯源、可更新、可控对于企业级应用,RAG 还有一个关键价值:私有知识库。
LLM 的训练数据是公开的,但企业的内部文档、产品手册、合同、代码库是私有的。RAG 让 LLM 能够访问这些私有知识,而不需要重新训练模型(成本极高)。
这就是为什么 ES + Milvus + Rerank 这套混合检索架构,是目前生产级 RAG 系统的标准配置。
八、小结
| 概念 | 一句话 |
|---|---|
| 倒排索引 | 关键词 → 文档,O(1) 查找,ES 毫秒级检索的根本 |
| IK 分词 | 写入用 ik_max_word(高召回),搜索用 ik_smart(高精确) |
| BM25 | 词频减速 + 文档长度纠正 + 稀有词加权,ES 默认打分算法 |
| ES vs Milvus | 词条检索 vs 语义检索,不是替代关系,是互补关系 |
| RAG | 检索增强生成,让 LLM 基于真实文档回答,解决幻觉和时效性问题 |
ES 在 RAG 里的定位: 精确召回的那一路。它不做语义理解,但它能保证精确实体、术语、编号不会漂移。这是向量检索做不到的事。
下一篇
概念讲完了,下一篇直接上实践:
- 在 Kibana Dev Tools 里建索引、配置 Mapping、插入文档
- 用
match、term、bool查询跑几个真实的搜索请求 - 用
examples/es-test/的代码跑一次完整的混合检索 - 看看 ES + Milvus 两路结果合并后,召回质量有多大提升
昇哥 · 2026年6月 90后 JS 全栈 × AI 学习途中,把踩过的坑写下来 专注羽毛球,爱音乐,正在研究易经 🎵🏸
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!