1. 了解 ES
1.1.1. Elasticsearch 的作用
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
1.1.2. ELK 技术栈
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:

而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

1.1.3. Elasticsearch 与 Lucene
elasticsearch底层是基于lucene来实现的。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:https://lucene.apache.org/ 。

elasticsearch的发展历史:
- 2004年Shay Banon基于Lucene开发了Compass
- 2010年Shay Banon 重写了Compass,取名为Elasticsearch。

1.1.4. 为什么不是其他搜索技术?
目前比较知名的搜索引擎技术排名:

虽然在早期,Apache Solr是最主要的搜索引擎技术,但随着发展elasticsearch已经渐渐超越了Solr,独占鳌头:

1.1.5. 总结
| 概念 | 说明 |
|---|---|
| Elasticsearch | 开源的分布式搜索引擎,可用于搜索、日志统计、分析、系统监控等场景 |
| Elastic Stack(ELK) | 以 Elasticsearch 为核心的技术栈,包括 Beats、Logstash、Kibana、Elasticsearch |
| Lucene | Apache 的开源搜索引擎类库,提供搜索引擎的核心 API |
1.2. 倒排索引
倒排索引的概念是基于 MySQL 这样的正向索引而言的。
1.2.1. 正向索引
那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:

如果是根据id查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
- 用户搜索数据,条件是
title符合"%手机%" - 逐行获取数据,比如
id为 1 的数据 - 判断数据中的
title是否符合用户搜索条件 - 如果符合则放入结果集,不符合则丢弃,回到步骤 1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
1.2.2. 倒排索引
倒排索引中有两个非常重要的概念:
- 文档(
Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息 - 词条(
Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引
如图:

倒排索引的搜索流程如下(以搜索"华为手机"为例):
- 用户输入条件
"华为手机"进行搜索。 - 对用户输入内容分词,得到词条:
华为、手机。 - 拿着词条在倒排索引中查找,可以得到包含词条的文档 id:1、2、3。
- 拿着文档 id 到正向索引中查找具体文档。
如图:

虽然要先查询倒排索引,再查询正向索引,但是无论是词条、还是文档 id 都建立了索引,查询速度非常快!无需全表扫描。
1.2.3. 正向索引与倒排索引的对比
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
是不是恰好反过来了?
那么两者方式的优缺点是什么呢?
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
1.3. ES 的核心概念
Elasticsearch 中有很多独有的概念,与 MySQL 中略有差别,但也有相似之处。
1.3.1. 文档与字段
elasticsearch是面向**文档(Document)**存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:

而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
1.3.2. 索引与映射
索引(Index),就是相同类型的文档的集合。
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;

因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
1.3.3. MySQL 与 Elasticsearch 对比
MySQL 与 Elasticsearch 的概念对比如下:
| MySQL | Elasticsearch | 说明 |
|---|---|---|
| Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
| Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
| Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
| Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
| SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
两者各有擅长的领域,并不能相互替代:
- MySQL:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性

1.4.3. 分词器小结
| 问题 | 说明 |
|---|---|
| 分词器的作用 | 创建倒排索引时对文档分词;用户搜索时对输入内容分词 |
| IK 分词器模式 | ik_smart(智能切分,粗粒度);ik_max_word(最细切分,细粒度) |
| 扩展 / 停用词条 | 在 config/IKAnalyzer.cfg.xml 中配置扩展词典和停用词典,并在词典文件中添加对应词条 |
2. 索引库操作
索引库类似数据库表,mapping 映射类似表结构。向 ES 中存储数据,必须先创建"库"和"表"。
2.1. mapping 映射属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
例如下面的json文档:
1 | { |
对应的每个字段映射(mapping):
- age:类型为 integer;参与搜索,因此需要index为true;无需分词器
- weight:类型为float;参与搜索,因此需要index为true;无需分词器
- isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
- info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
- email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
- score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
- name:类型为object,需要定义多个子属性
- name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
- name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
store 与 index 的区别
如果对某个field做了索引,则可以查询。如果store:yes,则可以展示该field的值。
但是如果你存储了这个doc的数据(_source enable),即使store为no,仍然可以得到field的值(client去解析)。
所以一个store设置为no 的field,如果_source被disable,则只能检索不能展示。
可以指定一些字段store为true,这意味着这个field的数据将会被单独存储。这时候,如果你要求返回field1(store:yes),es会分辨出field1已经被存储了,因此不会从_source中加载,而是从field1的存储块中加载。
哪些情形下需要显式的指定store属性呢?大多数情况并不是必须的。从_source中获取值是快速而且高效的。如果文档长度很长,存储 _source或者从_source中获取field的代价很大,可以显式的将某些field的store属性设置为yes。缺点如上边所说:假设存 储了10个field,而如果想获取这10个field的值,则需要多次的io,如果从_source中获取则只需要一次,而且_source是被压缩过 的。
还有一种情形:reindex from some field,对某些字段重建索引的时候。从source中读取数据然后reindex,和从某些field中读取数据相比,显然后者代价更低一些。这些字段store设置为yes比较合适。
2.2. 索引库的 CRUD
这里统一使用 Kibana 编写 DSL 的方式来演示。
2.2.1. 创建索引库和映射
基本语法
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping映射
格式:
1 | PUT /索引库名称 |
示例
1 | PUT /heima |
2.2.2. 查询索引库
基本语法:
请求方式:GET
请求路径:/索引库名
请求参数:无
格式:
1 | GET /索引库名 |
示例:

2.2.3. 修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法说明:
1 | PUT /索引库名/_mapping |
示例:

2.2.4. 删除索引库
语法:
请求方式:DELETE
请求路径:/索引库名
请求参数:无
格式:
1 | DELETE /索引库名 |
在kibana中测试:

2.2.5. 索引库操作小结
| 操作 | 语法 |
|---|---|
| 创建索引库 | PUT /索引库名 |
| 查询索引库 | GET /索引库名 |
| 删除索引库 | DELETE /索引库名 |
| 添加字段 | PUT /索引库名/_mapping |
3. 文档操作
3.1. 新增文档
语法:
1 | POST /索引库名/_doc/文档id |
示例:
1 | POST /heima/_doc/1 |
响应:

3.2. 查询文档
根据 REST 风格,新增是 POST,查询应该是 GET,不过查询一般需要带上文档 id。
语法:
1 | GET /{索引库名称}/_doc/{id} |
通过 Kibana 查看数据:
1 | GET /heima/_doc/1 |
查看结果:

3.3. 删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
语法:
1 | DELETE /{索引库名}/_doc/id值 |
示例:
1 | # 根据id删除数据 |
结果:

3.4. 修改文档
修改有两种方式:
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
3.4.1. 全量修改
全量修改是覆盖原来的文档,其本质是:
- 根据指定的id删除文档
- 新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
1 | PUT /{索引库名}/_doc/文档id |
示例:
1 | PUT /heima/_doc/1 |
3.4.2. 增量修改
增量修改是只修改指定id匹配的文档中的部分字段。
语法:
1 | POST /{索引库名}/_update/文档id |
示例:
1 | POST /heima/_update/1 |
3.4.3. 批量操作
bulk 操作类型说明
| 操作 | 说明 |
|---|---|
create | 文档不存在则创建,存在则返回错误 |
index | 文档不存在则创建,存在则更新 |
update | 更新文档,不存在则返回错误 |
delete | 删除文档,不存在则返回错误 |
bulk的操作,某一个操作失败,是不会影响其他文档的操作的,它会在返回结果中告诉你失败的详细的原因。
bulk常用示例
- 测试数据准备
索引及映射结构准备
1 | PUT example |
- 批量插入
插入的action为index。
示例语法
1 | POST example/_doc/_bulk |
- 批量修改
修改的action为update。
示例语法
1 | POST example/_doc/_bulk |
由上面示例可以看出,批量更新支持参数选项:
doc(部分文档)、upsert、doc_as_upsert、脚本、params(用于脚本)、lang(用于脚本)和_source。
- 批量删除
修改的action为delete。
示例如下。
1 | POST example/_doc/_bulk |
- 批量的混合操作
不过一般不推荐这种使用,项目中也用的极少。
1 | POST _bulk |
3.5. 文档操作小结
| 操作 | 语法 |
|---|---|
| 创建文档 | POST /{索引库名}/_doc/文档id { json文档 } |
| 查询文档 | GET /{索引库名}/_doc/文档id |
| 删除文档 | DELETE /{索引库名}/_doc/文档id |
| 全量修改 | PUT /{索引库名}/_doc/文档id { json文档 } |
| 增量修改 | POST /{索引库名}/_update/文档id { "doc": {字段} } |
| 批量操作 | POST _bulk |
4. RestAPI
ES 官方提供了各种不同语言的客户端,用来操作 ES。这些客户端的本质就是组装 DSL 语句,通过 HTTP 请求发送给 ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的 Java Rest Client 包括两种:
- Java Low Level Rest Client
- Java High Level Rest Client

我们学习的是Java HighLevel Rest Client客户端API
4.0.1. mapping 映射分析
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
- 字段名
- 字段数据类型
- 是否参与搜索
- 是否需要分词
- 如果分词,分词器是什么?
其中:
- 字段名、字段数据类型,可以参考数据表结构的名称和类型
- 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索
- 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词
- 分词器,我们可以统一使用ik_max_word
来看下酒店数据的索引库结构:
1 | PUT /hotel |
几个特殊字段说明:
- location:地理坐标,里面包含精度、纬度
- all:一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索
地理坐标说明:

copy_to说明:

4.0.2. 初始化 RestClient
在 Elasticsearch 提供的 API 中,与 ES 的所有交互都封装在 RestHighLevelClient 类中,必须先完成该对象的初始化,建立与 Elasticsearch 的连接。
分为三步:
- 引入 ES 的
RestHighLevelClient依赖:
1 | <dependency> |
- 因为 SpringBoot 默认的 ES 版本是 7.6.2,需要覆盖默认版本:
1 | <properties> |
- 初始化
RestHighLevelClient:
1 | RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( |
为了单元测试方便,创建测试类 HotelIndexTest,将初始化代码写在 @BeforeEach 方法中:
1 | package cn.itcast.hotel; |
4.1. 创建索引库
4.1.1. 代码解读
创建索引库的 API 如下:

代码分为三步:
- 创建 Request 对象。创建索引库时使用
CreateIndexRequest。 - 添加请求参数,即 DSL 的 JSON 参数部分。JSON 字符串较长,这里定义了静态字符串常量
MAPPING_TEMPLATE。 - 发送请求,
client.indices()方法返回IndicesClient类型,封装了所有索引库相关操作方法。
4.1.2. 完整示例
在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:
1 | package cn.itcast.hotel.constants; |
在 HotelIndexTest 测试类中,编写单元测试实现创建索引:
1 |
|
4.2. 删除索引库
删除索引库的 DSL 语句如下:
1 | DELETE /hotel |
与创建索引库相比:
- 请求方式从
PUT变为DELETE - 请求路径不变
- 无请求参数
代码差异体现在 Request 对象上,依然是三步走:
- 创建 Request 对象,这次是
DeleteIndexRequest - 准备参数(无参)
- 发送请求,改用
delete方法
在 HotelIndexTest 测试类中,编写单元测试实现删除索引:
1 |
|
4.3. 判断索引库是否存在
判断索引库是否存在,本质就是查询,对应的 DSL 是:
1 | GET /hotel |
与删除的 Java 代码流程类似,依然是三步走:
- 创建 Request 对象,这次是
GetIndexRequest - 准备参数(无参)
- 发送请求,改用
exists方法
1 |
|
4.4. 小结
JavaRestClient 操作 Elasticsearch 的流程基本一致,核心是通过 client.indices() 获取索引库操作对象。
索引库操作的基本步骤:
- 初始化
RestHighLevelClient - 创建
XxxIndexRequest(Xxx为Create、Get或Delete) - 准备 DSL(仅 Create 时需要,其它无参)
- 发送请求,调用
client.indices().xxx()方法(xxx为create、exists或delete)
5. RestClient 操作文档
为了与索引库操作分离,创建一个新的测试类,做两件事情:
- 初始化
RestHighLevelClient - 酒店数据来自数据库,需要注入
IHotelService接口
1 | package cn.itcast.hotel; |
5.1. 新增文档
将数据库的酒店数据查询出来,写入 Elasticsearch。
5.1.1. 索引库实体类
数据库查询后的结果是一个Hotel类型的对象。结构如下:
1 |
|
与索引库结构存在差异:longitude 和 latitude 需要合并为 location。因此定义新类型 HotelDoc 与索引库结构对齐:
1 | package cn.itcast.hotel.pojo; |
5.1.2. 语法说明
新增文档的DSL语句如下:
1 | POST /{索引库名}/_doc/1 |
对应的 Java 代码如图:

与创建索引库类似,同样是三步走:
- 创建 Request 对象
- 准备请求参数(DSL 中的 JSON 文档)
- 发送请求
与索引库操作的区别在于,这里直接使用 client.xxx(),无需 client.indices()。
5.1.3. 完整代码
导入酒店数据的整体步骤:
- 根据
id查询酒店数据Hotel - 将
Hotel封装为HotelDoc - 将
HotelDoc序列化为 JSON - 创建
IndexRequest,指定索引库名和id - 准备请求参数(JSON 文档)
- 发送请求
在 HotelDocumentTest 测试类中编写单元测试:
1 |
|
5.2. 查询文档
5.2.1. 语法说明
查询的DSL语句如下:
1 | GET /hotel/_doc/{id} |
非常简单,因此代码大概分两步:
- 准备Request对象
- 发送请求
不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

结果是一个 JSON,其中文档放在 _source 属性中,解析时取出 _source 反序列化为 Java 对象即可。
同样是三步走:
- 准备 Request 对象(查询使用
GetRequest) - 发送请求,调用
client.get()方法 - 解析结果,对 JSON 做反序列化
5.2.2. 完整代码
在 HotelDocumentTest 测试类中编写单元测试:
1 |
|
5.3. 删除文档
删除的 DSL 如下:
1 | DELETE /hotel/_doc/{id} |
与查询相比,请求方式从 GET 变为 DELETE,Java 代码同样是三步走:
- 准备
DeleteRequest对象,指定索引库名和id - 准备参数(无参)
- 发送请求,调用
client.delete()方法
在 HotelDocumentTest 测试类中编写单元测试:
1 |
|
5.4. 修改文档
5.4.1. 语法说明
修改有两种方式:
- 全量修改:本质是先根据
id删除,再新增 - 增量修改:修改文档中的指定字段值
在 RestClient 的 API 中,全量修改与新增的 API 完全一致,判断依据是 ID:若 ID 已存在则修改,若不存在则新增。这里重点关注增量修改。
代码示例如图:

同样是三步走:
- 准备
UpdateRequest对象 - 准备参数(包含要修改字段的 JSON 文档)
- 调用
client.update()发送请求
5.4.2. 完整代码
在 HotelDocumentTest 测试类中编写单元测试:
1 |
|
5.5. 批量导入文档
利用 BulkRequest 批量将数据库数据导入索引库,步骤如下:
- 利用 mybatis-plus 查询酒店数据
- 将
Hotel转换为文档类型HotelDoc - 利用
BulkRequest批处理,实现批量新增文档
5.5.1. 语法说明
BulkRequest 的本质是将多个普通 CRUD 请求组合在一起发送,通过 add 方法添加子请求:

可以添加的请求类型:
IndexRequest(新增)UpdateRequest(修改)DeleteRequest(删除)
添加多个 IndexRequest 即实现批量新增:

同样是三步走:
- 创建
BulkRequest对象 - 准备参数(多个
IndexRequest对象) - 调用
client.bulk()发送请求
5.5.2. 完整代码
在 HotelDocumentTest 测试类中编写单元测试:
1 |
|
5.6. 小结
文档操作的基本步骤:
- 初始化
RestHighLevelClient - 创建
XxxRequest(Xxx为Index、Get、Update、Delete或Bulk) - 准备参数(
Index、Update、Bulk时需要) - 发送请求,调用
RestHighLevelClient#xxx()方法 - 解析结果(
Get时需要)