Elasticsearch + Kibana 增删改查
用 Kibana Dev Tools 手把手跑通 ES 的索引/文档增删改查,并从分词器原理出发讲透为什么中文检索必须上 IK
Leo
2026.09.03 · Updated 2026.09.14
Elasticsearch + Kibana 增删改查与中文分词全解
给 MySQL 里的一百万条商品数据做模糊搜索,
LIKE '%xxx%'会全表扫描、慢到怀疑人生,还谈不上"相关度排序"。 Elasticsearch(ES)就是为了全文搜索而生的引擎——它靠一套叫"倒排索引"的数据结构,把"查词"变成"翻字典"一样快,再配合 Kibana 这个图形控制台,CRUD、调分词、看索引状态全部点点鼠标就能跑通。本文基于 ES 8.17 单机版 + Kibana 8.17(已装 IK 中文分词插件),所有示例都写在 Kibana Dev Tools 里,打开就能复现。每段都会给你预期输出,实际跑一下对照即可。
为什么:LIKE 解决不了的事,才轮到 ES
先别急着敲代码,想清楚"ES 解决了什么",后面每个概念都顺理成章。
LIKE '%手机%' 有三个先天不足:
- 慢:无法走索引,必须逐行扫描。
- 笨:只能按"字符挨个包含"匹配,搜
手机壳的关键词手机想命中"智能机型配件"?没门——因为它不认识"词"。 - 不会排序:结果按行号返回,没有"哪条更相关"的概念。
ES 的解法是倒排索引:建索引时先把每篇文档拆成一个个"词"(分词),然后记一张"词 → 出现在哪些文档"的表。搜索时拿查询词直接查表,一步到位。
一个粗糙的对照表,帮你判断"这事到底该不该上 ES":
| 场景 | MySQL / 关系库 | Elasticsearch |
|---|---|---|
| 精确等值查询(用户名、订单号) | ✅ 主场,靠索引飞快 | 可以,用 keyword 字段,但没必要 |
| 区间、聚合统计、事务、复杂联表 | ✅ 擅长 | ❌ 弱项,别硬来 |
| 全文检索、模糊词、相关度排序 | ❌ LIKE 全表扫 | ✅ 倒排索引 + 分词 + 评分 |
| 中文搜索"懂的词义级拆分" | ❌ 只会字符匹配 | ✅ 配合 IK 分词 |
一句话结论:ES 是"搜索 + 分析"的引擎,不是通用数据库。业务主数据放库里,搜索需求交给 ES。
一、环境:Docker compose 拉起 ES + Kibana(含 IK 分词)
这套环境配了三件事,直接可用:
- ES 8.17 单节点,关闭了安全认证(免密码)、关闭 HTTPS,因此所有请求走
http://localhost:9200。 - Kibana 8.17,控制台在
http://localhost:5601。 - ES 镜像里预装了 IK 分词插件(版本与 ES 严格一致 8.17.0),这是中文检索的核心,后面专门讲。
如果从零起,最小配置长这样(实际部署建议再把数据目录挂上持久化卷,避免重启丢数据):
services:
es:
image: elasticsearch:8.17.0
container_name: es-dev
ports: ["9200:9200"]
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m
kibana:
image: kibana:8.17.0
ports: ["5601:5601"]
environment:
- ELASTICSEARCH_HOSTS=http://es:9200
depends_on: [es]
在保存这份配置的目录下执行,然后确认两个服务都活着:
docker compose up -d
curl http://localhost:9200 # 返回带 version 的 JSON 就说明 ES 好了
浏览器打开 **http://localhost:5601**,左侧菜单 → Management / Dev Tools → Console(旧版叫 Dev Tools)。Console 里的请求其实就是一套简化的 REST 语法,底层等价于 curl,后面所有示例都贴在这里执行:
# 在 Dev Tools 里输下面这行,点右上角 ▶(或 Ctrl+Enter)执行
GET /_cat/health?v
预期输出:一行状态表,status 为 green / yellow 都算健康(单节点无副本时 yellow 正常),cluster: es-dev。
要点:
- Dev Tools ≈ curl,你写的每一行 REST 都能翻译成
curl -X<方法> http://localhost:9200/<路径>。 - 每次写完请求自己点 ▶ 跑一遍,本文所有预期输出都建议实测对照——这就是学 ES 最快的方式。
- 常见对象层级:Cluster(集群)→ Index(索引)→ Document(文档),下面从"索引"讲起。
二、索引(Index)的增删改查
一句话:Index 是存放同结构文档的容器,可以粗略理解成关系库里的"表"。但它比"表"更特殊:建好之后字段类型(mapping)基本定死,所以建索引要想清楚结构。
创建索引
PUT /books
预期输出:{ "acknowledged": true, "shards_acknowledged": true, "index": "books" }
默认索引没有任何字段约束(mapping),ES 会动态推断字段类型,随写随建,开发期很省事。
查看索引
GET /books # 查看单个索引的详情(mapping、settings 等)
GET /_cat/indices?v # 一行一个索引,看全量状态
预期输出(_cat 是给人类看的精简接口,v 是显示表头):能看到 index、health、docs.count 等列。
修改索引(改 mapping / 改 settings)
mapping 铁律:已经存在的字段不能改类型,只能新增字段,否则删了重建。
# 给 books 新增一个字段(合法)
PUT /books/_mapping
{
"properties": {
"summary": { "type": "text" }
}
}
# 调整 settings 中的动态配置(合法)
PUT /books/_settings
{
"index": { "number_of_replicas": 1 }
}
删除索引
DELETE /books
预期输出:{ "acknowledged": true }。删除后索引连同里面的文档一起没了,生产环境请三思。
要点:
- 查看用
GET /<index>、GET /_cat/indices?v;改结构用_mapping与_settings两个子端点。 - 删除是物理级的,没有回收站。
- ES 8.x 已经彻底移除了"类型(type)"概念——老教程里的
/books/book/1这种三段式写法在 8.x 已不可用。
三、文档(Document)的增删改查
一句话:Document 是 ES 存储的最小单元,本质是一份 JSON,类比数据库里的"一行"。
新增:手动指定 id 或让 ES 自动生成
# 指定 id = 1(可重复执行,会整体覆盖旧版本)
PUT /books/_doc/1
{
"title": "Elasticsearch 实战",
"author": "张三",
"price": 89.5,
"tags": ["搜索引擎", "Java"],
"summary": "从零开始讲清楚 Elasticsearch 的搜索与中文分词"
}
# 不指定 id,ES 自动生成(返回结果里带 _id)
POST /books/_doc
{
"title": "MySQL 性能优化",
"author": "李四",
"price": 69
}
预期输出(PUT 指定 id):状态码 201,响应含 "_id": "1"、"_version": 1、"_seq_no" 等。_version 每次更新都会 +1,是天然的版本号。
查询单个文档
GET /books/_doc/1
预期输出:found: true,_source 里是完整 JSON。id 不存在时返回 found: false,HTTP 状态码 404。
修改:部分更新 vs 整体覆盖
# 部分更新:只改 doc 里出现的字段,其余不动
POST /books/_update/1
{
"doc": { "price": 79.5 }
}
# 整体覆盖:重新 PUT 整个文档(_version 会 +1)
PUT /books/_doc/1
{
"title": "Elasticsearch 实战(第二版)",
"author": "张三",
"price": 99
}
预期输出(update):"result": "updated"。若 doc 内容和原文档完全一致,则返回 "result": "noop"(没变化,不写盘)。
删除文档
DELETE /books/_doc/1
GET /books/_doc/1 # 再查一次,found 应为 false
预期输出:删除返回 "result": "deleted"。
批量操作(进阶):一条请求干多件事
需要灌测试数据、或大批量更新时,用 _bulk 一次提交,省掉海量网络往返:
POST /_bulk
{ "index": { "_index": "books", "_id": 2 } }
{ "title": "Kibana 可视化入门", "author": "王五", "price": 55 }
{ "index": { "_index": "books", "_id": 3 } }
{ "title": "倒排索引原理", "author": "赵六", "price": 45 }
{ "delete": { "_index": "books", "_id": 2 } }
预期输出:errors: false,逐条 items 里能看到每个操作的结果。格式约定:奇数行是操作描述,偶数行是文档数据,没有空行。
要点:
- 更新有两种心智:
PUT整体覆盖、_update部分更新。频繁改单字段用_update。 - 并发写同一文档可能互相覆盖,ES 用
_seq_no+_primary_term做乐观锁:更新时带上?if_seq_no=1&if_primary_term=1,版本对不上就报冲突,让你重读再改。 - 刚写入立刻搜可能搜不到:ES 默认约 1 秒才刷新一次索引段。开发调试可手动
POST /books/_refresh强制刷新。
四、搜索:让 CRUD 变成"检索"
文档存进去就是为了搜。_search 是 ES 的检索入口,配合查询 DSL(一段 JSON)表达各种条件。
GET /books/_search
{
"query": { "match": { "title": "Elasticsearch" } }
}
预期输出:hits.total.value 为命中数,hits.hits[] 里每条带 _score(相关度分数)。match 查询会把关键词先分词再匹配。
四个高频 DSL 动词
| DSL | 干什么 | 什么时候用 |
|---|---|---|
match | 全文检索:查询词先分词,再对 text 字段匹配 | 搜标题、正文、简介 |
term | 精确匹配:不分词,整词比 | 对 keyword 字段、枚举值 |
range | 区间过滤:价格、日期 | 数值 / 时间范围 |
bool | 组合:must(必须) / should(任一) / filter(过滤) | 复杂查询拼装 |
示例:找标题含"Elasticsearch"、且价格 ≤ 100 的书:
GET /books/_search
{
"query": {
"bool": {
"must": [ { "match": { "title": "Elasticsearch" } } ],
"filter": [ { "range": { "price": { "lte": 100 } } } ]
}
}
}
预期输出:must 里的条件参与算分,filter 里的条件只过滤不算分——所以 filter 结果可被缓存,性能更好。
精确匹配 keyword 字段用 term
GET /books/_search
{
"query": { "term": { "author": "张三" } }
}
注意:term 不分词,所以它只对 keyword / 数字 / 日期这类整体值字段有意义;对 text 全文段用 term 常常搜不中,这也是下一章"分词"要解决的核心问题。
精简返回与分页
GET /books/_search
{
"_source": ["title", "price"],
"from": 0,
"size": 10
}
_source:只返回需要的字段,省流量。from / size:最朴素的分页;数据量大、要深翻页时换search_after(进阶)。
要点:
text字段用match(全文),keyword字段用term(精确)——这条配比关系是 ES 最容易踩的坑之一。bool里filter不算分、可缓存,能放filter别放must。- 搜索之前,先想想这些词当初是怎么被切进倒排索引的——这就引出了全文搜索的灵魂:分词器。
五、分词器(Analyzer):全文搜索真正的引擎
一句话:Analyzer(分词器)负责把"一段文本"切成"一个个索引词"。倒排索引能不能命中、中文搜得准不准,全看它切得好不好。
5.1 一个 Analyzer = 三段流水线
- char filter:写索引前先处理原始字符,如
html_strip剥掉 HTML 标签。 - tokenizer:把字符串切成一个个 term,如
standard(按空格/标点切)、ik_max_word。 - token filter:加工已切好的词,如
lowercase(转小写,让Apple和apple命中同一个词)、stop(去停用词)、stemmer(词干还原,running → run)。
5.2 用 _analyze 亲手"看"分词结果
这是本章最重要的一招:任何分词器的效果,用 _analyze 一测便知。先拿英文测试内置的 standard:
GET /_analyze
{
"analyzer": "standard",
"text": "The 2 QUICK Brown-Foxes jump over the lazy dog's bone."
}
预期输出:standard 按"非字母数字"切分并统一小写,但默认不去停用词:
the / 2 / quick / brown / foxes / jump / over / the / lazy / dog / s / bone
注意两点:Brown-Foxes 被切成 brown foxes;dog's 被切成 dog s。看到 the 还在,说明 standard 不带去停用词。想体验"去停用词 + 词干还原",换成 english:
GET /_analyze
{
"analyzer": "english",
"text": "The foxes are running away"
}
预期输出:fox / run / away——the/are 被停用词表吃掉,foxes→fox、running→run 做了词干还原。这就是"同一个意思的不同写法能搜到"的底层原因。
5.3 内置分词器怎么选(决策表)
GET /_analyze
{
"text": "The 2 QUICK Brown-Foxes",
"analyzer": "keyword"
}
| Analyzer | 切词规则 | 上面例子的输出 | 典型用途 |
|---|---|---|---|
standard(默认) | 按非字母数字切 + 转小写 | the 2 quick brown foxes | 大多数英文全文的默认选择 |
simple | 按非字母切,丢弃数字 | the quick brown foxes | 只要单词、不要数字的纯文本 |
whitespace | 只按空格切,不改大小写 | The 2 QUICK Brown-Foxes | 想保留原始大小写/标点时 |
keyword | 不切,整串当一个词 | The 2 QUICK Brown-Foxes | 等值/前缀匹配,本质同 keyword 字段 |
english | standard + 去停用词 + 词干 | fox run | 英文检索要"忽略变形"时 |
ik_smart / ik_max_word | 中文按词典切词 | 见 5.4 | 中文搜索必装插件 |
5.4 为什么中文必须上 IK
英文天然用空格和标点把词分开,standard 就够了。中文没有空格,用默认 standard 切中文会怎样?试试:
GET /_analyze
{
"analyzer": "standard",
"text": "我爱北京天安门"
}
预期输出:我 / 爱 / 北 / 京 / 天 / 安 / 门——一个字一个词!这样搜"北京",匹配到的是任何同时含"北""京"的文档,噪声极大,等于退化成了逐字包含。
装上 IK 分词插件后(本文环境的 ES 镜像里已装好),它按中文词典切出真正的词。IK 提供两种粒度:
# ik_max_word:切出最细、最全的词,宁多勿漏
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "中华人民共和国国歌"
}
# ik_smart:只切出最合理的粗粒度词,宁少勿滥
GET /_analyze
{
"analyzer": "ik_smart",
"text": "我爱北京天安门"
}
预期输出:
ik_max_word对"中华人民共和国国歌"产出:中华人民共和国 / 中华人民 / 中华 / 华人 / 人民共和国 / 人民 / 共和 / 共和国 / 国 / 国歌。ik_smart对"我爱北京天安门"产出:我 / 爱 / 北京 / 天安门。
两者对比,正是"查得全"与"查得准"的权衡:
| 模式 | 粒度 | 优点 | 缺点 | 建议用在哪 |
|---|---|---|---|---|
ik_max_word | 最细,尽量穷举所有组合 | 召回率高,长词也能被拆中 | 索引体积大、噪声略多 | 写入时(索引分词),保证不漏 |
ik_smart | 最合理、最少的粗词 | 精准、噪声小 | 可能漏掉偏门的子词 | 搜索时(查询分词),减少误报 |
5.5 建索引时给字段指定 analyzer
光装插件不会自动生效——必须在你自己的索引里给 text 字段显式声明 ik_max_word:
PUT /articles
{
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ik_max_word" },
"content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
"author": { "type": "keyword" }
}
}
}
三个关键认知:
- analyzer 只对
text生效。keyword、数字、日期不参与分词(keyword整个值就是一个词)。 - analyzer vs search_analyzer:
analyzer:写入时把文档切成哪些词、建倒排索引。search_analyzer:查询时把关键词切成哪些词;不写就默认跟随analyzer。- 上面配置的意思是:索引时用
ik_max_word切全(别漏),搜的时候用ik_smart切精(别噪)。
- 写进去的词和搜的词必须对得上。如果建索引用的是
ik_max_word,而查询时search_analyzer却悄悄被换成了别的分词器,两个倒排集合对不上,就会出现"明明有这篇文章却搜不到"。
验证一下整条链路:塞一篇文章进去,再用中文词搜。
# 写入
PUT /articles/_doc/1
{
"title": "北京欢迎你",
"content": "我爱北京天安门,天安门上太阳升。",
"author": "小北"
}
# 强制刷新后搜索(避免等默认 1s 刷新)
POST /articles/_refresh
# 全文检索:搜"北京"应命中
GET /articles/_search
{
"query": { "match": { "content": "北京" } }
}
# 用 author(keyword 字段)做精确匹配
GET /articles/_search
{
"query": { "term": { "author": "小北" } }
}
预期输出:两个搜索都能命中那篇文档——match 靠分词命中"北京",term 靠整词精确命中"小北"。再把 content 换成没声明 analyzer 的默认 text 重试,体会一下"逐字分词"和"词典分词"的差别。
5.6 自定义分词器:按需拼装流水线
内置的不够用?可以自由组装 char filter + tokenizer + filter 做成自己的 analyzer。注意:analysis 配置只能在创建索引时指定,建好就改不了。
PUT /blog_posts
{
"settings": {
"analysis": {
"filter": {
"my_stop": { "type": "stop", "stopwords": ["的", "了", "和", "是"] }
},
"analyzer": {
"my_cn_analyzer": {
"type": "custom",
"char_filter": ["html_strip"],
"tokenizer": "ik_max_word",
"filter": ["lowercase", "my_stop"]
}
}
}
},
"mappings": {
"properties": {
"body": { "type": "text", "analyzer": "my_cn_analyzer" }
}
}
}
# 自定义 analyzer 属于某个索引,要测它必须对着该索引调 _analyze
POST /blog_posts/_analyze
{
"analyzer": "my_cn_analyzer",
"text": "我的博客是关于的和是搜索引擎的"
}
预期输出:自定义分析器先去掉了 HTML 标签(char filter),用 ik_max_word 切词,再丢掉"的/了/和/是"这类虚词,剩下的都是实义词。在真实场景里,html_strip 专治"网页正文残留的标签噪声"。
要点:
- 排查"为什么搜不到"的第一动作永远是
_analyze:分别测一下写入的词和查询的词,对不上就是分词配置问题。 - 内置分词器直接给
analyzer赋值即可;想组合,就用settings.analysis定义 custom。 - IK 词典是可扩展的(改它的自定义词库配置,把业务专有名词加进去),加词之后才会被正确切分。
- 进阶还能叠 拼音、同义词(synonym)、edge_ngram(前缀补全) 等 filter——那是"中文搜索优化"的下一个篇章。
六、小结:一张表记住怎么选
| 问题 | 答案 |
|---|---|
| 什么时候上 ES? | 全文检索 / 模糊匹配 / 相关度排序 / 聚合分析;主数据别放这 |
字段什么时候用 text? | 内容要"搜词命中"时(标题、正文、简介) |
字段什么时候用 keyword? | 要整体精确匹配、排序、聚合时(标签、作者名、状态码) |
| 英文用什么 analyzer? | 默认 standard 就够;要忽略大小写变形用 english |
| 中文呢? | 装 IK:写入 ik_max_word(宁全),搜索 ik_smart(宁准) |
| 搜不到先查什么? | GET /<index>/_analyze 对同一句话分别测 analyzer 与 search_analyzer |
| 结构能随便改吗? | 不能:mapping 只能加字段,analysis 配置建索引前就要定好 |
从
LIKE全表扫,到倒排索引秒回;从逐字切分的中文,到 IK 词典级断词——ES 的一切魔法,最后都归结到"当年写入的那些词,和此刻查询的词,能不能在倒排索引上相遇"。学会用_analyze审视每一次分词,你就握住了调试全文搜索的钥匙。下一步建议顺序:聚合(Aggregation)报表 → IK 自定义词典 / 同义词 → pinyin 拼音搜索 → 高亮与 suggest 补全。每次只加一个能力,用
_analyze验证,地基就稳了。
本文所有请求在 Kibana Dev Tools(http://localhost:5601)中可直接执行复现;环境为 Docker 一键拉起的 ES 8.17 + Kibana 8.17(预装 IK 8.17)。
Leo
BloggerIndependent developer / Blogger and the maintainer of the original blog “大道至简”. Migrating years of posts and shiyu from WordPress to Next.js.
Reader comments
COMMENTS · 0Leave a comment