Java SpringBoot Elasticsearch

前言

Spring Boot 中如何整合 ES,以及如何在 Spring Cloud 微服务项目中使用 ES 来实现全文检索,来达到搜索题库的功能。
而且题库的数据量是非常大的,题目的答案也是非常长的,通过 ES 正好可以解决 mysql 模糊搜索的低效性。
通过本实战可以学到如下知识点:

  • Spring Boot 如何整合 ES。
  • 微服务中 ES 的 API 使用。
  • 项目中如何使用 ES 来达到全文检索。

本篇主要内容如下:
Spring Boot 中整合 Elasticsearch - 图1
主要内容
本文案例都是基于 PassJava 实战项目来演示的。
Github 地址:https://github.com/Jackson0714/PassJava-Platform
为了让大家更清晰地理解 PassJava 项目中 ES 是如何使用的,画了三个流程图:

第一步:创建 question 索引

首先定义 question 索引,然后在 ES 中创建索引。
Spring Boot 中整合 Elasticsearch - 图2

第二步:存 question 数据进 ES

前端保存数据时,保存的 API 请求先经过网关,然后转发到 passjava-question 微服务,然后远程调用 passjava-search 微服务,将数据保存进 ES 中。
Spring Boot 中整合 Elasticsearch - 图3

第三步:从 ES 中查数据

前端查询数据时,先经过网关,然后将请求转发给 passjava-search 微服务,然后从 ES 中查询数据。
Spring Boot 中整合 Elasticsearch - 图4

一、Elasticsearch 组件库介绍

在讲解之前,在这里再次提下全文检索是什么:
全文检索: 指以全部文本信息作为检索对象的一种信息检索技术。而使用的数据库,如 Mysql,MongoDB 对文本信息检索能力特别是中文检索并没有 ES 强大。所以来看下 ES 在项目中是如何来代替 SQL 来工作的。
这里使用的 Elasticsearch 服务是 7.4.2 的版本,然后采用官方提供的 Elastiscsearch-Rest-Client 库来操作 ES,而且官方库的 API 上手简单。
该组件库的官方文档地址:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
另外这个组件库是支持多种语言的:
Spring Boot 中整合 Elasticsearch - 图5
支持多语言
注意:Elasticsearch Clients 就是指如何用 API 操作 ES 服务的组件库。
可能有同学会提问,Elasticsearch 的组件库中写着 JavaScript API,是不是可以直接在前端访问 ES 服务?可以是可以,但是会暴露 ES 服务的端口和 IP 地址,会非常不安全。所以还是用后端服务来访问 ES 服务。
这个项目是 Java 项目,自然就是用上面的两种:Java Rest Client 或者 Java API。先看下 Java API,但是会发现已经废弃了。如下图所示:
Spring Boot 中整合 Elasticsearch - 图6
Java API 已经废弃了
所以只能用 Java REST Client 了。而它又分成两种:高级和低级的。高级包含更多的功能,如果把高级比作MyBatis的话,那么低级就相当于JDBC。所以用高级的 Client。
Spring Boot 中整合 Elasticsearch - 图7
高级和低级 Client

二、整合检索服务

把检索服务单独作为一个服务。就称作 passjava-search 模块吧。

1.1 添加搜索服务模块

  • 创建 passjava-search 模块。

首先在 PassJava-Platform 模块创建一个 搜索服务模块 passjava-search。然后勾选 spring web 服务。如下图所示。
第一步:选择 Spring Initializr,然后点击 Next。
Spring Boot 中整合 Elasticsearch - 图8
选择 Spring Initializr
第二步:填写模块信息,然后点击 Next。
Spring Boot 中整合 Elasticsearch - 图9
passjava-search 服务模块
第三步:选择 Web->Spring Web 依赖,然后点击 Next。
Spring Boot 中整合 Elasticsearch - 图10

1.2 配置 Maven 依赖

  • 参照 ES 官网配置。

进入到 ES 官方网站,可以看到有低级和高级的 Rest Client,选择高阶的(High Level Rest Client)。然后进入到高阶 Rest Client 的 Maven 仓库。官网地址如下所示:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/index.html
Spring Boot 中整合 Elasticsearch - 图11
Rest Client 官方文档

  • 加上 Maven 依赖。对应文件路径:\passjava-search\pom.xml

    1. <dependency>
    2. <groupId>org.elasticsearch.client</groupId>
    3. <artifactId>elasticsearch-rest-high-level-client</artifactId>
    4. <version>7.4.2</version>
    5. </dependency>
  • 配置 elasticsearch 的版本为7.4.2因加上 Maven 依赖后,elasticsearch 版本为 7.6.2,所以遇到这种版本不一致的情况时,需要手动改掉。对应文件路径:\passjava-search\pom.xml

    1. <properties>
    2. <elasticsearch.version>7.4.2</elasticsearch.version>
    3. </properties>

    刷新 Maven Project 后,可以看到引入的 elasticsearch 都是 7.4.2 版本了,如下图所示:
    Spring Boot 中整合 Elasticsearch - 图12
    设置版本为 7.4.2

  • 引入 PassJava 的 Common 模块依赖。Common 模块是 PassJava 项目独立的出来的公共模块,引入了很多公共组件依赖,其他模块引入 Common 模块依赖后,就不需要单独引入这些公共组件了,非常方便。对应文件路径:\passjava-search\pom.xml

    1. <dependency>
    2. <groupId>com.jackson0714.passjava</groupId>
    3. <artifactId>passjava-common</artifactId>
    4. <version>0.0.1-SNAPSHOT</version>
    5. </dependency>

    添加完依赖后,就可以将搜索服务注册到 Nacos 注册中心了。Nacos 注册中心的用法在前面几篇文章中也详细讲解过,这里需要注意的是要先启动 Nacos 注册中心,才能正常注册 passjava-search 服务。

    1.3 注册搜索服务到注册中心

    修改配置文件:src/main/resources/application.properties。配置应用程序名、注册中心地址、注册中心的命名中间。

    1. spring.application.name=passjava-search
    2. spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    3. spring.cloud.nacos.config.namespace=passjava-search

    给启动类添加服务发现注解:@EnableDiscoveryClient。这样 passjava-search 服务就可以被注册中心发现了。
    因 Common 模块依赖数据源,但 search 模块不依赖数据源,所以 search 模块需要移除数据源依赖:

    1. exclude = DataSourceAutoConfiguration.class

    以上的两个注解如下所示:

    1. @EnableDiscoveryClient
    2. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    3. public class PassjavaSearchApplication {
    4. public static void main(String[] args) {
    5. SpringApplication.run(PassjavaSearchApplication.class, args);
    6. }
    7. }

    接下来添加一个 ES 服务的专属配置类,主要目的是自动加载一个 ES Client 来供后续 ES API 使用,不用每次都 new 一个 ES Client。

    1.4 添加 ES 配置类

    配置类:PassJavaElasticsearchConfig.java
    核心方法就是 RestClient.builder 方法,设置好 ES 服务的 IP 地址、端口号、传输协议就可以了。最后自动加载了 RestHighLevelClient。 ```java import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;

@Configuration public class PassJavaElasticsearchConfig {

  1. @Bean
  2. // 给容器注册一个 RestHighLevelClient,用来操作 ES
  3. // 参考官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/java-rest-high-getting-started-initialization.html
  4. public RestHighLevelClient restHighLevelClient() {
  5. return new RestHighLevelClient(
  6. RestClient.builder(
  7. new HttpHost("192.168.56.10", 9200, "http")));
  8. }

}

  1. 接下来测试下 ES Client 是否自动加载成功。
  2. <a name="oe4xV"></a>
  3. ### 1.5 测试 ES Client 自动加载
  4. 在测试类 PassjavaSearchApplicationTests 中编写测试方法,打印出自动加载的 ES Client。期望结果是一个 RestHighLevelClient 对象。
  5. ```java
  6. import org.elasticsearch.client.RestHighLevelClient;
  7. import org.junit.jupiter.api.Test;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.beans.factory.annotation.Qualifier;
  10. import org.springframework.boot.test.context.SpringBootTest;
  11. @SpringBootTest
  12. class PassjavaSearchApplicationTests {
  13. @Qualifier("restHighLevelClient")
  14. @Autowired
  15. private RestHighLevelClient client;
  16. @Test
  17. public void contextLoads() {
  18. System.out.println(client);
  19. }
  20. }

运行结果如下所示,打印出了 RestHighLevelClient。说明自定义的 ES Client 自动装载成功。
Spring Boot 中整合 Elasticsearch - 图13
ES 测试结果

1.6 测试 ES 简单插入数据

测试方法 testIndexData,省略 User 类。users 索引在 ES 中是没有记录的,所以期望结果是 ES 中新增了一条 users 数据。

  1. /**
  2. * 测试存储数据到 ES。
  3. * */
  4. @Test
  5. public void testIndexData() throws IOException {
  6. IndexRequest request = new IndexRequest("users");
  7. request.id("1"); // 文档的 id
  8. //构造 User 对象
  9. User user = new User();
  10. user.setUserName("PassJava");
  11. user.setAge("18");
  12. user.setGender("Man");
  13. //User 对象转为 JSON 数据
  14. String jsonString = JSON.toJSONString(user);
  15. // JSON 数据放入 request 中
  16. request.source(jsonString, XContentType.JSON);
  17. // 执行插入操作
  18. IndexResponse response = client.index(request, RequestOptions.DEFAULT);
  19. System.out.println(response);
  20. }

执行 test 方法,可以看到控制台输出以下结果,说明数据插入到 ES 成功。另外需要注意的是结果中的 result 字段为 updated,是因为本地为了截图,多执行了几次插入操作,但因为 id = 1,所以做的都是 updated 操作,而不是 created 操作。
Spring Boot 中整合 Elasticsearch - 图14
控制台输出结果
再来到 ES 中看下 users 索引中数据。查询 users 索引:

  1. GET users/_search

结果如下所示:
Spring Boot 中整合 Elasticsearch - 图15
查询 users 索引结果
可以从图中看到有一条记录被查询出来,查询出来的数据的 _id = 1,和插入的文档 id 一致。另外几个字段的值也是一致的。说明插入的数据没有问题。

  1. "age" : "18",
  2. "gender" : "Man",
  3. "userName" : "PassJava"

1.7 测试 ES 查询复杂语句

示例:搜索 bank 索引,address 字段中包含 big 的所有人的年龄分布 ( 前 10 条 ) 以及平均年龄,以及平均薪资。

1.7.1 构造检索条件

可以参照官方文档给出的示例来创建一个 SearchRequest 对象,指定要查询的索引为 bank,然后创建一个 SearchSourceBuilder 来组装查询条件。总共有三种条件需要组装:

  • address 中包含 road 的所有人。
  • 按照年龄分布进行聚合。
  • 计算平均薪资。

Spring Boot 中整合 Elasticsearch - 图16
查询复杂语句示例
将打印出来的检索参数复制出来,然后放到 JSON 格式化工具中格式化一下,再粘贴到 ES 控制台执行,发现执行结果是正确的。
Spring Boot 中整合 Elasticsearch - 图17
打印出检索参数
用在线工具格式化 JSON 字符串,结果如下所示:
Spring Boot 中整合 Elasticsearch - 图18
然后去掉其中的一些默认参数,最后简化后的检索参数放到 Kibana 中执行。
Kibana Dev Tools 控制台中执行检索语句如下图所示,检索结果如下图所示:
Spring Boot 中整合 Elasticsearch - 图19
控制台中执行检索语句
找到总记录数:29 条。
第一条命中记录的详情如下:

  1. 平均 balance13136
  2. 平均年龄:26
  3. 地址中包含 Road 的:263 Aviation Road

和 IDEA 中执行的测试结果一致,说明复杂检索的功能已经成功实现。

17.2 获取命中记录的详情

而获取命中记录的详情数据,则需要通过两次 getHists() 方法拿到,如下所示:

  1. // 3.1)获取查到的数据。
  2. SearchHits hits = response.getHits();
  3. // 3.2)获取真正命中的结果
  4. SearchHit[] searchHits = hits.getHits();

可以通过遍历 searchHits 的方式打印出所有命中结果的详情。

  1. // 3.3)、遍历命中结果
  2. for (SearchHit hit: searchHits) {
  3. String hitStr = hit.getSourceAsString();
  4. BankMember bankMember = JSON.parseObject(hitStr, BankMember.class);
  5. }

拿到每条记录的 hitStr 是个 JSON 数据,如下所示:

  1. {
  2. "account_number": 431,
  3. "balance": 13136,
  4. "firstname": "Laurie",
  5. "lastname": "Shaw",
  6. "age": 26,
  7. "gender": "F",
  8. "address": "263 Aviation Road",
  9. "employer": "Zillanet",
  10. "email": "laurieshaw@zillanet.com",
  11. "city": "Harmon",
  12. "state": "WV"
  13. }

而 BankMember 是根据返回的结果详情定义的的 JavaBean。可以通过工具自动生成。在线生成 JavaBean 的网站如下:https://www.bejson.com/json2javapojo/new/
把这个 JavaBean 加到 PassjavaSearchApplicationTests 类中:

  1. @ToString
  2. @Data
  3. static class BankMember {
  4. private int account_number;
  5. private int balance;
  6. private String firstname;
  7. private String lastname;
  8. private int age;
  9. private String gender;
  10. private String address;
  11. private String employer;
  12. private String email;
  13. private String city;
  14. private String state;
  15. }

然后将 bankMember 打印出来:

  1. System.out.println(bankMember);

Spring Boot 中整合 Elasticsearch - 图20
bankMember
得到的结果确实是封装的 BankMember 对象,而且里面的属性值也都拿到了。

1.7.3 获取年龄分布聚合信息

ES 返回的 response 中,年龄分布的数据是按照 ES 的格式返回的,如果想按照自己的格式来返回,就需要将 response 进行处理。
如下图所示,这个是查询到的年龄分布结果,需要将其中某些字段取出来,比如 buckets,它代表了分布在 21 岁的有 4 个。
Spring Boot 中整合 Elasticsearch - 图21
ES 返回的年龄分布信息
下面是代码实现:

  1. Aggregations aggregations = response.getAggregations();
  2. Terms ageAgg1 = aggregations.get("ageAgg");
  3. for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
  4. String keyAsString = bucket.getKeyAsString();
  5. System.out.println("用户年龄: " + keyAsString + " 人数:" + bucket.getDocCount());
  6. }

最后打印的结果如下,21 岁的有 4 人,26 岁的有 4 人,等等。
Spring Boot 中整合 Elasticsearch - 图22
打印结果:用户年龄分布

1.7.4 获取平均薪资聚合信息

现在来看看平均薪资如何按照所需的格式返回,ES 返回的结果如下图所示,需要获取 balanceAvg 字段的 value 值。
Spring Boot 中整合 Elasticsearch - 图23
ES 返回的平均薪资信息
代码实现:

  1. Avg balanceAvg1 = aggregations.get("balanceAvg");
  2. System.out.println("平均薪资:" + balanceAvg1.getValue());

打印结果如下,平均薪资 28578 元。
Spring Boot 中整合 Elasticsearch - 图24
打印结果:平均薪资

三、实战:同步 ES 数据

3.1 定义检索模型

PassJava 这个项目可以用来配置题库,如果想通过关键字来搜索题库,该怎么做呢?
类似于百度搜索,输入几个关键字就可以搜到关联的结果,这个功能也是类似,通过 Elasticsearch 做检索引擎,后台管理界面和小程序作为搜索入口,只需要在小程序上输入关键字,就可以检索相关的题目和答案。
首先需要把题目和答案保存到 ES 中,在存之前,第一步是定义索引的模型,如下所示,模型中有 title 和 answer 字段,表示题目和答案。

  1. "id": {
  2. "type": "long"
  3. },
  4. "title": {
  5. "type": "text",
  6. "analyzer": "ik_smart"
  7. },
  8. "answer": {
  9. "type": "text",
  10. "analyzer": "ik_smart"
  11. },
  12. "typeName": {
  13. "type": "keyword"
  14. }

3.2 在 ES 中创建索引

上面已经定义了索引结构,接着就是在 ES 中创建索引。
在 Kibana 控制台中执行以下语句:

  1. PUT question
  2. {
  3. "mappings" : {
  4. "properties": {
  5. "id": {
  6. "type": "long"
  7. },
  8. "title": {
  9. "type": "text",
  10. "analyzer": "ik_smart"
  11. },
  12. "answer": {
  13. "type": "text",
  14. "analyzer": "ik_smart"
  15. },
  16. "typeName": {
  17. "type": "keyword"
  18. }
  19. }
  20. }
  21. }

执行结果如下所示:
Spring Boot 中整合 Elasticsearch - 图25
创建 question 索引
可以通过以下命令来查看 question 索引是否在 ES 中:

  1. GET _cat/indices

执行结果如下图所示:
Spring Boot 中整合 Elasticsearch - 图26
查看 ES 中所有的索引

3.3 定义 ES model

上面定义 ES 的索引,接着就是定义索引对应的模型,将数据存到这个模型中,然后再存到 ES 中。
ES 模型如下,共四个字段:id、title、answer、typeName。和 ES 索引是相互对应的。

  1. @Data
  2. public class QuestionEsModel {
  3. private Long id;
  4. private String title;
  5. private String answer;
  6. private String typeName;
  7. }

3.4 触发保存的时机

当在后台创建题目或保存题目时,先将数据保存到 mysql 数据库,然后再保存到 ES 中。
如下图所示,在管理后台创建题目时,触发保存数据到 ES 。
Spring Boot 中整合 Elasticsearch - 图27
第一步,保存数据到 mysql 中,项目中已经包含此功能,就不再讲解了,直接进入第二步:保存数据到 ES 中。
而保存数据到 ES 中,需要将数据组装成 ES 索引对应的数据,所以用了一个 ES model,先将数据保存到 ES model 中。

3.5 用 model 来组装数据

这里的关键代码时 copyProperties,可以将 question 对象的数据取出,然后赋值到 ES model 中。不过 ES model 中还有些字段是 question 中没有的,所以需要单独拎出来赋值,比如 typeName 字段,question 对象中没有这个字段,它对应的字段是 question.type,所以把 type 取出来赋值到 ES model 的 typeName 字段上。如下图所示:
Spring Boot 中整合 Elasticsearch - 图28
用 model 来组装数据

3.6 保存数据到 ES

在 passjava-search 微服务中写了一个保存题目的 api 用来保存数据到 ES 中。
Spring Boot 中整合 Elasticsearch - 图29
保存数据到 ES
然后在 passjava-question 微服务中调用 search 微服务的保存 ES 的方法就可以了。

  1. // 调用 passjava-search 服务,将数据发送到 ES 中保存。
  2. searchFeignService.saveQuestion(esModel);

3.7 检验 ES 中是否创建成功

可以通过 kibana 的控制台来查看 question 索引中的文档。通过以下命令来查看:

  1. GET question/_search

执行结果如下图所示,有一条记录:
Spring Boot 中整合 Elasticsearch - 图30
另外大家有没有疑问:可以重复更新题目吗?
答案是可以的,保存到 ES 的数据是幂等的,因为保存的时候带了一个类似数据库主键的 id。

四、实战:查询 ES 数据

已经将数据同步到了 ES 中,现在就是前端怎么去查询 ES 数据中,这里还是使用 Postman 来模拟前端查询请求。

4.1 定义请求参数

请求参数定义了三个:

  • keyword:用来匹配问题或者答案。
  • id:用来匹配题目 id。
  • pageNum:用来分页查询数据。

这里将这三个参数定义为一个类:

  1. @Data
  2. public class SearchParam {
  3. private String keyword; // 全文匹配的关键字
  4. private String id; // 题目 id
  5. private Integer pageNum; // 查询第几页数据
  6. }

4.2 定义返回参数

返回的 response 也定义了四个字段:

  • questionList:查询到的题目列表。
  • pageNum:第几页数据。
  • total:查询到的总条数。
  • totalPages:总页数。

定义的类如下所示:

  1. @Data
  2. public class SearchQuestionResponse {
  3. private List<QuestionEsModel> questionList; // 题目列表
  4. private Integer pageNum; // 查询第几页数据
  5. private Long total; // 总条数
  6. private Integer totalPages; // 总页数
  7. }

4.3 组装 ES 查询参数

调用 ES 的查询 API 时,需要构建查询参数。
组装查询参数的核心代码如下所示:
Spring Boot 中整合 Elasticsearch - 图31
组装查询参数

  • 第一步:创建检索请求。
  • 第二步:设置哪些字段需要模糊匹配。这里有三个字段:title,answer,typeName。
  • 第三步:设置如何分页。这里分页大小是 5 个。
  • 第四步:调用查询 api。

    4.4 格式化 ES 返回结果

    ES 返回的数据是 ES 定义的格式,真正的数据被嵌套在 ES 的 response 中,所以需要格式化返回的数据。
    核心代码如下图所示:
    Spring Boot 中整合 Elasticsearch - 图32
    格式化 ES 返回结果

  • 第一步:获取查到的数据。

  • 第二步:获取真正命中的结果。
  • 第三步:格式化返回的数据。
  • 第四步:组装分页参数。

    4.5 测试 ES 查询

    4.5.1 实验一:测试 title 匹配

    现在想要验证 title 字段是否能匹配到,传的请求参数 keyword = 111,匹配到了 title = 111 的数据,且只有一条。页码 pageNum 传的 1,表示返回第一页数据。如下图所示:
    Spring Boot 中整合 Elasticsearch - 图33
    测试匹配 title

    4.5.2 实验二:测试 answer 匹配

    现在想要验证 answer 字段是否能匹配到,传的请求参数 keyword = 测试答案,匹配到了 title = 测试答案的数据,且只有一条,说明查询成功。如下图所示:
    Spring Boot 中整合 Elasticsearch - 图34
    测试匹配 answer

    4.5.2 实验三:测试 id 匹配

    现在想要匹配题目 id 的话,需要传请求参数 id,而且 id 是精确匹配。另外 id 和 keyword 是取并集,所以不能传 keyword 字段。
    请求参数 id = 5,返回结果也是 id =5 的数据,说明查询成功。如下图所示:
    Spring Boot 中整合 Elasticsearch - 图35
    测试 id 匹配