前面学了基于向量数据库 Milvus 实现 RAG 语义检索,并且基于 LangGraph 实现了闭环的 Agentic RAG——也就是 Agent 自主决策要不要检索、用什么检索、信息够不够、效果怎么样、要不要重新搜。具体的 Agentic RAG 要根据业务场景设计,理解这个闭环的思路就行。
但向量检索有个问题:
解决方案是:
这里关键词检索用 ElasticSearch 中间件,它是专门用来实现全文检索的。
一句话定位:数据库是根,而中间件是特种兵。我们会把原始数据存 MySQL,把需要检索的部分同步到 ES 里,这样关键词检索就可以走 ES 了。
我们这节学一下 ElasticSearch 全文检索的中间件。
用上节学的 docker compose 的方式安装(之前安装 Milvus 也是这样)。
创建目录:
mkdir es-test
cd es-test
npm init -y添加 docker-compose.yml:
version: '3.8'
services:
# Elasticsearch 最新稳定版:8.17.0
es:
image: elasticsearch:8.17.0
container_name: es-dev
ports:
- "9200:9200" # ES 对外提供服务的端口
environment:
- discovery.type=single-node # 单节点运行(开发环境)
- xpack.security.enabled=false # 关闭安全认证,免密码访问
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m # JVM 内存配置,避免占用过高
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/es:/usr/share/elasticsearch/data
restart: always
# Kibana 最新稳定版:8.17.0(必须与 ES 版本完全一致)
kibana:
image: kibana:8.17.0
container_name: kibana-dev
ports:
- "5601:5601" # Kibana 网页控制台端口
environment:
- ELASTICSEARCH_HOSTS=http://es:9200 # 连接 ES 容器内部地址
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/kibana:/usr/share/kibana/data
restart: always
depends_on:
- es # 等待 ES 启动完成后再启动 Kibana
networks:
default:
name: common-networkKibana 是 ES 的一个可视化控制台。跑一下:
docker compose up -dMySQL 是表和行,在 ES 里就是索引(index)和文档(document)。ES 没有 SQL,都是一些 HTTP 的接口(我们下面用 Kibana 的 Dev Tools 写这些 HTTP 请求)。
我们试一下索引的创建和文档的增删改查。
# 1. 查看所有索引
GET /_cat/indices?v&h=health,status,index,docs.count
# 2. 创建索引
PUT /article
{
"mappings": {
"properties": {
"title": { "type": "text" },
"content": { "type": "text" },
"author": { "type": "keyword" },
"createTime": { "type": "date" },
"viewCount": { "type": "integer" }
}
}
}
# 3. 查看索引结构
GET /article/_mapping
# 4. 查看索引配置
GET /article/_settings
# 5. 删除索引
DELETE /article注意字段类型:text 会被分词、可用于全文检索;keyword 不分词、用于精确匹配(如作者、分类)。
# 1. 新增文档(自动生成 ID)
POST /article/_doc
{
"title": "Elasticsearch 全文检索入门",
"content": "ES 基于倒排索引与 BM25 实现全文搜索,适用于文本检索场景",
"author": "后端开发",
"createTime": "2026-04-26",
"viewCount": 128
}
# 2. 新增文档(指定自定义 ID)
PUT /article/_doc/1001
{
"title": "RAG 混合检索实战",
"content": "ES 负责关键词检索,Milvus 负责向量语义检索,结合使用效果更佳",
"author": "AI 开发",
"createTime": "2026-04-26",
"viewCount": 256
}
# 3. 根据 ID 查询单条
GET /article/_doc/1001
# 4. 查询全部文档
GET /article/_search
{ "query": { "match_all": {} } }
# 5. 全文分词检索(text 字段)
GET /article/_search
{ "query": { "match": { "content": "RAG 向量 检索" } } }
# 6. 精确匹配查询(keyword 字段)
GET /article/_search
{ "query": { "term": { "author": "AI 开发" } } }
# 7. 只返回指定字段
GET /article/_search
{ "_source": ["title", "author"], "query": { "match_all": {} } }
# 8. 分页 + 排序
GET /article/_search
{
"from": 0, "size": 10,
"sort": [{ "viewCount": "desc" }],
"query": { "match_all": {} }
}
# 9. 局部更新文档(推荐)
POST /article/_update/1001
{ "doc": { "viewCount": 999, "title": "RAG 混合检索高级实战" } }
# 10. 全量覆盖更新
PUT /article/_doc/1001
{
"title": "全量覆盖测试", "content": "原始内容被替换",
"author": "测试用户", "createTime": "2026-04-26", "viewCount": 66
}
# 11. 根据 ID 删除文档
DELETE /article/_doc/1001
# 12. 条件批量删除
POST /article/_delete_by_query
{ "query": { "match": { "author": "后端开发" } } }
# 13. 统计文档总数
GET /article/_count
# 14. 清空索引数据(保留表结构)
POST /article/_delete_by_query
{ "query": { "match_all": {} } }我们测了一遍索引的创建、文档的增删改查,整体比较简单。
那用 ES 和之前用 MySQL 比有啥好处呢?ES 相比 MySQL 最大的核心优势,本质来源于倒排索引的底层设计。
普通 MySQL 使用的是正向索引:以一行为单位存储完整数据,检索文本内容时,需要逐行遍历、逐个字段匹配内容。数据量越大、文本越长,模糊 / 全文搜索就越慢,性能极差,并不适合大范围关键词检索。
而 Elasticsearch 采用倒排索引机制:会自动对 text 类型字段进行分词处理,拆解为一个个独立词条,再以「词条」为核心,反向关联所有包含该词条的文档。
简单来说:
基于这种结构,用户输入关键词检索时,ES 只需通过词条快速匹配对应的文档,无需全表遍历,就能实现海量文本下毫秒级的全文检索。
综上,ES 倒排索引的底层架构,就是专门为海量文本、关键词模糊检索、内容匹配场景量身设计的,这也是它吊打 MySQL 全文搜索的根本原因。理解了倒排索引,就理解了 ES 了。
显然,在 ES 里分词是很重要的——不同的分词建的索引表都不同。
ES 默认的 standard 分词器对中文支持不好:
POST /_analyze
{
"analyzer": "standard",
"text": "Elasticsearch RAG 混合检索知识库"
}它是每个字单独拆开的,而实际上应该"混合"、"检索"分别是整体来建立索引。这就需要用到 IK 分词器了。
创建 elasticsearch/Dockerfile:
# 官方 ES 基础镜像
FROM elasticsearch:8.17.0
# 安装 IK 分词(版本严格和 ES 一致)
RUN elasticsearch-plugin install --batch \
https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-8.17.0.zip就是在 ES 的容器里,执行命令安装 IK 分词器插件。我们用这个 Dockerfile 来构建 ES 镜像,改一下 docker-compose.yml:
# Elasticsearch 8.17.0 + IK 中文分词(内置到镜像)
es:
build: ./elasticsearch # 从本地 Dockerfile 构建镜像(自带 IK)
container_name: es-dev
ports:
- "9200:9200"
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/es/data:/usr/share/elasticsearch/data
restart: always重新构建并启动:
docker compose down
docker compose up -d --build验证:
# 1. 检查 ES 状态
GET /
# 2. 查看已安装插件
GET /_cat/plugins?v
# 3. 原生 standard 分词
POST /_analyze
{ "analyzer": "standard", "text": "Elasticsearch RAG 混合检索知识库" }
# 4. IK 细粒度分词(索引入库用)
POST /_analyze
{ "analyzer": "ik_max_word", "text": "Elasticsearch RAG 混合检索知识库" }
# 5. IK 智能分词(搜索查询用)
POST /_analyze
{ "analyzer": "ik_smart", "text": "Elasticsearch RAG 混合检索知识库" }IK 分词器有两种模式:
ik_max_word(细粒度):把文本尽可能拆成最多的词,用于索引入库(比如"知识库"会进一步细分为"知识"、"库"),召回率高ik_smart(智能/粗粒度):只拆出最关键、不重叠的词,用于搜索查询,效率高有了 IK 分词器之后,就可以快速地查询关键词对应的文档了。之前(standard)是把搜索词分词后去索引表匹配,但默认分词器拆太细,索引的意义不大;用了 IK 分词器之后,按照中文的词语来分词建立倒排索引表,查询时也用分词器分词、查索引表、结果合并后返回——这样的机制,自然可以毫秒级实现关键词检索。
之前创建 article 的索引用的是默认的分词器。这次我们用 ik_max_word 来做索引生成(更细粒度),用 ik_smart 来做检索的分词。关键就是在字段上同时配 analyzer(入库分词)和 search_analyzer(查询分词):
# Elasticsearch IK 分词版操作手册
# 索引:life_note
# 字段全部配置 IK 分词:入库 ik_max_word / 查询 ik_smart
# 1. 创建索引(生活笔记场景 + IK 双分词)
PUT /life_note
{
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
"content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
"type": { "type": "keyword" },
"author": { "type": "keyword" },
"record_time": { "type": "date" }
}
}
}
# 2. 查看索引结构
GET /life_note/_mapping
# 3. 查看索引配置
GET /life_note/_settings
# 4. 删除索引
DELETE /life_note文档的增删改查和前面 article 基本一致,只是字段换成了生活笔记场景(title/content/type/author/record_time),检索示例:
# 全文分词检索(IK 中文分词,搜:健康 作息 旅行)
GET /life_note/_search
{ "query": { "match": { "content": "健康 作息 旅行" } } }
# 精确匹配查询(keyword 分类字段)
GET /life_note/_search
{ "query": { "term": { "type": "健康生活" } } }
# 分页 + 时间排序
GET /life_note/_search
{
"from": 0, "size": 10,
"sort": [{ "record_time": "desc" }],
"query": { "match_all": {} }
}试一下,我们就基于 IK 分词器实现了中文的关键词检索。
学习 ElasticSearch 还要知道一个 BM25 的算法。虽然用 IK 分词器分词后,建立倒排索引表,然后就可以检索了。但是检索到的文档相关性怎么样,如何排序?这就是 BM25 算法做的。
BM25(Best Matching 25)是全文检索的相关性打分算法。它有一些策略,比如:
总之,BM25 算法是公平、高效、稳定的关键词排序算法,ES 默认用它,后面 RAG 做关键词检索也是依赖这个。
理解了 IK 分词器 + 倒排索引 + BM25 算法,就理解了 ElasticSearch 了。
这节我们学了 ElasticSearch 做全文检索。用 docker compose 安装了 ES 和它的可视化控制台 Kibana。
ES 里只有索引、文档这两层。我们通过 HTTP 的接口创建了索引、文档,做了文档的增删改查。
ES 的检索原理就是倒排索引,也就是关键词 → 文档的索引表,这是它能实现海量文档毫秒级检索的核心:
text 的字段分词,放到倒排索引表(类型为 keyword 的字段不会)中文分词我们安装了 IK 分词器,入库用 ik_max_word 细粒度分词,检索用 ik_smart 高效分词。
理解了倒排索引表 + BM25 算法 + IK 分词器,串起来就理解了 ElasticSearch 的核心了。
下一篇就是混合检索 RAG:把 ES 的关键词检索和 Milvus 的向量语义检索多路召回,再用重排模型融合——这正是前面 Agentic RAG 里"结合关键词与语义检索"那句话的工程落地。
学完 ES 之后,一个很自然的问题浮出来了:平时做东西,MySQL、ES、向量数据库这三个,我们是都要有,还是只要 ES + 向量库就行?做 RAG 通常怎么选?MySQL 还是必须的吗?
先给结论:这三个不是"三选几"的关系,而是各管一类访问模式、互补的。是否全要,取决于你的数据和查询长什么样。
| 数据库 | 擅长什么 | 在 RAG 里的位置 |
|---|---|---|
| MySQL / Postgres | 业务结构化数据、事务、增删改查、join、精确更新 | 系统 of record(权威源),不是检索组件 |
| 向量库(Milvus / pgvector) | 语义相似检索——"意思接近"的文档 | 语义召回那一半 |
| ES | 关键词 / 全文精确检索——精确词、术语、ID、短语,BM25 排序 | 关键词召回那一半 |
关键认知:向量库和 ES 解决的是"怎么把相关文档找出来",MySQL 解决的是"我的核心业务数据存在哪、怎么管"。它们是不同层的事。
按检索强度分三档:
id 回 MySQL 取完整/最新数据。分两层看:
现实工程里更常见的省心做法:直接用 Postgres 一把梭——关系表 + pgvector(向量)+ pg_search(BM25 关键词)。这样不单独养 Milvus 和 ES,运维成本低,中小企业/创业项目完全够用(呼应上面评论区"pg 也有 BM25 插件""中小企业用 PostgreSQL 足够"的讨论)。只有当数据量很大、并发很高时,才值得把 Milvus / ES 拆出来当专用中间件。
先问自己两件事:① 我的核心业务数据要事务和结构化查询吗? 要 → 必有 MySQL/Postgres。② 我需要多少种"找文档"的方式? 只要语义 → 向量库;要专业术语/精确匹配也准 → 再加 ES。三者不是都要,但"系统 of record + 至少一种检索"这个组合基本跑不掉。
预览到此为止,输入密码解锁全文
解锁后本机会记住,同密码的其他文章也无需重复输入