Elasticsearch 入门教程:核心原理、Docker 部署与中文搜索实战

做全文搜索功能,折腾了一圈 Elasticsearch,从装环境到调分词再到上线同步方案,踩了不少坑。这份 Elasticsearch 入门教程整理了从零到跑通的全部经历,希望能帮你少走弯路。点个收藏,我们开始。

一、Elasticsearch 是什么:不只是"搜索引擎"

Elasticsearch(简称 ES) 是一个开源的分布式搜索与分析引擎,基于 Apache Lucene 构建。它存数据,但真正擅长的是对海量文本做近实时的全文检索——你可以把它理解为"给数据库装上了百度搜索"。

如果把 ES 和 MySQL 做一层概念映射,对应关系大致如下:

MySQLElasticsearch说明
DatabaseCluster一个 ES 集群对应一个数据库实例
TableIndex(索引)数据存储的逻辑单元
RowDocument(文档)最小数据单位,JSON 格式
ColumnField(字段)文档中的属性
SchemaMapping(映射)定义字段类型和索引方式

ES 7.x 之后,Type 的概念被废弃,一个 Index 下默认只保留 _doc 类型,架构比 6.x 清爽了不少。

ES 有几个让我觉得"真香"的特性:分布式架构天生支持横向扩展、数据写入后近实时可搜(默认 1 秒 refresh)、RESTful API 让任何语言都能调用、插件化设计可以按需扩展。这几个特性叠加在一起,让 ES 成了全文搜索领域的事实标准。

Elasticsearch 与 MySQL 概念对比示意图,Elasticsearch 入门教程核心概念:左侧 MySQL 术语,右侧对应 ES 术语

二、倒排索引:为什么 ES 搜得这么快

讲 ES 绕不开倒排索引——它是全文搜索能力的根基,不理解它,后面的分词器和查询优化都只能死记硬背。

2.1 什么是倒排索引

传统的"正排索引"是文档 → 关键词的映射,就像翻到一本书的某页去看这页写了什么。而倒排索引(Inverted Index) 反其道而行之,是关键词 → 文档的映射——就像书末的索引页,查"分布式"三个字,直接告诉你出现在哪几页。

举个例子,假设有 3 篇文档:

  • 文档 1:Elasticsearch 是分布式引擎
  • 文档 2:分布式系统的核心是搜索
  • 文档 3:搜索引擎的未来是智能化

经过分词和归一化处理后,倒排索引长这样:

关键词文档 ID
elasticsearch1
分布式1, 2
系统2
搜索2, 3
引擎1, 3

当用户搜索"引擎"时,ES 直接查表定位到文档 1 和 3,不需要扫描全部文档——时间复杂度从 O(n) 降到了 O(1)。

2.2 倒排索引的构建过程

倒排索引构建三步流程,Elasticsearch 入门教程必知:分词 → 归一化 → 创建倒排列表

第一步:分词(Tokenization)。把原始文本拆成独立的词条。比如"Elasticsearch支持全文检索"拆成 [elasticsearch, 支持, 全文, 检索]

第二步:归一化(Normalization)。对分词结果做标准化——转小写(Elasticsearch → elasticsearch)、去掉"的""是"这类停用词、做词干提取(running → run)。这一步直接决定了后续搜索的召回质量。

第三步:创建倒排列表。为每个词项建立到文档 ID 的映射关系,持久化存储。最终结构类似 {"elasticsearch": [1], "分布式": [1, 2], ...}

2.3 用户搜索时发生了什么

搜索执行流程:用户输入 → 解析查询词并分词 → 查倒排索引 → 合并结果集 → 相关性排序 → 返回

当用户输入"分布式搜索"时,ES 的执行链路是这样的:查询词同样被分词(分布式 + 搜索)→ 分别查倒排索引(分布式 → [1,2],搜索 → [2,3])→ 按 AND/OR 逻辑合并结果 → 用 TF-IDF 或 BM25 算法算相关性评分并排序。整个链路在毫秒级完成,这就是倒排索引的威力。

三、分词器:搜索质量的命门

倒排索引的质量,很大程度上取决于分词器(Analyzer)。分词器决定了文本怎么拆、怎么存、怎么搜——它是在字符写入索引之前必须要经过的"安检通道"。

ES 的 Analyzer 由三个组件串联而成:

分词器三大组件处理流程:字符过滤器预处理 → 分词器切分文本 → 词项过滤器后处理

  1. 字符过滤器(Character Filters):预处理,比如去掉 HTML 标签、替换特殊字符(如把 & 转成 and
  2. 分词器(Tokenizer):将文本按规则切分成一个个词项,这是整个流程中唯一必须的组件
  3. 词项过滤器(Token Filters):对词项做后处理——转小写、去停用词、词干提取等

ES 内置了多种分词器,我整理了一张场景对照表:

分词器分词方式中文效果适用场景
standard按词边界切分,转小写逐字拆分(差)英文通用场景
simple按非字母字符切分逐字拆分简单英文文本
whitespace仅按空格切分不分词大小写敏感场景
keyword不分词,整个输入作为单个词项不分词精确匹配(ID、状态码)
stopstandard + 过滤停用词逐字拆分英文长文搜索

注意到没? 上面所有内置分词器对中文的处理都是"逐字拆分"——standard 会把"秋季穿搭指南"切成"秋、季、穿、搭、指、南"六个单字。这意味着搜"秋季"时,term 查询可能颗粒无收(后面第六章会详细演示这个坑)。这就是为什么必须装 IK 分词器。

四、Docker 部署 ES + Kibana:十分钟跑起本地环境

开发环境用 Docker 跑 ES 最省心——一行命令启动,不想要了直接 docker rm -f 清掉。以下基于 ES 7.3.0(2026 年 7 月实测可用)。

4.1 拉取镜像并单节点启动

# 拉取镜像
docker pull elasticsearch:7.3.0

# 单节点启动
docker run -d --name es7 \
  -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e ES_JAVA_OPTS="-Xms1024m -Xmx1024m" \
  elasticsearch:7.3.0
  • 9200:REST API 端口,外部调用走这个
  • 9300:集群内部节点间通信端口,单节点用不到但建议暴露
  • discovery.type=single-node:跳过主节点选举,单机开发必备
  • ES_JAVA_OPTS:设 JVM 堆内存为 1GB,建议不超过宿主机物理内存的一半

浏览器访问 http://localhost:9200,返回 JSON 格式的集群信息就说明跑起来了。

4.2 挂载配置与数据,防止容器删除数据丢失

容器一删数据就没了,必须把关键目录挂载出来:

docker cp es7:/usr/share/elasticsearch/config E:\docker\elasticsearch
docker cp es7:/usr/share/elasticsearch/data E:\docker\elasticsearch
docker cp es7:/usr/share/elasticsearch/plugins E:\docker\elasticsearch

编辑 config/elasticsearch.yml,加上跨域配置:

cluster.name: "my-es-cluster"
network.host: 0.0.0.0
http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-headers: Authorization,X-Requested-With,Content-Length,Content-Type

删掉旧容器,重新用带 -v 挂载的参数启动:

docker rm -f es7

docker run -d --name es7 \
  -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e ES_JAVA_OPTS="-Xms1024m -Xmx1024m" \
  -v E:\docker\elasticsearch\config:/usr/share/elasticsearch/config \
  -v E:\docker\elasticsearch\data:/usr/share/elasticsearch/data \
  -v E:\docker\elasticsearch\plugins:/usr/share/elasticsearch/plugins \
  elasticsearch:7.3.0

4.3 搭上 Kibana

Kibana 是 ES 官方的可视化和分析工具,它自带的 Dev Tools 是最好的 ES 查询练习环境。版本号必须和 ES 保持一致,否则有兼容性问题:

docker pull kibana:7.3.0
docker run -d --name kibana -p 5601:5601 kibana:7.3.0

同样把配置文件复制出来,编辑 kibana.yml

server.name: kibana
server.host: "0"
elasticsearch.hosts: ["http://172.17.0.1:9200"]  # 宿主机 IP,不是 localhost
i18n.locale: "zh-CN"

注意:Kibana 在容器内访问 ES 时,localhost 指向的是容器自身,必须用宿主机的实际 IP。Windows 下通常为 172.17.0.1(用 docker inspect es7 可以确认)。

docker rm -f kibana
docker run -d --name kibana \
  -p 5601:5601 \
  -v E:\docker\kibana\config:/usr/share/kibana/config \
  kibana:7.3.0

访问 localhost:5601,看到中文界面的 Kibana 控制台就成功了。左侧导航栏找到 Dev Tools 入口,后面所有查询都在这里写。

五、IK 中文分词器:让 ES 真正认识中文

最早用默认分词器建索引,上线后发现中文搜索效果很差——搜"美食"出来一堆不相关的结果。排查后才发现,"美食"被 standard 分词器拆成了"美"和"食"两个单字,匹配逻辑全乱了。

IK 分词器(IK Analyzer) 是专为中文设计的 Lucene 分词插件,通过内置词典识别中文词组。你可以理解为"给 ES 装了一本现代汉语词典"——它知道"美食"是一个完整的词,而不是"美"+“食”。

5.1 安装与持久化

# 进入 ES 容器
docker exec -it es7 /bin/sh

# 安装 IK(版本号与 ES 一致)
bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.3.0

安装时提示确认,输入 y 回车。看到 -> Installed analysis-ik 就装好了。

我踩过的坑:插件安装在容器内部,容器一删就没了。安装完一定要把插件文件复制到宿主机挂载目录:

docker cp es7:/usr/share/elasticsearch/plugins/analysis-ik E:\docker\elasticsearch\plugins
docker cp es7:/usr/share/elasticsearch/config/analysis-ik E:\docker\elasticsearch\config

这样配合第四章的 -v 挂载,插件就能持久化保留了。重启后查日志确认:

docker restart es7
docker logs -f es7 2>&1 | grep analysis-ik

看到 loaded plugin [analysis-ik] 就说明插件已生效。

5.2 ik_smart vs ik_max_word

IK 提供两种模式,选择困难症友好——记住一句话:建索引用 ik_max_word(细粒度),搜索时用 ik_smart(智能分词)

模式策略输入"深度学习框架使用指南"的产出适用场景
ik_smart智能粗粒度[深度学习, 框架, 使用指南]搜索查询时使用
ik_max_word最细粒度穷举[深度学习, 深度, 学习, 框架, 使用, 指南]建索引时使用

_analyze API 实际验证一下,左边 IK、右边 standard,差距一目了然:

# IK 智能分词
POST /_analyze
{
  "analyzer": "ik_smart",
  "text": "这个技术博客写得怎么样?"
}
# 结果:这 / 个 / 技术 / 博客 / 写 / 得 / 怎么样

# 默认 standard 分词
POST /_analyze
{
  "analyzer": "standard",
  "text": "这个技术博客写得怎么样?"
}
# 结果:这 / 个 / 技 / 术 / 博 / 客 / 写 / 得 / 怎 / 么 / 样

IK 能识别"技术""博客"等中文词组,而 standard 只会逐字拆分——这在生产环境中意味着完全不同的搜索体验。

六、ES 基础查询语法:从增删改查到聚合搜索

理论都讲完了,下面该写查询了。

6.1 结构化建索引

先建一个笔记索引,指定每个字段的分词器:

PUT /note
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "title": { "type": "text", "analyzer": "ik_max_word" },
      "content": { "type": "text", "analyzer": "ik_max_word" },
      "create_time": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss"
      }
    }
  }
}

Mapping(映射) 相当于 MySQL 的建表语句——定义每个字段的类型和索引方式。这里 titlecontent 都用 ik_max_word,意味着写入时会按最细粒度分词,提高搜索召回率。

插入几条测试数据:

PUT /note/_doc/1
{ "title": "夏日旅游攻略", "content": "去海边玩耍的必备攻略,推荐三亚和青岛。", "create_time": "2024-08-16 16:49:35" }

PUT /note/_doc/2
{ "title": "美食笔记", "content": "推荐几家成都的火锅店,真的很辣很过瘾!", "create_time": "2024-11-02 17:00:36" }

PUT /note/_doc/3
{ "title": "秋季穿搭指南", "content": "分享适合秋天的服装搭配技巧,风衣必不可少。", "create_time": "2024-11-03 16:59:58" }

6.2 增删改查速览

几个常用操作,一张代码块看完:

# 全量更新(文档存在则覆盖,不存在则插入)
PUT /note/_doc/1
{ "title": "更新后的标题", "content": "新内容" }

# 只更新指定字段(推荐)
POST /note/_update/1
{ "doc": { "title": "只更新标题" } }

# 仅创建,已存在则报错
PUT /note/_create/4
{ "title": "新文档", "content": "测试" }

# 删除
DELETE /note/_doc/1

# 查看单条
GET /note/_doc/2

# 搜索全部
GET /note/_search

6.3 搜索:match vs term,一个常踩的坑

全文搜索用 match——搜索词会先被分词再匹配:

GET /note/_search
{
  "query": { "match": { "title": "穿搭" } }
}
# "穿搭"分词后匹配 → 命中文档 3

精确匹配用 term——搜索词不经过分词,直接去索引里比对:

GET /note/_search
{
  "query": {
    "bool": {
      "filter": { "term": { "title": "秋季" } }
    }
  }
}

可能有人会问:用 term 搜"秋季",文档 3 标题明明包含"秋季穿搭指南",为什么一条结果都没有?

在项目中真遇到过的问题。原因出在分词环节:文档入库时 title 用了 ik_max_word 分词,“秋季穿搭指南"被拆成了多个词项。但 term 查询不做分词,直接用"秋季"这个完整字符串去索引里找——索引里存的是被 IK 拆散后的词项,很可能不包含完整的"秋季”。

换成 match 就能搜到,因为 match 会先把"秋季"分词后再分别匹配索引。一句话概括:match 分词后搜索(覆盖面广),term 原样匹配(精准但可能漏)。

6.4 分页、排序、聚合

# 分页:from = (page-1) * size
GET /note/_search
{
  "query": { "match_all": {} },
  "from": 0, "size": 2
}

# 按发布时间倒序
GET /note/_search
{
  "query": { "match_all": {} },
  "sort": [{ "create_time": { "order": "desc" } }]
}

# 聚合:按发布时间分组统计文档数(类似 SQL 的 GROUP BY)
GET /note/_search
{
  "query": { "match_all": {} },
  "aggs": {
    "group_by_create_time": {
      "terms": { "field": "create_time" }
    }
  }
}

补充一点:上面 term 查询用 filter 而非 must,是因为 filter 不计算相关性评分 _score,性能更好——适合"状态等于已发布""价格在 100-200 之间"这类纯过滤场景。

七、数据增量同步:怎么把 MySQL 的数据搬到 ES

生产环境中,MySQL 通常作为主存储,ES 作为搜索的从库。如何把 MySQL 的变更实时同步到 ES?业内主要两条路。

7.1 方案一:MQ 通知

MQ 同步方案架构图:业务服务写 MySQL 后发 MQ 消息 → 搜索服务消费消息 → 更新 ES 索引

业务服务在写 MySQL 的事务内,同时发一条 MQ 消息。搜索服务作为消费者监听消息,收到后更新 ES 索引。

这个方案的优势是灵活——消息体可以只携带变更字段,减少数据传输;MQ 自带持久化能力,搜索服务挂了消息不会丢。但代价是侵入性强——每个涉及搜索的业务点都要写 MQ 发送逻辑,团队大、服务多时维护成本不低。另外"数据库写成功但 MQ 发送失败"这种分布式事务问题也需要补偿机制兜底。

7.2 方案二:Canal

Canal 同步方案架构图:MySQL 产生 binlog → Canal 伪装从库订阅 binlog → 解析变更事件 → 推送至 ES

Canal 是阿里巴巴开源的一个 MySQL binlog 增量订阅中间件。它的原理很巧妙——把自己伪装成 MySQL 的从库,监听主库的 binlog,解析出 INSERT/UPDATE/DELETE 事件后推送给下游消费者。你可以理解为"给 MySQL 接了一个监听器,数据一变它就通知你"。

Canal 最大的卖点是零侵入——业务代码一行不用改,直接订阅数据库日志就行。一致性也天然优于 MQ 方案,因为数据来源就是 binlog。但代价是运维成本高(Canal Server 需要独立部署和高可用),而且它只能感知到数据库层面的变更——如果你的同步逻辑涉及 Redis 缓存或外部 API 的数据加工,Canal 就无能为力了。

7.3 选型对比

对比维度MQ 通知方式Canal
实时性较高(受消费速率影响)近乎实时
数据一致性需要事务消息或补偿机制与数据库强一致
业务侵入性高,需在业务代码中埋点零侵入
开发成本需开发生产 + 消费两端逻辑需开发 binlog 解析与格式转换
运维成本中等(MQ 需高可用部署)较高(Canal Server 需独立部署与维护)
适用场景多数据源、复杂业务逻辑单一 MySQL 数据源、简单同步逻辑

可能有人会问:两种方案到底怎么选?

我的建议很简单:如果你只有一个 MySQL 数据源,同步逻辑就是"MySQL 多了/改了什么,ES 就跟着改",直接上 Canal,省心。但如果数据源不止 MySQL(比如还有 MongoDB、外部 API),或者同步前需要做字段拼装、多表关联等业务加工,那就选 MQ 方案——灵活性更高。我个人两个方案都在项目里用过:早期团队小、业务简单时用 Canal 确实省了不少事;后来数据源多了、同步逻辑复杂了,才切到 MQ。所以这不是"谁更好"的问题,而是什么阶段适合什么方案。


从倒排索引原理到 Docker 部署,从分词器到查询语法,再到数据同步方案,这篇 Elasticsearch 入门教程帮你把入门的坑都踩了一遍。这些内容在实际项目中够用了——剩下的高级功能(高亮搜索、Suggest 建议器、嵌套聚合等)建议在用到的场景下查官方文档,记忆效率更高。

系列下一篇计划写 Spring Boot 如何集成 Elasticsearch,覆盖 RestHighLevelClient 和 Spring Data Elasticsearch 两种方式,感兴趣的可以关注。

参考链接


如果对你有帮助,欢迎点赞、收藏、转发。

更多推荐