写在前面
做后端的朋友大概率都遇到过这个场景:产品经理跑过来跟你说,"能不能给文章加个搜索功能?就按内容搜,跟百度那样。"
你第一反应可能是:
SELECT * FROM article WHERE content LIKE '%关键词%';
跑一下,几条数据的时候挺快,等到表里躺了几十万条、content 字段动辄几百字的时候,你会发现------查询直接奔着秒级去了,稍微并发一下数据库 CPU 拉满。
于是你就被逼着去了解 ElasticSearch。
这篇文章不打算把 ES 讲成大部头教材,我只想带你走完这条线:
为什么 MySQL 扛不住 → 倒排索引到底是怎么回事 → ES 怎么上手 → 最后怎么和向量检索组合成混合检索,落到 RAG / Agentic RAG 场景里。
读完你应该能把一个"关键词搜索 + 语义搜索"的服务真正跑起来。
一、MySQL 为什么不适合全文检索?
先说清楚一件事:MySQL 不是不好,它是被用错了地方。
MySQL 是关系型数据库,它的强项是什么?
- 按字段精确查找(
WHERE id = 1) - 表关联(
JOIN) - 事务、一致性、约束
这些都是它吃饭的本事,快得一批。
但到了"按文本内容模糊搜索"这件事上,它就很难受了。原因在于它的存储和检索方式:
MySQL 是按行存储 的,一行数据是一个完整单位。当你用
LIKE '%关键词%'时,它没有任何捷径可走,只能逐行扫描、逐字匹配。
说白了就是:一条一条看过去,看完才知道有没有。
数据量越大、文本越长、模糊匹配越复杂,它就越慢。这不是调优能解决的问题,这是数据结构决定的先天缺陷。
所以结论很直接:
大范围的关键词/文本检索,别用
LIKE,交给专门的搜索引擎来做。
这个"专门的搜索引擎",就是 ElasticSearch。
二、核心差异:正向索引 vs 倒排索引
ES 相比 MySQL,最大的杀手锏不是"它是个新数据库",而是它底层的 倒排索引(Inverted Index) 机制。
我们用一个具体的例子感受一下。假设有这样一段文本:
可以在浏览器中运行机器学习模型,支持图像、文本和声音等多种应用场景
MySQL 的思路(正向索引)是:
文档 → 关键词
存的是"这句话",你搜的时候再去这句话里翻。
ES 的思路(倒排索引)是:
关键词 → 文档
它会先对 text 类型字段做分词(tokenization) ,把句子拆成一个个独立词条:
可以 / 浏览器 / 机器学习 / 模型 / 图像 / 文本 / 声音 / 应用场景...
然后以词条为核心,反向记录"哪些文档包含这个词":
浏览器 → [文档1, 文档2]
机器学习 → [文档1, 文档5]
图像 → [文档1]
用户输入关键词时,ES 只需要拿着这个词去词条表里秒查 ,直接拿到文档 ID 列表,完全不需要全表遍历。
这就是为什么 ES 能在海量文本 下做到毫秒级全文检索。
一句话记忆:MySQL 是"给你一本书,一页页翻着找词";ES 是"先给你建好目录,直接翻到那一页"。
三、先把环境跑起来:docker-compose 三件套
理论说完了,动手。ES 用 Docker 起是最省事的。
先明确几个概念,别被 Docker 的名词吓到:
- 镜像 image:代码 + 环境依赖,打包好的"模板"
- 容器 container:镜像运行起来之后的实例
- compose :把多个镜像编排 到一起启动,配置文件就是
docker-compose.yml
我们要起两个东西:
| 服务 | 端口 | 作用 |
|---|---|---|
| Elasticsearch | 9200 | 存索引、提供检索 API |
| Kibana | 5601 | 可视化操作 ES,类似 phpMyAdmin 之于 MySQL |
docker-compose.yml 大概长这样:
version: '3.8'
services:
# Elasticsearch 最新稳定版:8.17.0
es:
build: ./elasticsearch
container_name: es-dev
ports:
- "9200:9200" # ES 对外提供服务的端口
environment:
- discovery.type=single-node # 单节点运行(开发环境)
- xpack.security.enabled=false # 关闭安全认证,免密码访问
- xpack.security.http.ssl.enabled=false # 关闭 HTTPS 加密
- 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-network
启动命令就一行:
docker compose up -d
拆解一下这条命令:
docker compose:在当前目录找docker-compose.ymlup:把服务启动起来-d:后台运行(detached)
起来之后,浏览器打开 http://localhost:5601 就能看到 Kibana,后面所有操作都可以在它的 Dev Tools 里直接敲。
四、ES 请求心智模型:方法、路径、参数、Body
很多同学第一次看 ES 命令会懵:
- 为什么有的用
GET,有的用POST,有的用PUT? /_doc、/_search、/_mapping这些路径到底怎么拼?- Kibana 里能跑,换成 curl 或 Java 客户端怎么写?
POST和PUT到底有什么区别?- 为什么有时候查不到刚写入的数据?
这一节就把 ES 的 RESTful 请求方式彻底拆开。
4.1 ES 的本质:一切皆 HTTP 请求
ES 对外暴露的是一套 RESTful JSON API。你可以把它理解成一个 HTTP 服务:
HTTP 方法决定"做什么",路径决定"操作谁",查询参数决定"细节",请求体里的 DSL 决定"怎么做"。
一个完整的 ES 请求由四部分组成:
<HTTP 方法> /<路径>?<查询参数>
{
"请求体 JSON"
}
例如:
POST /article/_search?pretty
{
"query": {
"match": { "title": "Elasticsearch" }
}
}
拆开看:
| 部分 | 内容 | 作用 |
|---|---|---|
| HTTP 方法 | POST |
提交一次搜索请求 |
| 路径 | /article/_search |
对 article 索引执行搜索 |
| 查询参数 | ?pretty |
返回结果格式化 |
| 请求体 | query.match... |
查询 DSL |
Kibana Dev Tools 里之所以能省略 http://localhost:9200,是因为它已经帮你连好了 ES。换成 curl,就必须补全地址和请求头。
4.2 HTTP 方法速查:GET / POST / PUT / DELETE / HEAD
ES 里最常用的 HTTP 方法就这几个:
| HTTP 方法 | 语义 | ES 典型用途 | 是否幂等 |
|---|---|---|---|
GET |
读取 | 查询文档、搜索、查看 mapping/settings、_cat | 是 |
POST |
提交/执行 | 自动生成 ID 新增文档、_update、_search、_bulk、_delete_by_query |
通常不幂等 |
PUT |
创建/覆盖 | 创建索引、指定 ID 写入文档、更新 mapping/settings | 是 |
DELETE |
删除 | 删除索引、删除文档 | 是 |
HEAD |
探测 | 判断索引/文档是否存在 | 是 |
一句话记忆:
GET 查,POST 提交,PUT 覆盖,DELETE 删,HEAD 探。
GET:读操作,但也能带 body
GET 在 ES 里最常用来查:
GET /article/_doc/1001
GET /article/_mapping
GET /article/_settings
GET /_cat/indices?v
GET /article/_search
{
"query": { "match_all": {} }
}
注意:ES 是支持 GET 带请求体的 ,所以上面 GET /article/_search 带 JSON 在 Kibana 里能跑。
但这里有个坑:
虽然 HTTP 规范没有完全禁止 GET 带 body,但很多代理、网关、HTTP 客户端会忽略 GET 的 body。
所以复杂搜索建议用 POST,兼容性更好。
也就是说,下面两种写法在 ES 里都合法:
GET /article/_search
{
"query": { "match": { "title": "ES" } }
}
POST /article/_search
{
"query": { "match": { "title": "ES" } }
}
生产环境更推荐 POST /article/_search。
POST:新增、更新、搜索、批量
POST 是 ES 里最灵活的方法,常见用途:
POST /article/_doc # 自动生成 ID 新增文档
POST /article/_update/1001 # 部分更新文档
POST /article/_search # 搜索
POST /_bulk # 批量操作
POST /article/_delete_by_query # 按条件删除
为什么新增文档有时用 POST 有时用 PUT?看下面。
PUT:指定 ID 写入,幂等
PUT 通常表示"创建一个确定 ID 的资源":
PUT /article
{
"mappings": { ... }
}
创建索引 article。
PUT /article/_doc/1001
{
"title": "ES 入门",
"content": "倒排索引..."
}
指定文档 ID 为 1001。
PUT 的特点是幂等 :你执行一次和一百次,结果都是 ID 为 1001 的文档被写成同样的内容。
但注意:
PUT /article/_doc/1001是全量覆盖 。如果你原来文档有
title、content、author,这次只传了title,那么其他字段会丢失。想部分更新,用
POST /article/_update/1001。
DELETE:删除索引或文档
DELETE /article/_doc/1001 # 删除单条文档
DELETE /article # 删除整个索引
删除也是幂等的:删完之后再删一次,会返回 not_found,但不会产生额外副作用。
HEAD:只关心存在不存在
HEAD /article
HEAD /article/_doc/1001
HEAD 不返回 body,只看 HTTP 状态码:
200:存在404:不存在
适合在程序里做存在性判断,省带宽。
4.3 ES 路径结构:索引、文档、搜索、映射
ES 的路径其实很有规律,记住几个核心模板就行:
/<index> # 索引本身
/<index>/_doc/<id> # 单条文档
/<index>/_search # 搜索
/<index>/_mapping # 映射
/<index>/_settings # 设置
/_cat/... # 集群/索引查看
/_bulk # 批量
/_mget # 多文档查询
举几个例子:
GET /article/_doc/1001
GET /article/_search
GET /article/_mapping
GET /article/_settings
GET /_cat/indices?v
POST /_bulk
GET /_mget
在 ES 7.x 之前,路径里还有"类型 type"的概念,比如 /<index>/<type>/<id>。
7.x 之后 type 被废弃,统一用 _doc:
PUT /article/_doc/1001
你可以把 _doc 理解成固定端点,不用再纠结 type。
4.4 HTTP 状态码:看返回值判断结果
ES 返回的 HTTP 状态码很有用:
| 状态码 | 含义 | 常见场景 |
|---|---|---|
200 OK |
成功 | 查询、更新、删除成功 |
201 Created |
创建成功 | 新建索引、写入新文档 |
400 Bad Request |
请求错误 | DSL 语法错误、字段类型不匹配 |
401 Unauthorized |
未认证 | 没带用户名密码 |
403 Forbidden |
无权限 | 用户权限不足 |
404 Not Found |
不存在 | 索引/文档不存在 |
409 Conflict |
冲突 | 版本冲突、索引已存在 |
413 Payload Too Large |
请求体太大 | 单次 bulk 太大 |
429 Too Many Requests |
请求过多 | 限流、线程池满 |
500 Internal Server Error |
服务端错误 | 分片失败、脚本异常 |
程序里不要只看 body,HTTP 状态码是第一道判断。
五、索引操作:建表、映射、设置
5.1 索引(Index)≈ MySQL 的表
ES 里没有"表"的概念,对应的叫索引(Index) 。
先看看当前有哪些索引:
GET /_cat/indices?v&h=health,status,index,docs.count
创建索引的时候需要定义 mappings------这玩意儿就相当于 MySQL 的建表 schema。
PUT /article
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"title": { "type": "text" },
"content": { "type": "text" },
"author": { "type": "keyword" },
"createTime": { "type": "date" },
"viewCount": { "type": "integer" }
}
}
}
这里有一个新手最容易踩的坑,也是 ES 字段类型设计的核心:
| 类型 | 行为 | 适用场景 |
|---|---|---|
text |
会分词,拆成词条再建索引 | 标题、正文等内容字段 |
keyword |
不分词,整体作为一个词条精确匹配 | 作者、状态、标签、ID |
看上面的定义:title 和 content 用 text,因为你要按关键词搜;author 用 keyword,因为你要的是"作者恰好等于 AI开发"这种精确匹配。
金句:
text负责"全文检索",keyword负责"精确过滤",两者搭配才是完整的搜索体验。
5.2 查看映射与设置
GET /article/_mapping # 查看字段结构
GET /article/_settings # 查看索引配置
5.3 判断索引是否存在
HEAD /article
返回 200 表示存在,404 表示不存在。
5.4 新增字段
PUT /article/_mapping
{
"properties": {
"status": { "type": "keyword" }
}
}
注意:ES 不支持直接修改已有字段类型,只能新增字段。要改类型,一般要重建索引 + reindex。
5.5 修改设置
PUT /article/_settings
{
"number_of_replicas": 1
}
5.6 删除索引
DELETE /article
删库慎用。
5.7 关闭 / 打开索引
POST /article/_close
POST /article/_open
5.8 别名操作
POST /_aliases
{
"actions": [
{ "add": { "index": "article_v1", "alias": "article" } }
]
}
别名的好处是:重建索引时,应用层不用改代码,只切别名。
六、文档操作:新增、查询、更新、删除
6.1 新增:自动 ID vs 指定 ID
自动生成 ID:
POST /article/_doc
{
"title": "Elasticsearch 全文检索入门",
"content": "ES 基于倒排索引与 BM25 实现全文索引,适用于文本检索场景",
"author": "后端开发",
"createTime": "2026-09-20",
"viewCount": 120
}
指定 ID:
PUT /article/_doc/1001
{
"title": "Elasticsearch 全文检索入门",
"content": "ES 基于倒排索引与 BM25 实现全文索引,适用于文本检索场景",
"author": "后端开发",
"createTime": "2026-09-20",
"viewCount": 120
}
区别:
| 操作 | 方法 | 路径 | ID | 幂等 |
|---|---|---|---|---|
| 自动 ID | POST | /_doc |
ES 生成 | 否 |
| 指定 ID | PUT | /_doc/1001 |
手动指定 | 是 |
6.2 查询单条文档
GET /article/_doc/1001
6.3 判断文档是否存在
HEAD /article/_doc/1001
6.4 部分更新
POST /article/_update/1001
{
"doc": {
"viewCount": 121
}
}
只会更新 viewCount,其他字段不变。
也可以用脚本更新:
POST /article/_update/1001
{
"script": {
"source": "ctx._source.viewCount += params.inc",
"params": {
"inc": 1
}
}
}
6.5 删除文档
DELETE /article/_doc/1001
6.6 多文档查询 _mget
GET /_mget
{
"docs": [
{ "_index": "article", "_id": "1001" },
{ "_index": "article", "_id": "1002" }
]
}
也可以指定索引:
GET /article/_mget
{
"docs": [
{ "_id": "1001" },
{ "_id": "1002" }
]
}
七、搜索操作:match / term / multi_match / bool
搜索是 ES 最核心的操作。两种方式:
GET:
GET /article/_search
{
"query": {
"match": { "title": "ES" }
}
}
POST:
POST /article/_search
{
"query": {
"match": { "title": "ES" }
}
}
前面说过,复杂 DSL 推荐 POST。
7.1 URI 查询:简单场景
GET /article/_search?q=title:检索&size=10
适合临时调试,不适合复杂业务。
7.2 match ------ 会分词
GET /article/_search
{
"query": {
"match": { "content": "全文检索" }
}
}
match 会把你输入的内容也分词 ,然后按词条去匹配。适合 text 字段。
7.3 term ------ 不分词,精确匹配
GET /article/_search
{
"query": {
"term": { "author": "AI开发" }
}
}
term 不做任何分词 ,拿整个字符串去词条表里精确查。专门用于 keyword 字段。
记住这条规则:
match配text,term配keyword,配错了就是查不出结果。
7.4 multi_match ------ 多字段匹配
GET /article/_search
{
"query": {
"multi_match": {
"query": "检索",
"fields": ["title", "content"]
}
}
}
一次查多个字段,用户搜"检索"的时候,标题或正文命中都算,非常实用。
7.5 bool 查询:must / filter / should / must_not
POST /article/_search
{
"query": {
"bool": {
"must": [
{ "match": { "content": "检索" } }
],
"filter": [
{ "term": { "author": "AI开发" } }
],
"should": [
{ "match": { "title": "Elasticsearch" } }
],
"must_not": [
{ "term": { "status": "deleted" } }
]
}
},
"_source": ["title", "author", "createTime"],
"from": 0,
"size": 10,
"sort": [
{ "createTime": "desc" }
],
"highlight": {
"fields": {
"content": {}
}
}
}
这段基本覆盖了日常搜索:
must:必须匹配,参与打分filter:必须匹配,不参与打分,可缓存,适合精确过滤should:可选匹配,命中加分must_not:必须不匹配_source:返回哪些字段from/size:分页sort:排序highlight:高亮
7.6 只返回部分字段(省流量)
GET /article/_search
{
"_source": ["title", "author"],
"query": { "match_all": {} }
}
_source 就相当于 SQL 里的 SELECT title, author,返回大字段(比如 content)非常吃带宽,按需指定。
7.7 分页:from/size 与深分页
浅分页:
"from": 0,
"size": 10
深分页不建议用大 from,比如 from: 100000,性能很差。生产深分页用 search_after + PIT。
7.8 聚合查询
POST /article/_search
{
"size": 0,
"aggs": {
"authors": {
"terms": {
"field": "author",
"size": 10
}
}
}
}
size: 0 表示不返回文档,只要聚合结果。
五、从 ES 到混合检索:为什么单靠关键词还不够?
到这里你已经能搭起一个可用的全文检索服务了。但如果你在做 RAG(检索增强生成)或者 Agent,只上 ES 会撞到一堵墙。
先看两个典型问题:
问题一:语义相近但字面不同,ES 搜不到。
用户搜"土豆怎么做",但你的文档里写的是"马铃薯的烹饪方法"。ES 基于词条, "土豆"和"马铃薯"是两个完全不同的词条,匹配不上。
问题二:纯语义检索(向量检索)也不完美。
如果把文档灌进向量数据库(比如 Milvus),用 embedding 做相似度检索,上面那个问题就解决了------因为"土豆"和"马铃薯"在语义空间里离得很近。
但是反过来,专业术语、精确实体、代号、ID 这类东西,纯语义检索经常"匹配不准",甚至"糊成一团"。
举个例子:用户明确要找"型号 X200",向量检索可能给你返回一堆"X100、X300、X500",语义都很像,但全都不是他要的。
于是答案就清楚了:
关键词检索(ES)+ 语义检索(Milvus)= 混合检索(Hybrid Search)
六、混合检索架构长什么样?
大致流程:
┌─────────────────┐
用户 Query ────┬──▶│ ES 关键词检索 │──▶ 命中结果 A
│ └─────────────────┘
│
│ ┌─────────────────┐
└──▶│ Milvus 向量检索 │──▶ 命中结果 B
└─────────────────┘
│
▼
┌─────────────────┐
│ 模型统一融合 │──▶ 最终 Top-K
│ (RRF / rerank) │
└─────────────────┘
│
▼
交给 LLM 生成
关键点:
- 两路并行:ES 走关键词,Milvus 走向量,各自召回一批候选
- 统一融合:用 RRF(Reciprocal Rank Fusion)或者重排模型(reranker)把两路结果合并、打分、排序
- 再喂给 LLM:融合后的 Top-K 作为上下文
这样做的好处非常直接:专业术语/精确实体靠 ES 保证不跑偏,语义泛化靠向量保证不漏召。
放到 Agentic RAG 的场景里会更明显。比如用 LangGraph 搭一个闭环 Agent:
- Agent 自主决策这次要不要检索
- 检索的话用哪个工具:Web Search / Milvus / ES
- 拿到结果后判断信息够不够 、效果好不好
- 不够就换个检索方式再搜一轮
比如用户问"型号 X200 的语义相似产品",Agent 第一轮可以直接走 ES 锁定 X200,再用 Milvus 找语义相近的品类。
工具组合是死的,Agent 的判断是活的。真正要理解的是这个"决策---检索---评估---再检索"的闭环思路,具体怎么实现一定是要贴着业务场景设计的。
七、几个容易忽略的落地建议
最后给几条实战经验,都是踩过坑总结的:
1. 字段类型别乱设
author、status、tag 这类字段一律 keyword。如果你既想精确匹配又想分词,可以用 text + keyword 多字段:
"title": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
}
2. 中文分词要用 IK 插件
ES 默认分词器对中文是"一字一词",效果很差。上 IK 分词器:
用docker
RUN elasticsearch-plugin install --batch \
https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-8.17.0.zip
然后在 mapping 里指定 "analyzer": "ik_max_word"。
3. 别把 ES 当主数据库用
ES 更像个"特种兵"------专门做关键词检索的。原始数据、事务、强一致,还是老老实实放 MySQL。ES 里的数据是从 MySQL 同步过来的副本。
4. Kibana 是开发期神器
http://localhost:5601 里的 Dev Tools 可以直接敲上面所有命令,比 Postman 顺手太多。它之于 ES,就像 phpMyAdmin 之于 MySQL。
总结
把这条线再串一遍:
- MySQL 的 LIKE 慢,是因为正向索引下只能全表逐行扫描
- ES 快,是因为它用倒排索引把"文档 → 词"反过来了,变成"词 → 文档"
text分词、keyword不分词 ,match配text、term配keyword- 单靠 ES 搜不了语义,单靠向量搜不准实体 ,所以要有混合检索
- 混合检索的融合层才是准确率的关键,RRF / rerank 都值得试
- Agentic RAG 本质上是把"选哪种检索"这件事交给 Agent 自主决策
一句话收尾:
关键词检索解决"找得到",语义检索解决"找得准",两者融合加上 Agent 的自主决策,才是真正能落地的 RAG。