ES 高级实战

PassJava (佳必过) 项目全套学习教程连载中,关注公众号悟空聊架构第一时间获取。

文档在线地址: www.passjava.cn

整合检索服务

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

1.1 添加搜索服务模块

  • 创建 passjava-search 模块。

首先我们在 PassJava-Platform 模块创建一个 搜索服务模块 passjava-search。然后勾选 spring web 服务。如下图所示。

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

选择 Spring Initializr

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

passjava-search 服务模块

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

mark

1.2 配置 Maven 依赖

  • 参照 ES 官网配置。

进入到 ES 官方网站,可以看到有低级和高级的 Rest Client,我们选择高阶的(High Level Rest Client)。然后进入到高阶 Rest Client 的 Maven 仓库。官网地址如下所示:

https:///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.class, args);
    }
}

接下来我们添加一个 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:///Jackson0714/PassJava-Platform
 */
@Configuration
public class PassJavaElasticsearchConfig {

    @Bean
    // 给容器注册一个 RestHighLevelClient,用来操作 ES
    // 参考官方文档:https:///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");
    ("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 字符串,结果如下所示:

<img src="http://cdn.jayh.club/blog/20201015/Q6OCLg0bwTMo.png?imageslim" alt="格式化 JSON 字符串" style="zoom:50%;" />

然后我们去掉其中的一些默认参数,最后简化后的检索参数放到 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 元。

打印结果:平均薪资