【ElasticSearch 基础】倒排索引、IK 分词、BM25 一次搞懂

4095 字
20 分钟
【ElasticSearch 基础】倒排索引、IK 分词、BM25 一次搞懂

读这篇,你可以带走什么#

#你会学到对应概念
1ES 是什么,和 MySQL 的本质区别正向索引 vs 倒排索引
2为什么有了 Milvus 向量检索还需要 ES语义检索 vs 词条检索
3IK 分词器是什么,两种模式怎么选中文分词
4BM25 是什么,为什么比 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 层级
TableIndex(索引)建一张表 = 建一个 index
RowDocument(文档)一行记录 = 一个 JSON document,每个有 _id
ColumnField(字段)textkeywordintegerdate 等类型
SchemaMapping定义字段类型,类似建表语句
SQLREST API / DSLGET /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 支持挂载自定义词典文件,把你的专有词汇加进去:

custom_dict.txt
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、插入文档
  • matchtermbool 查询跑几个真实的搜索请求
  • examples/es-test/ 的代码跑一次完整的混合检索
  • 看看 ES + Milvus 两路结果合并后,召回质量有多大提升

昇哥 · 2026年6月 90后 JS 全栈 × AI 学习途中,把踩过的坑写下来 专注羽毛球,爱音乐,正在研究易经 🎵🏸

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【ElasticSearch 基础】倒排索引、IK 分词、BM25 一次搞懂
https://blog.fridolph.top/posts/2026-06-06__es_1/
作者
Fridolph
发布于
2026-06-01
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录