ELasticsearch 是一款非常强大的开源搜索引擎,具备非常多强大的功能,可以帮助我们从海量数据中快速找到所需要的内容,可以用来实现搜索、日志统计、分析、系统监控等功能。
例如:



elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:
而 elasticsearch 是 elastic stack 的核心,负责存储、搜索、分析数据。
elasticsearch底层是基于lucene来实现的。
Lucene 是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:https://lucene.apache.org/ 。

elasticsearch 的发展历史:

目前比较知名的搜索引擎技术排名:

虽然在早期,Apache Solr 是最主要的搜索引擎技术,但随着发展elasticsearch已经渐渐超越了Solr,独占鳌头:
什么是elasticsearch?
什么是elastic stack(ELK)?
什么是Lucene?
首先,倒排索引的概念是基于 MySQL 这样的正向索引而言的。
那么什么是正向索引呢?例如给下表(tb_goods)中的 id 创建索引

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

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

虽然要先查询倒排索引,再查询正向索引,但无论是词条还是文档id都建立了索引,查询速度非常快!无需全表扫描。
什么是文档和词条?
每一条数据就是一个文档
对文档中的内容进行分词,得到的词语就是词条
什么是正向索引?
基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条
什么是倒排索引?
对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条找到文档id,而后获取到文档
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在 elasticsearch 中:

而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
索引(Index),就是相同类型的文档的集合。
例如:

因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。

因此在企业中,往往是两者结合使用的:

因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
docker pull elasticsearch:7.12.1
运行docker命令,部署单点es:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m":配置es运行时的内存大小-e "discovery.type=single-node":非集群模式-v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录-v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录--privileged:授予逻辑卷访问权--network es-net :让 es 容器加入一个名为 es-net 的网络中-p 9200:9200:端口映射配置-e "cluster.name=es-docker-cluster":设置集群名称-e "http.host=0.0.0.0":监听的地址,可以外网访问-v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录访问地址:http://es服务器ip地址:9200,即可看到elasticsearch的响应结果:

kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
docker pull kibana:7.12.1
运行docker命令,部署kibana:
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
--network es-net :让 kibana 容器加入一个名为 es-net 的网络中,与 elasticsearch 在同一个网络中-e ELASTICSEARCH_HOSTS=http://es:9200":设置 elasticsearch 的地址,因为kibana已经和elasticsearch在同一个网络中,因此可以用容器名直接访问elasticsearch-p 5601:5601:端口映射配置访问地址:http://kibana服务器ip地址:5601,即可看到结果

es在创建倒排索引时需要对文档进行分词。在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。
我们在kibana的DevTools中测试:
POST /_analyze
{
"text": "黑马程序员学习java太棒了",
"analyzer": "standard"
}
POST:请求方式
/_analyze:请求路径,这里省略了 http://es服务器ip地址:9200,由kibana帮我们补充
请求参数,json风格:

可以看到,默认的分词器是通过逐字进行分词的,显然这种分词效果并不是我们想要的。
所以在处理中文分词时,一般会使用IK分词器:https://github.com/medcl/elasticsearch-analysis-ik
由于国内访问 GitHub 较慢,我们选择离线模式安装。
1、安装插件需要知道 elasticsearch 的 plugins 目录位置,而我们用了数据卷挂载,因此需要查看 elasticsearch 的数据卷目录,通过下面命令查看:
docker volume inspect es-plugins

说明 plugins 目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data 这个目录中。
2、解压缩分词器安装包,并上传到es容器的插件数据卷中

3、重启 es 容器
# 重启es容器
docker restart es
# 查看es日志
docker logs -f es
IK分词器包含两种模式:
ik_smart:智能切分,粗粒度ik_max_word:最细切分,细粒度
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等等。
所以我们的词汇需要不断的更新,IK分词器也提供了扩展词汇的功能。
1、打开IK分词器 config 目录的 IKAnalyzer.cfg.xml,添加一个文件名,我们以 ext.dic 文件名为例。


2、创建 ext.dic 文件,在其中添加热点词就好了,一个词一行。

3、重启 elasticsearch
docker restart es
4、测试效果:

在互联网项目中,网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略掉当前的停用词汇表中的内容。
1、打开IK分词器 config 目录的 IKAnalyzer.cfg.xml,添加一个文件名,我们以 stopword.dic 文件名为例。


2、在 stopword.dic 中添加停用词

3、重启 elasticsearch
docker restart es
分词器的作用是什么?
IK分词器有几种模式?
IK分词器如何拓展词条?如何停用词条?
索引库就类似数据库表,mapping映射就类似表的结构。
我们要向es中存储数据,必须先创建“库”和“表”。
mapping是对索引库中文档的约束,常见的mapping属性包括:
例如下面的json文档:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",
"lastName": "赵"
}
}
对应的每个字段映射(mapping):
ES中通过Restful请求操作索引库、文档。请求内容用DSL语句来表示。
基本语法:
格式:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
示例:
# 创建索引库
PUT /xiexu
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"type": "object",
"properties": {
"firstname": {
"type": "keyword"
},
"lastname": {
"type": "keyword"
}
}
}
}
}
}

基本语法:
格式:
GET /索引库名
示例:
# 查询索引库
GET /xiexu

语法:
格式:
DELETE /索引库名
示例:
# 删除索引库
DELETE /xiexu

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

语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
示例:
# 插入文档
POST /xiexu/_doc/1
{
"info": "小明Java讲师",
"email": "1193499619@qq.com",
"name": {
"firstname": "云",
"lastname": "赵"
}
}

语法:
GET /索引库名称/_doc/{id}
示例:
# 查询文档
GET /xiexu/_doc/1

语法:
DELETE /索引库名/_doc/id值
示例:
# 根据id删除数据
DELETE /xiexu/_doc/1

修改文档有两种方式:
全量修改是覆盖原来的文档,其本质是:
如果根据id删除时,id不存在,第二步的新增也会执行,也就是从修改变成了新增操作。
注意:如果使用全量修改时,只修改个别字段,那么其余未修改的字段数据会被删除。
语法:
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
示例:
# 全量修改文档
PUT /xiexu/_doc/1
{
"info": "小明Java讲师",
"email": "zhaoyun@qq.com",
"name": {
"firstname": "云",
"lastname": "赵"
}
}

增量修改,修改指定字段的值
语法:
POST /索引库名/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
示例:
# 增量修改文档
POST /xiexu/_update/1
{
"doc": {
"email": "xiexu@11.com"
}
}

ES官方提供了各种不同语言的客户端,用来操作 ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括两种:

我们下面学习的是 Java HighLevel Rest Client 客户端API
数据表结构
CREATE TABLE `tb_hotel` (
`id` bigint(20) NOT NULL COMMENT '酒店id',
`name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
`address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
`price` int(10) NOT NULL COMMENT '酒店价格;例:329',
`score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
`brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
`city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
`star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
`business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
`latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
`longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
`pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
其中:
# 酒店的mapping
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"
},
"starName": {
"type": "keyword"
},
"business": {
"type": "keyword",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
几个特殊字段说明:
地理坐标说明:

copy_to说明:

在 elasticsearch 提供的 API 中,elasticsearch 一切交互都封装在一个名为 RestHighLevelClient 的类中,必须先完成这个对象的初始化,建立与 elasticsearch 的连接。
1、引入 es 的 RestHighLevelClient 依赖:
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
dependency>
2、因为 SpringBoot 默认的 ES 版本是 7.6.2,所以我们需要覆盖默认的 ES 版本:
<properties>
<java.version>1.8java.version>
<elasticsearch.version>7.12.1elasticsearch.version>
properties>
3、初始化 RestHighLevelClient:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://es服务器ip地址:9200")
));
4、我们创建一个测试类 HotelIndexTest,然后将初始化的代码编写在 @BeforeEach 方法
@SpringBootTest
class HotelIndexTest {
private RestHighLevelClient client;
@Test
void testInit() {
System.out.println("client = " + client);
}
@BeforeEach
void setUp() {
// 初始化客户端
this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://es服务器ip地址:9200")));
}
@AfterEach
void tearDown() throws IOException {
// 关闭客户端
this.client.close();
}
}
创建索引库的API如下:

代码分为三步:
1、创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
2、添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量 MAPPING_TEMPLATE,让代码看起来更加优雅。
3、发送请求,client.indices() 方法的返回值是 IndicesClient 类型,封装了所有与索引库操作有关的方法。
在 hotel-demo 的 cn.itcast.hotel.constants 包下,创建一个类,定义 mapping 映射的 JSON 字符串常量:
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" +
" },\n" +
" \"starName\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\": {\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"pic\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"location\": {\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"all\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}
在 hotel-demo 中的 HotelIndexTest 测试类中,编写单元测试,实现创建索引库:
/**
* 创建索引库
*
* @throws IOException
*/
@Test
void createHotelIndex() throws IOException {
// 1.创建Request对象(PUT /hotel)
CreateIndexRequest request = new CreateIndexRequest("hotel"); // 索引库名称
// 2.准备请求参数:DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
测试:

/**
* 删除索引库
*
* @throws IOException
*/
@Test
void testDeleteIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel"); // 索引库名称
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
/**
* 判断索引库是否存在
*
* @throws IOException
*/
@Test
void testExistsIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel"); // 索引库名称
// 2.发送请求
boolean isExists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(isExists ? "索引库已经存在!" : "索引库不存在!");
}
为了与索引库操作分离,我们再次创建一个测试类,做两件事情:
@SpringBootTest
class HotelDocumentTest {
@Autowired
private IHotelService hotelService;
private RestHighLevelClient client;
@BeforeEach
void setUp() {
// 初始化客户端
this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://es服务器ip地址:9200")));
}
@AfterEach
void tearDown() throws IOException {
// 关闭客户端
this.client.close();
}
}
我们要将数据库的酒店数据查询出来,写入 elasticsearch 的 hotel 索引库中。
数据库查询后的结果是一个Hotel类型的对象。结构如下:
@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;
}
这个 Hotel 对象与我们的数据库结构完全一致,但是它与我们的索引库结构存在差异:
因此,我们需要定义一个新的类型,与索引库结构吻合:
@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();
}
}
新增文档的DSL语句如下:
POST /索引库名/_doc/1
{
"name": "Jack",
"age": 21
}
对应的java代码如图:

我们导入酒店数据,流程基本一致,但需要考虑几点变化:
因此,代码整体步骤如下:
1、根据id查询酒店数据Hotel
2、将Hotel封装成HotelDoc
3、将HotelDoc序列化为JSON
4、创建IndexRequest,指定索引库名和id
5、准备请求参数,也就是JSON文档
6、发送请求
在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试:
/**
* 新增文档
*
* @throws IOException
*/
@Test
void testAddDocument() throws IOException {
// 1.根据id查询酒店数据
Hotel hotel = hotelService.getById(61083L);
// 2.转换为HotelDoc文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.转换成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);
}
测试:

查询的DSL语句如下:
GET /hotel/_doc/{id}
查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

可以看到,结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可。
1、准备Request对象。这次是查询,所以是 GetRequest
2、发送请求,得到结果。因为是查询,这里调用 client.get() 方法
3、解析结果,就是对JSON做反序列化
在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试:
/**
* 查询文档
*
* @throws IOException
*/
@Test
void testGetDocumentById() throws IOException {
// 1.创建Request对象
GetRequest request = new GetRequest("hotel", "61083");
// 2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();
// 4.将json文档转换成HotelDoc对象
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}

修改文档有两种方式:
1、全量修改:本质是先根据id删除,再新增
2、增量修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
所以全量修改写法与新增文档一样,下面我们主要介绍增量修改。
代码示例如图:

在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试:
/**
* 修改文档
*
* @throws IOException
*/
@Test
void testUpdateDocumentById() throws IOException {
// 1.创建Request对象
UpdateRequest request = new UpdateRequest("hotel", "61083");
// 2.准备参数,每2个参数为一对 key value
request.doc(
"price", "88888",
"starName", "四钻"
);
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);
}
/**
* 删除文档
*
* @throws IOException
*/
@Test
void testDeleteDocumentById() throws IOException {
// 1.创建Request对象
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}
案例需求:利用BulkRequest批量将数据库数据导入到索引库中。
思路:
1、利用 mybatis-plus 查询酒店数据
2、将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
3、利用JavaRestClient中的BulkRequest批处理,实现批量新增文档
批量处理 BulkRequest,其本质就是将多个普通的 CRUD 请求组合在一起发送。
其中提供了一个add方法,用来添加其他请求:

可以看到,能添加的请求包括:
因此我们在 BulkRequest 中添加多个 IndexRequest,就是批量新增功能了。示例:

在 hotel-demo 的 HotelDocumentTest 测试类中,编写单元测试:
/**
* 批量新增文档
*
* @throws IOException
*/
@Test
void testBulkRequest() throws IOException {
// 查询所有的酒店数据
List<Hotel> list = hotelService.list();
// 1.创建Request对象
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个新增的Request
for (Hotel hotel : list) {
// 2.1 转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2 转json
String json = JSON.toJSONString(hotelDoc);
// 2.3 添加请求
request.add(new IndexRequest("hotel").id(hotel.getId().toString()).source(json, XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}
