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
LuceneApache 的开源搜索引擎类库,提供搜索引擎的核心 API

1.2. 倒排索引

倒排索引的概念是基于 MySQL 这样的正向索引而言的。

1.2.1. 正向索引

那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:

如果是根据id查询,那么直接走索引,查询速度非常快。

但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:

  1. 用户搜索数据,条件是 title 符合 "%手机%"
  2. 逐行获取数据,比如 id 为 1 的数据
  3. 判断数据中的 title 是否符合用户搜索条件
  4. 如果符合则放入结果集,不符合则丢弃,回到步骤 1

逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

1.2.2. 倒排索引

倒排索引中有两个非常重要的概念:

  • 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
  • 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

创建倒排索引是对正向索引的一种特殊处理,流程如下:

  • 将每一个文档的数据利用算法分词,得到一个个词条
  • 创建表,每行数据包括词条、词条所在文档id、位置等信息
  • 因为词条唯一性,可以给词条创建索引,例如hash表结构索引

如图:

倒排索引的搜索流程如下(以搜索"华为手机"为例):

  1. 用户输入条件 "华为手机" 进行搜索。
  2. 对用户输入内容分词,得到词条:华为手机
  3. 拿着词条在倒排索引中查找,可以得到包含词条的文档 id:1、2、3。
  4. 拿着文档 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 的概念对比如下:

MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是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
2
3
4
5
6
7
8
9
10
11
12
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",
"lastName": "赵"
}
}

对应的每个字段映射(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;无需分词器

storeindex 的区别

如果对某个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PUT /heima
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": "falsae"
},
"name":{
"properties": {
"firstName": {
"type": "keyword"
}
}
},
// ... 略
}
}
}

2.2.2. 查询索引库

基本语法

  • 请求方式:GET

  • 请求路径:/索引库名

  • 请求参数:无

格式

1
GET /索引库名

示例

2.2.3. 修改索引库

倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping

虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。

语法说明

1
2
3
4
5
6
7
8
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}

示例

2.2.4. 删除索引库

语法:

  • 请求方式:DELETE

  • 请求路径:/索引库名

  • 请求参数:无

格式:

1
DELETE /索引库名

在kibana中测试:

2.2.5. 索引库操作小结

操作语法
创建索引库PUT /索引库名
查询索引库GET /索引库名
删除索引库DELETE /索引库名
添加字段PUT /索引库名/_mapping

3. 文档操作

3.1. 新增文档

语法:

1
2
3
4
5
6
7
8
9
10
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}

示例:

1
2
3
4
5
6
7
8
9
POST /heima/_doc/1
{
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}

响应:

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
2
# 根据id删除数据
DELETE /heima/_doc/1

结果:

3.4. 修改文档

修改有两种方式:

  • 全量修改:直接覆盖原来的文档
  • 增量修改:修改文档中的部分字段

3.4.1. 全量修改

全量修改是覆盖原来的文档,其本质是:

  • 根据指定的id删除文档
  • 新增一个相同id的文档

注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

语法:

1
2
3
4
5
6
7
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}

示例:

1
2
3
4
5
6
7
8
9
PUT /heima/_doc/1
{
"info": "黑马程序员高级Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}

3.4.2. 增量修改

增量修改是只修改指定id匹配的文档中的部分字段。

语法:

1
2
3
4
5
6
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}

示例:

1
2
3
4
5
6
POST /heima/_update/1
{
"doc": {
"email": "ZhaoYun@itcast.cn"
}
}

3.4.3. 批量操作

bulk 操作类型说明

操作说明
create文档不存在则创建,存在则返回错误
index文档不存在则创建,存在则更新
update更新文档,不存在则返回错误
delete删除文档,不存在则返回错误

bulk的操作,某一个操作失败,是不会影响其他文档的操作的,它会在返回结果中告诉你失败的详细的原因。

bulk常用示例

  • 测试数据准备

索引及映射结构准备

1
2
3
4
5
6
7
8
9
10
PUT example
PUT example/_doc/_mapping
{
"properties": {
"id": {"type": "long"},
"name": {"type": "text"},
"counter": {"type": "integer"},
"tags": {"type": "text"}
}
}
  • 批量插入

插入的actionindex

示例语法

1
2
3
4
5
6
7
8
9
POST example/_doc/_bulk
{"index": {"_id": 1}}
{"id":1, "name": "admin", "counter":"10", "tags":["red", "black"]}
{"index": {"_id": 2}}
{"id":2, "name": "张三", "counter":"20", "tags":["green", "purple"]}
{"index": {"_id": 3}}
{"id":3, "name": "李四", "counter":"30", "tags":["red", "blue"]}
{"index": {"_id": 4}}
{"id":4, "name": "tom", "counter":"40", "tags":["orange"]}
  • 批量修改

修改的actionupdate

示例语法

1
2
3
4
5
6
7
8
9
POST example/_doc/_bulk
{"update": {"_id": 1}}
{"doc": {"id":1, "name": "admin-02", "counter":"11"}}
{"update": {"_id": 2}}
{"script":{"lang":"painless","source":"ctx._source.counter += params.num","params": {"num":2}}}
{"update":{"_id": 3}}
{"doc": {"name": "test3333name", "counter": 999}}
{"update":{"_id": 4}}
{"doc": {"name": "test444name", "counter": 888}, "doc_as_upsert" : true}

由上面示例可以看出,批量更新支持参数选项:doc(部分文档)、upsertdoc_as_upsert、脚本、params(用于脚本)、lang(用于脚本)和 _source

  • 批量删除

修改的actiondelete

示例如下。

1
2
3
4
5
POST example/_doc/_bulk
{"delete": {"_id": 1}}
{"delete": {"_id": 2}}
{"delete": {"_id": 3}}
{"delete": {"_id": 4}}
  • 批量的混合操作

不过一般不推荐这种使用,项目中也用的极少。

1
2
3
4
5
6
7
8
POST _bulk
{ "index" : { "_index" : "example", "_type" : "_doc", "_id" : "1" } }
{ "name" : "value11111" }
{ "delete" : { "_index" : "example", "_type" : "_doc", "_id" : "2" } }
{ "create" : { "_index" : "example", "_type" : "_doc", "_id" : "3" } }
{ "tags" : "value333" }
{ "update" : {"_id" : "4", "_type" : "_doc", "_index" : "example"} }
{ "doc" : {"name" : "混合444"}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
PUT /hotel
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword",
"copy_to": "all"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}

几个特殊字段说明:

  • location:地理坐标,里面包含精度、纬度
  • all:一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索

地理坐标说明:

copy_to说明:

4.0.2. 初始化 RestClient

在 Elasticsearch 提供的 API 中,与 ES 的所有交互都封装在 RestHighLevelClient 类中,必须先完成该对象的初始化,建立与 Elasticsearch 的连接。

分为三步:

  1. 引入 ES 的 RestHighLevelClient 依赖:
1
2
3
4
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
  1. 因为 SpringBoot 默认的 ES 版本是 7.6.2,需要覆盖默认版本:
1
2
3
4
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
  1. 初始化 RestHighLevelClient
1
2
3
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));

为了单元测试方便,创建测试类 HotelIndexTest,将初始化代码写在 @BeforeEach 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package cn.itcast.hotel;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class HotelIndexTest {
private RestHighLevelClient client;

@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}

@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}

4.1. 创建索引库

4.1.1. 代码解读

创建索引库的 API 如下:

代码分为三步:

  1. 创建 Request 对象。创建索引库时使用 CreateIndexRequest
  2. 添加请求参数,即 DSL 的 JSON 参数部分。JSON 字符串较长,这里定义了静态字符串常量 MAPPING_TEMPLATE
  3. 发送请求,client.indices() 方法返回 IndicesClient 类型,封装了所有索引库相关操作方法。

4.1.2. 完整示例

在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package cn.itcast.hotel.constants;

public class HotelConstants {
public static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}

HotelIndexTest 测试类中,编写单元测试实现创建索引:

1
2
3
4
5
6
7
8
9
@Test
void createHotelIndex() throws IOException {
// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.准备请求的参数:DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}

4.2. 删除索引库

删除索引库的 DSL 语句如下:

1
DELETE /hotel

与创建索引库相比:

  • 请求方式从 PUT 变为 DELETE
  • 请求路径不变
  • 无请求参数

代码差异体现在 Request 对象上,依然是三步走:

  1. 创建 Request 对象,这次是 DeleteIndexRequest
  2. 准备参数(无参)
  3. 发送请求,改用 delete 方法

HotelIndexTest 测试类中,编写单元测试实现删除索引:

1
2
3
4
5
6
7
@Test
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}

4.3. 判断索引库是否存在

判断索引库是否存在,本质就是查询,对应的 DSL 是:

1
GET /hotel

与删除的 Java 代码流程类似,依然是三步走:

  1. 创建 Request 对象,这次是 GetIndexRequest
  2. 准备参数(无参)
  3. 发送请求,改用 exists 方法
1
2
3
4
5
6
7
8
9
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}

4.4. 小结

JavaRestClient 操作 Elasticsearch 的流程基本一致,核心是通过 client.indices() 获取索引库操作对象。

索引库操作的基本步骤:

  1. 初始化 RestHighLevelClient
  2. 创建 XxxIndexRequestXxxCreateGetDelete
  3. 准备 DSL(仅 Create 时需要,其它无参)
  4. 发送请求,调用 client.indices().xxx() 方法(xxxcreateexistsdelete

5. RestClient 操作文档

为了与索引库操作分离,创建一个新的测试类,做两件事情:

  • 初始化 RestHighLevelClient
  • 酒店数据来自数据库,需要注入 IHotelService 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package cn.itcast.hotel;

import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.service.IHotelService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.util.List;

@SpringBootTest
public class HotelDocumentTest {
@Autowired
private IHotelService hotelService;

private RestHighLevelClient client;

@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}

@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}

5.1. 新增文档

将数据库的酒店数据查询出来,写入 Elasticsearch。

5.1.1. 索引库实体类

数据库查询后的结果是一个Hotel类型的对象。结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@TableName("tb_hotel")
public class Hotel {
@TableId(type = IdType.INPUT)
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String longitude;
private String latitude;
private String pic;
}

与索引库结构存在差异:longitudelatitude 需要合并为 location。因此定义新类型 HotelDoc 与索引库结构对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.itcast.hotel.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;

public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}

5.1.2. 语法说明

新增文档的DSL语句如下:

1
2
3
4
5
POST /{索引库名}/_doc/1
{
"name": "Jack",
"age": 21
}

对应的 Java 代码如图:

与创建索引库类似,同样是三步走:

  1. 创建 Request 对象
  2. 准备请求参数(DSL 中的 JSON 文档)
  3. 发送请求

与索引库操作的区别在于,这里直接使用 client.xxx(),无需 client.indices()

5.1.3. 完整代码

导入酒店数据的整体步骤:

  1. 根据 id 查询酒店数据 Hotel
  2. Hotel 封装为 HotelDoc
  3. HotelDoc 序列化为 JSON
  4. 创建 IndexRequest,指定索引库名和 id
  5. 准备请求参数(JSON 文档)
  6. 发送请求

HotelDocumentTest 测试类中编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
void testAddDocument() throws IOException {
// 1.根据id查询酒店数据
Hotel hotel = hotelService.getById(61083L);
// 2.转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.将HotelDoc转json
String json = JSON.toJSONString(hotelDoc);

// 1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
// 2.准备Json文档
request.source(json, XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
}

5.2. 查询文档

5.2.1. 语法说明

查询的DSL语句如下:

1
GET /hotel/_doc/{id}

非常简单,因此代码大概分两步:

  • 准备Request对象
  • 发送请求

不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

结果是一个 JSON,其中文档放在 _source 属性中,解析时取出 _source 反序列化为 Java 对象即可。

同样是三步走:

  1. 准备 Request 对象(查询使用 GetRequest
  2. 发送请求,调用 client.get() 方法
  3. 解析结果,对 JSON 做反序列化

5.2.2. 完整代码

HotelDocumentTest 测试类中编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testGetDocumentById() throws IOException {
// 1.准备Request
GetRequest request = new GetRequest("hotel", "61082");
// 2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();

HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}

5.3. 删除文档

删除的 DSL 如下:

1
DELETE /hotel/_doc/{id}

与查询相比,请求方式从 GET 变为 DELETE,Java 代码同样是三步走:

  1. 准备 DeleteRequest 对象,指定索引库名和 id
  2. 准备参数(无参)
  3. 发送请求,调用 client.delete() 方法

HotelDocumentTest 测试类中编写单元测试:

1
2
3
4
5
6
7
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}

5.4. 修改文档

5.4.1. 语法说明

修改有两种方式:

  • 全量修改:本质是先根据 id 删除,再新增
  • 增量修改:修改文档中的指定字段值

在 RestClient 的 API 中,全量修改与新增的 API 完全一致,判断依据是 ID:若 ID 已存在则修改,若不存在则新增。这里重点关注增量修改。

代码示例如图:

同样是三步走:

  1. 准备 UpdateRequest 对象
  2. 准备参数(包含要修改字段的 JSON 文档)
  3. 调用 client.update() 发送请求

5.4.2. 完整代码

HotelDocumentTest 测试类中编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testUpdateDocument() throws IOException {
// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "61083");
// 2.准备请求参数
request.doc(
"price", "952",
"starName", "四钻"
);
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);
}

5.5. 批量导入文档

利用 BulkRequest 批量将数据库数据导入索引库,步骤如下:

  1. 利用 mybatis-plus 查询酒店数据
  2. Hotel 转换为文档类型 HotelDoc
  3. 利用 BulkRequest 批处理,实现批量新增文档

5.5.1. 语法说明

BulkRequest 的本质是将多个普通 CRUD 请求组合在一起发送,通过 add 方法添加子请求:

可以添加的请求类型:

  • IndexRequest(新增)
  • UpdateRequest(修改)
  • DeleteRequest(删除)

添加多个 IndexRequest 即实现批量新增:

同样是三步走:

  1. 创建 BulkRequest 对象
  2. 准备参数(多个 IndexRequest 对象)
  3. 调用 client.bulk() 发送请求

5.5.2. 完整代码

HotelDocumentTest 测试类中编写单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void testBulkRequest() throws IOException {
// 批量查询酒店数据
List<Hotel> hotels = hotelService.list();

// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个新增的Request
for (Hotel hotel : hotels) {
// 2.1.转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}

5.6. 小结

文档操作的基本步骤:

  1. 初始化 RestHighLevelClient
  2. 创建 XxxRequestXxxIndexGetUpdateDeleteBulk
  3. 准备参数(IndexUpdateBulk 时需要)
  4. 发送请求,调用 RestHighLevelClient#xxx() 方法
  5. 解析结果(Get 时需要)