Elasticsearch 全文检索的核心并没有想象中复杂。文本先经过分词建立倒排索引,查询时再通过关键词快速找到文档,最后使用 BM25 计算相关性并排序。中文场景下,再加上 IK 分词器。把这三件事串起来,ES 的检索主线就通了。
之前做 RAG 的时候,我们用 Milvus 做过向量检索。
向量检索有一个很明显的特点:
它更关心"意思像不像"。
比如用户问:
怎么让 AI 记住以前聊过的内容?
知识库里可能根本没有这句话,而是:
可以使用向量数据库存储大模型的长期记忆。
两个句子的字面内容差别很大,但语义比较接近,所以向量检索仍然可能把它找出来。
但有些东西并不适合只靠语义。
比如:
Elasticsearch
IK 分词器
订单号
错误码
产品型号
这时候我们真正关心的往往就是:
这个关键词到底有没有出现。
这就是 Elasticsearch 擅长的场景:全文检索。
Elasticsearch 为什么搜文本这么快?
先看一个最简单的例子。
现在有三篇文档:
doc1:正在学习 Elasticsearch
doc2:Elasticsearch 可以做全文检索
doc3:Milvus 可以做向量检索
现在搜索:
Elasticsearch
最直接的办法当然是:
检查 doc1 有没有 Elasticsearch
检查 doc2 有没有 Elasticsearch
检查 doc3 有没有 Elasticsearch
三篇文章完全没问题。
但如果不是三篇,而是:
yaml
1000 万篇文章
每次查询都把所有文本重新扫描一遍,就很难接受了。
Elasticsearch 的思路不是"查询的时候再找",而是:
提前把以后搜索需要的信息整理好。
这个东西就是倒排索引。
什么是倒排索引?
还是刚才三篇文档。
先把里面的文本拆成一个个词。
比如:
doc1:
学习
Elasticsearch
doc2:
Elasticsearch
全文检索
doc3:
Milvus
向量检索
然后建立这样的关系:
Elasticsearch → doc1、doc2
全文检索 → doc2
Milvus → doc3
向量检索 → doc3
这就是倒排索引。
它名字听起来有点唬人,其实理解起来非常简单。
正常情况下,我们习惯从文档出发:
文档 → 里面有哪些词
倒排索引把这个关系反过来了:
词 → 出现在哪些文档
所以叫"倒排"。
现在再搜索:
Elasticsearch
ES 不需要重新扫描所有文章。
直接查:
Elasticsearch → doc1、doc2
就知道这个词出现在哪些文档里。
如果用 JavaScript 粗略理解,大概就是:
ini
const invertedIndex = {
Elasticsearch: ["doc1", "doc2"],
Milvus: ["doc3"],
全文检索: ["doc2"],
向量检索: ["doc3"],
};
console.log(invertedIndex["Elasticsearch"]);
输出:
css
["doc1", "doc2"]
当然,真正的 Elasticsearch 底层远比这个复杂。
但理解 ES 时,先记住这一层就够了:
倒排索引本质上维护的是"词条 → 文档"的关系。
小结
所以 Elasticsearch 适合全文检索,核心原因并不是"它搜索算法更快",而是它提前为文本建立了倒排索引。
数据写入时,文本会被拆成一个个词条,并建立:
词条 → 文档
这样的映射关系。
查询时就不需要再扫描所有原始文档,而是直接通过词条去倒排索引中找到对应文档。
可以把这一节压缩成一句话:
倒排索引解决的是:如何根据一个关键词,快速找到包含它的文档。
理解到这里,马上就会出现下一个问题:
既然倒排索引是围绕"词条"建立的,那一句中文到底应该怎么切成词?
中文到底怎么分词?
倒排索引依赖一个很重要的前提:
先把文本拆成词。
比如:
Elasticsearch RAG 混合检索知识库
英文还比较好办。
中文就麻烦了。
如果直接变成:
混
合
检
索
知
识
库
虽然也能建立索引,但这些单字的意义很弱。
我们真正想要的可能是:
混合
检索
知识库
知识
所以对于 Elasticsearch 来说:
怎么分词,会直接影响后面建立出来的倒排索引。
这也是为什么中文场景经常会使用 IK 分词器。
IK 分词器是干什么的?
先把一句话记住:
IK 不负责搜索,它负责告诉 Elasticsearch 中文应该怎么切。
常见的两个模式是:
ik_max_word
ik_smart
它们一般承担不同的任务。
ik_max_word:建索引的时候尽量多切
比如:
知识库
ik_max_word 会倾向于尽可能拆出更多有意义的词。
可以粗略理解成:
知识库
知识
......
为什么数据入库的时候要切得这么细?
因为现在我们还不知道用户以后会搜什么。
所以建立索引时,希望:
能拆的尽量拆
↓
倒排索引里的词条更多
↓
用户未来搜索时更容易命中
也就是尽量提高:
召回率。
所谓召回率,可以先简单理解成:
本来应该被搜出来的内容,我到底搜到了多少。
所以:
ik_max_word
→ 建索引的时候尽量多切
→ 提高未来被搜索到的机会
ik_smart:查询的时候别切得太碎
用户真正搜索的时候,情况又不一样。
假设用户输入了一句话。
如果继续疯狂拆成大量小词,那么确实可能搜出来很多文档。
问题是:
搜出来很多
不等于:
搜出来的都相关
拆得太碎,很容易产生很多无意义的匹配。
所以查询时一般使用:
ik_smart
让查询词尽量保持比较合理的语义单位。
于是可以这样记:
ik_max_word
→ 建索引
→ 尽可能多切
→ 提高"能不能搜到"
ik_smart
→ 查询
→ 尽量合理地切
→ 提高"搜到的是不是更相关"
映射到 Elasticsearch 的配置里就是:
json
{
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
其中:
analyzer
→ 数据写进去的时候怎么分词
search_analyzer
→ 用户查询的时候怎么分词
这个区别还是很值得记住的。
文档已经找到了,谁排第一?
到这里,搜索流程已经能跑起来了。
比如用户搜索:
Elasticsearch 检索
通过倒排索引找到了:
doc1
doc2
doc3
doc4
......
但现在又出现一个新问题:
这么多文档,到底谁应该排在前面?
总不能随机返回。
这就是 BM25 负责的事情。
可以先记:
倒排索引
→ 负责找到候选文档
BM25
→ 负责判断谁更相关
BM25 本质上就是一个:
相关性打分算法。
它里面的数学公式可以以后再看。
刚开始理解它,只需要抓住三个思想。
第一:词出现得多,不代表可以无限加分
假设用户搜索:
Elasticsearch
文章 A:
Elasticsearch 是一个全文检索引擎。
文章 B:
Elasticsearch Elasticsearch Elasticsearch
Elasticsearch Elasticsearch Elasticsearch......
如果简单按照:
出现一次 +1 分
那就意味着,只要不停重复关键词,就能把自己的排名刷上去。
显然不合理。
所以 BM25 有:
词频饱和。
关键词多出现几次,确实能说明文章可能更加相关。
但是出现次数越来越多以后,额外带来的收益会越来越小。
大概可以理解成:
出现 1 次 → 很有价值
出现 2 次 → 继续加分
出现 5 次 → 还会增加
出现 100 次 → 不会跟着暴涨
所以:
关键词出现次数越多,相关性通常会上升,但不会无限线性上涨。
这就避免了简单堆砌关键词。
第二:长文章不能天然占便宜
再看两个文档。
css
文档 A:
100 个字
Elasticsearch 出现 3 次
css
文档 B:
10000 个字
Elasticsearch 出现 3 次
都是出现三次。
但直觉上显然不完全一样。
A 一共才 100 个字,出现了三次 Elasticsearch。
说明它很可能就在集中讨论 Elasticsearch。
而 B 有一万字,出现三次可能只是顺带提了一嘴。
所以 BM25 还会考虑:
文档长度。
也就是做一定的长度归一化。
目的很简单:
不让长文章因为内容多、更容易碰巧出现关键词,就天然获得更高排名。
所以相同词频下,关键词在短文档中更加集中,通常会体现出更高的相关性。
第三:稀有词比常见词更有价值
比如:
的
是
可以
这种词几乎到处都有。
如果一个文档命中了"的",其实并不能说明什么。
因为几乎所有文章都有。
但是:
IK 分词器
这种词就不一样。
它比较少见。
一旦一篇文章里出现了它,就很可能和用户当前搜索的话题有关。
所以 BM25 还会考虑:
一个词到底有多稀有。
可以这样理解:
常见词
→ 到处都有
→ 区分能力很弱
→ 权重低
稀有词
→ 很少出现
→ 区分能力强
→ 权重高
所以:
BM25 会给更有区分度的稀有词更高的权重。
把整条 Elasticsearch 检索流程串起来
到这里,ES 的核心流程其实已经很清楚了。
先看数据写入阶段:
文章
↓
IK 分词
↓
ik_max_word
↓
拆成多个词条
↓
建立倒排索引
词条 → 文档
然后是查询阶段:
用户输入
↓
ik_smart 分词
↓
得到查询词
↓
查询倒排索引
↓
找到候选文档
↓
BM25 计算相关性
↓
按照分数排序
↓
返回结果
所以 Elasticsearch 全文检索最核心的三个东西,其实可以压缩成三句话:
IK 分词器
→ 解决"词怎么切"
倒排索引
→ 解决"文档怎么快速找到"
BM25
→ 解决"找到以后谁排前面"
这三件事一旦串起来,Elasticsearch 的核心基本就通了。
再回头看 Milvus 和 Elasticsearch
现在再看之前学过的 Milvus,就会发现它和 Elasticsearch 其实不是一回事。
Milvus 更偏向:
用户问题
↓
Embedding
↓
向量
↓
计算向量相似度
↓
找到语义相近的内容
关注的是:
意思像不像。
而 Elasticsearch 更偏向:
用户问题
↓
分词
↓
关键词
↓
倒排索引
↓
BM25 排序
关注的是:
关键词有没有准确命中。
所以两者并不冲突。
在 RAG 里完全可以同时使用:
markdown
用户问题
↓
┌───────┴───────┐
↓ ↓
Elasticsearch Milvus
关键词检索 语义检索
↓ ↓
└───────┬───────┘
↓
合并结果
↓
重排
↓
LLM
一边解决关键词、专业术语、精确实体的问题。
另一边解决自然语言表达不同但语义相近的问题。
这也就是后面做混合检索的基础。
最后
第一次接触 Elasticsearch,会看到很多东西:
Index
Document
Mapping
Analyzer
Query DSL
IK
BM25
......
很容易学着学着就散掉。
其实全文检索这一块,先抓住一条主线就够了:
文本先经过 IK 分词器切成词,通过倒排索引快速找到包含这些词的文档,再使用 BM25 计算相关性并排序。
或者再短一点:
IK 负责切词,倒排索引负责找文档,BM25 负责排顺序。
这句话记住以后,再回头看 Elasticsearch 的各种 API,很多东西就都有位置了。