1W字|40 图|硬核 ES 实战

回复 PDF 领取资料 html

这是悟空的第 92 篇原创文章前端

做者 | 悟空聊架构java

来源 | 悟空聊架构(ID:PassJava666)mysql

转载请联系受权(微信ID:PassJava)git

前言

上篇咱们讲到了 Elasticsearch 全文检索的原理《别只会搜日志了,求你懂点原理吧》,经过在本地搭建一套 ES 服务,以多个案例来分析了 ES 的原理以及基础使用。此次咱们来说下 Spring Boot 中如何整合 ES,以及如何在 Spring Cloud 微服务项目中使用 ES 来实现全文检索,来达到搜索题库的功能。github

并且题库的数据量是很是大的,题目的答案也是很是长的,经过 ES 正好能够解决 mysql 模糊搜索的低效性。web

经过本实战您能够学到以下知识点:spring

  • Spring Boot 如何整合 ES。sql

  • 微服务中 ES 的 API 使用。数据库

  • 项目中如何使用 ES 来达到全文检索。

本篇主要内容以下:

主要内容

本文案例都是基于 PassJava 实战项目来演示的。

Github 地址:https://github.com/Jackson0714/PassJava-Platform

 

为了让你们更清晰地理解 PassJava 项目中 ES 是如何使用的,我画了三个流程图:

  • 第一步:建立 question 索引。

首先定义 question 索引,而后在 ES 中建立索引。

  • 第二步:存 question 数据进 ES 。

前端保存数据时,保存的 API 请求先通过网关,而后转发到 passjava-question 微服务,而后远程调用 passjava-search 微服务,将数据保存进 ES 中。

  • 第三步:从 ES 中查数据。

    前端查询数据时,先通过网关,而后将请求转发给 passjava-search 微服务,而后从 ES 中查询数据。

1、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

另外这个组件库是支持多种语言的:

支持多语言

注意:Elasticsearch Clients 就是指如何用 API 操做 ES 服务的组件库。

可能有同窗会提问,Elasticsearch 的组件库中写着 JavaScript API,是否是能够直接在前端访问 ES 服务?能够是能够,可是会暴露 ES 服务的端口和 IP 地址,会很是不安全。因此咱们仍是用后端服务来访问 ES 服务。

咱们这个项目是 Java 项目,天然就是用上面的两种:Java Rest Client 或者 Java API。咱们先看下 Java API,可是会发现已经废弃了。以下图所示:

Java API 已经废弃了

因此咱们只能用 Java REST Client 了。而它又分红两种:高级和低级的。高级包含更多的功能,若是把高级比做MyBatis的话,那么低级就至关于JDBC。因此咱们用高级的 Client。

高级和低级 Client

2、整合检索服务

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

1.1 添加搜索服务模块

  • 建立 passjava-search 模块。

首先咱们在 PassJava-Platform 模块建立一个 搜索服务模块 passjava-search。而后勾选 spring web 服务。以下图所示。

第一步:选择 Spring Initializr,而后点击 Next。

选择 Spring Initializr

第二步:填写模块信息,而后点击 Next。

passjava-search 服务模块

第三步:选择 Web->Spring Web 依赖,而后点击 Next。

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

Rest Client 官方文档

  • 加上 Maven 依赖。

    对应文件路径:\passjava-search\pom.xml

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
</dependency>
  • 配置 elasticsearch 的版本为7.4.2

    因加上 Maven 依赖后,elasticsearch 版本为 7.6.2,因此遇到这种版本不一致的状况时,须要手动改掉。

    对应文件路径:\passjava-search\pom.xml

<properties>
	<elasticsearch.version>7.4.2</elasticsearch.version>
</properties>

刷新 Maven Project 后,能够看到引入的 elasticsearch 都是 7.4.2 版本了,以下图所示:

设置版本为 7.4.2

  • 引入 PassJava 的 Common 模块依赖。

    Common 模块是 PassJava 项目独立的出来的公共模块,引入了不少公共组件依赖,其余模块引入 Common 模块依赖后,就不须要单独引入这些公共组件了,很是方便。

    对应文件路径:\passjava-search\pom.xml

 <dependency>
     <groupId>com.jackson0714.passjava</groupId>
     <artifactId>passjava-common</artifactId>
     <version>0.0.1-SNAPSHOT</version>
</dependency>

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

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

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

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

启动类添加服务发现注解:@EnableDiscoveryClient。这样 passjava-search 服务就能够被注册中心发现了。

因 Common 模块依赖数据源,但 search 模块不依赖数据源,因此 search 模块须要移除数据源依赖:

exclude = DataSourceAutoConfiguration.class 

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

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class PassjavaSearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(PassjavaSearchApplication.classargs);
    }
}

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

1.4 添加 ES 配置类

配置类:PassJavaElasticsearchConfig.java

核心方法就是 RestClient.builder 方法,设置好 ES 服务的 IP 地址、端口号、传输协议就能够了。最后自动加载了 RestHighLevelClient。

package com.jackson0714.passjava.search.config;

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;

/**  * @Author: 公众号 | 悟空聊架构  * @Date: 2020/10/8 17:02  * @Site: www.passjava.cn  * @Github: https://github.com/Jackson0714/PassJava-Platform  */
@Configuration
public class PassJavaElasticsearchConfig {

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

接下来咱们测试下 ES Client 是否自动加载成功。

1.5 测试 ES Client 自动加载

在测试类 PassjavaSearchApplicationTests 中编写测试方法,打印出自动加载的 ES Client。指望结果是一个 RestHighLevelClient 对象。

package com.jackson0714.passjava.search;

import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class PassjavaSearchApplicationTests {

    @Qualifier("restHighLevelClient")
    @Autowired
    private RestHighLevelClient client;

    @Test
    public void contextLoads() {
        System.out.println(client);
    }
}

运行结果以下所示,打印出了 RestHighLevelClient。说明自定义的 ES Client 自动装载成功。

ES 测试结果

1.6 测试 ES 简单插入数据

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

/**  * 测试存储数据到 ES。  * */
@Test
public void testIndexData() throws IOException {
    IndexRequest request = new IndexRequest("users");
    request.id("1"); // 文档的 id
    
    //构造 User 对象
    User user = new User();
    user.setUserName("PassJava");
    user.setAge("18");
    user.setGender("Man");
    
    //User 对象转为 JSON 数据
    String jsonString = JSON.toJSONString(user);
    
    // JSON 数据放入 request 中
    request.source(jsonString, XContentType.JSON);

    // 执行插入操做
    IndexResponse response = client.index(request, RequestOptions.DEFAULT);

    System.out.println(response);
}

执行 test 方法,咱们能够看到控制台输出如下结果,说明数据插入到 ES 成功。另外须要注意的是结果中的 result 字段为 updated,是由于我本地为了截图,多执行了几回插入操做,但由于 id = 1,因此作的都是 updated 操做,而不是 created 操做。

控制台输出结果

咱们再来到 ES 中看下 users 索引中数据。查询 users 索引:

GET users/_search

结果以下所示:

查询 users 索引结果

能够从图中看到有一条记录被查询出来,查询出来的数据的 _id = 1,和插入的文档 id 一致。另外几个字段的值也是一致的。说明插入的数据没有问题。

"age" : "18",
"gender" : "Man",
"userName" : "PassJava"

1.7 测试 ES 查询复杂语句

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

1.7.1 构造检索条件

咱们能够参照官方文档给出的示例来建立一个 SearchRequest 对象,指定要查询的索引为 bank,而后建立一个 SearchSourceBuilder 来组装查询条件。总共有三种条件须要组装:

  • address 中包含 road 的全部人。

  • 按照年龄分布进行聚合。

  • 计算平均薪资。

代码以下所示,须要源码请到个人 Github/PassJava 上下载。

查询复杂语句示例

将打印出来的检索参数复制出来,而后放到 JSON 格式化工具中格式化一下,再粘贴到 ES 控制台执行,发现执行结果是正确的。

打印出检索参数

用在线工具格式化 JSON 字符串,结果以下所示:

而后咱们去掉其中的一些默认参数,最后简化后的检索参数放到 Kibana 中执行。

Kibana Dev Tools 控制台中执行检索语句以下图所示,检索结果以下图所示:

控制台中执行检索语句

找到总记录数:29 条。

第一条命中记录的详情以下:

平均 balance:13136。

平均年龄:26。

地址中包含 Road 的:263 Aviation Road。

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

17.2 获取命中记录的详情

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

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

咱们能够经过遍历 searchHits 的方式打印出全部命中结果的详情。

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

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

{
 "account_number"431,
 "balance"13136,
 "firstname""Laurie",
 "lastname""Shaw",
 "age"26,
 "gender""F",
 "address""263 Aviation Road",
 "employer""Zillanet",
 "email""laurieshaw@zillanet.com",
 "city""Harmon",
 "state""WV"
}

而 BankMember 是根据返回的结果详情定义的的 JavaBean。能够经过工具自动生成。在线生成 JavaBean 的网站以下:

https://www.bejson.com/json2javapojo/new/

把这个 JavaBean 加到 PassjavaSearchApplicationTests 类中:

@ToString
@Data
static class BankMember {
    private int account_number;
    private int balance;
    private String firstname;
    private String lastname;
    private int age;
    private String gender;
    private String address;
    private String employer;
    private String email;
    private String city;
    private String state;
}

而后将 bankMember 打印出来:

System.out.println(bankMember);

bankMember

获得的结果确实是咱们封装的 BankMember 对象,并且里面的属性值也都拿到了。

1.7.3 获取年龄分布聚合信息

ES 返回的 response 中,年龄分布的数据是按照 ES 的格式返回的,若是想按照咱们本身的格式来返回,就须要将 response 进行处理。

以下图所示,这个是查询到的年龄分布结果,咱们须要将其中某些字段取出来,好比 buckets,它表明了分布在 21 岁的有 4 个。

ES 返回的年龄分布信息

下面是代码实现:

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

最后打印的结果以下,21 岁的有 4 人,26 岁的有 4 人,等等。

打印结果:用户年龄分布

1.7.4 获取平均薪资聚合信息

如今来看看平均薪资如何按照所需的格式返回,ES 返回的结果以下图所示,咱们须要获取 balanceAvg 字段的 value 值。

ES 返回的平均薪资信息

代码实现:

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

打印结果以下,平均薪资 28578 元。

打印结果:平均薪资

3、实战:同步 ES 数据

3.1 定义检索模型

PassJava 这个项目能够用来配置题库,若是咱们想经过关键字来搜索题库,该怎么作呢?

相似于百度搜索,输入几个关键字就能够搜到关联的结果,咱们这个功能也是相似,经过 Elasticsearch 作检索引擎,后台管理界面和小程序做为搜索入口,只须要在小程序上输入关键字,就能够检索相关的题目和答案。

首先咱们须要把题目和答案保存到 ES 中,在存以前,第一步是定义索引的模型,以下所示,模型中有 titleanswer 字段,表示题目和答案。

"id": {
    "type""long"
},
"title": {
    "type""text",
    "analyzer""ik_smart"
},
"answer": {
    "type""text",
    "analyzer""ik_smart"
},
"typeName": {
    "type""keyword"
}

3.2 在 ES 中建立索引

上面咱们已经定义了索引结构,接着就是在 ES 中建立索引。

在 Kibana 控制台中执行如下语句:

PUT question
{
 "mappings" : {
     "properties": {
     "id": {
      "type""long"
     },
     "title": {
      "type""text",
      "analyzer""ik_smart"
     },
     "answer": {
      "type""text",
      "analyzer""ik_smart"
     },
     "typeName": {
      "type""keyword"
     }
  }
  }
}

执行结果以下所示:

建立 question 索引

咱们能够经过如下命令来查看 question 索引是否在 ES 中:

GET _cat/indices

执行结果以下图所示:

查看 ES 中全部的索引

3.3 定义 ES model

上面咱们定义 ES 的索引,接着就是定义索引对应的模型,将数据存到这个模型中,而后再存到 ES 中。

ES 模型以下,共四个字段:id、title、answer、typeName。和 ES 索引是相互对应的。

@Data
public class QuestionEsModel {
    private Long id;
    private String title;
    private String answer;
    private String typeName;
}

3.4 触发保存的时机

当咱们在后台建立题目或保存题目时,先将数据保存到 mysql 数据库,而后再保存到 ES 中。

以下图所示,在管理后台建立题目时,触发保存数据到 ES 。


第一步,保存数据到 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 字段上。以下图所示:

用 model 来组装数据

3.6 保存数据到 ES

我在 passjava-search 微服务中写了一个保存题目的 api 用来保存数据到 ES 中。

保存数据到 ES

而后在 passjava-question 微服务中调用 search 微服务的保存 ES 的方法就能够了。

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

3.7 检验 ES 中是否建立成功

咱们能够经过 kibana 的控制台来查看 question 索引中的文档。经过如下命令来查看:

GET question/_search

执行结果以下图所示,有一条记录:

另外你们有没有疑问:能够重复更新题目吗?

答案是能够的,保存到 ES 的数据是幂等的,由于保存的时候带了一个相似数据库主键的 id。

4、实战:查询 ES 数据

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

4.1 定义请求参数

请求参数我定义了三个:

  • keyword:用来匹配问题或者答案。

  • id:用来匹配题目 id。

  • pageNum:用来分页查询数据。

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

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

4.2 定义返回参数

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

  • questionList:查询到的题目列表。

  • pageNum:第几页数据。

  • total:查询到的总条数。

  • totalPages:总页数。

定义的类以下所示:

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

4.3 组装 ES 查询参数

调用 ES 的查询 API 时,须要构建查询参数。

组装查询参数的核心代码以下所示:

组装查询参数

  • 第一步:建立检索请求。

  • 第二步:设置哪些字段须要模糊匹配。这里有三个字段:title,answer,typeName。

  • 第三步:设置如何分页。这里分页大小是 5 个。

  • 第四步:调用查询 api。

4.4 格式化 ES 返回结果

ES 返回的数据是 ES 定义的格式,真正的数据被嵌套在 ES 的 response 中,因此须要格式化返回的数据。

核心代码以下图所示:

格式化 ES 返回结果

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

  • 第二步:获取真正命中的结果。

  • 第三步:格式化返回的数据。

  • 第四步:组装分页参数。

4.5 测试 ES 查询

4.5.1 实验一:测试 title 匹配

咱们如今想要验证 title 字段是否能匹配到,传的请求参数 keyword = 111,匹配到了 title = 111 的数据,且只有一条。页码 pageNum 我传的 1,表示返回第一页数据。以下图所示:

测试匹配 title

4.5.2 实验二:测试 answer 匹配

咱们如今想要验证 answer 字段是否能匹配到,传的请求参数 keyword = 测试答案,匹配到了 title =  测试答案的数据,且只有一条,说明查询成功。以下图所示:

测试匹配 answer

4.5.2 实验三:测试 id 匹配

咱们如今想要匹配题目 id 的话,须要传请求参数 id,并且 id 是精确匹配。另外 id 和 keyword 是取并集,因此不能传 keyword 字段。

请求参数 id = 5,返回结果也是 id =5 的数据,说明查询成功。以下图所示:

测试 id 匹配

5、总结

本文经过个人开源项目 passjava 来说解 ES 的整合,ES 的 API 使用以及测试。很是详细地讲解了每一步该如何作,相信经过阅读本篇后,再加上本身的实践,必定能掌握先后端该如何使用 ES 来达到高效搜索的目的。

固然,ES API 还有不少功能未在本文实践,有兴趣的同窗能够到 ES 官网进行查阅和学习。

再次强调:本文的代码都是辛苦调试出来的,请不要忘记点赞和转发哦~

- END -

写了两本 PDF, 回复  分布式  或  PDF  载。

个人 JVM 专栏已上架,回复  JVM  领取

我是悟空,努力变强,变身超级赛亚人!

本文分享自微信公众号 - 悟空聊架构(PassJava666)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索