✦ 大道至简 · 时光是画在卷上的河流 · 行到水穷处,坐看云起时

elasticsearch2026.09.03 · 32 分钟阅读

Elasticsearch + Kibana 增删改查

用 Kibana Dev Tools 手把手跑通 ES 的索引/文档增删改查,并从分词器原理出发讲透为什么中文检索必须上 IK

L

Leo

2026.09.03 · 更新于 2026.09.14

4 次浏览
Elasticsearch + Kibana 增删改查

Elasticsearch + Kibana 增删改查与中文分词全解

给 MySQL 里的一百万条商品数据做模糊搜索,LIKE '%xxx%' 会全表扫描、慢到怀疑人生,还谈不上"相关度排序"。 Elasticsearch(ES)就是为了全文搜索而生的引擎——它靠一套叫"倒排索引"的数据结构,把"查词"变成"翻字典"一样快,再配合 Kibana 这个图形控制台,CRUD、调分词、看索引状态全部点点鼠标就能跑通。

本文基于 ES 8.17 单机版 + Kibana 8.17(已装 IK 中文分词插件),所有示例都写在 Kibana Dev Tools 里,打开就能复现。每段都会给你预期输出,实际跑一下对照即可。


为什么:LIKE 解决不了的事,才轮到 ES

先别急着敲代码,想清楚"ES 解决了什么",后面每个概念都顺理成章。

LIKE '%手机%' 有三个先天不足:

  1. 慢:无法走索引,必须逐行扫描。
  2. 笨:只能按"字符挨个包含"匹配,搜 手机壳 的关键词 手机 想命中"智能机型配件"?没门——因为它不认识"词"。
  3. 不会排序:结果按行号返回,没有"哪条更相关"的概念。

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 字段
englishstandard + 去停用词 + 词干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" }
    }
  }
}

三个关键认知:

  1. analyzer 只对 text 生效。keyword、数字、日期不参与分词(keyword 整个值就是一个词)。
  2. analyzer vs search_analyzer:
    • analyzer:写入时把文档切成哪些词、建倒排索引。
    • search_analyzer:查询时把关键词切成哪些词;不写就默认跟随 analyzer。
    • 上面配置的意思是:索引时用 ik_max_word 切全(别漏),搜的时候用 ik_smart 切精(别噪)。
  3. 写进去的词和搜的词必须对得上。如果建索引用的是 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)。

标签 / TAGSelasticsearch
L

Leo

博主

独立开发者 / Blogger,原博客「大道至简」维护者。正在把 WordPress 上攒了几年的文章与拾语迁移到 Next.js。

读者留言

COMMENTS · 0

发表留言

评论经审核后展示 · 请友善发言0/100