Springboot集成ElasticSearch7.x版本

276 阅读5分钟

本文由 简悦 SimpRead 转码, 原文地址 juejin.cn

前言

提起 ElasticSearch Java Client 你的第一反应肯定是 RestHighLevelClient,随着 7.X 版本的到来,Type 的概念被废除,为了适应这种数据结构的改变,ES 官方从 7.15 版本开始建议使用新的 ElasticSearch Java Client

如果你还对 RestHighLevelClient 或者 ES Rest API 还不了解参考:Elastic Stack-2:ElasticSearch API 使用

特性

  1. 所有的请求和相应使用强类型,使用泛型增强
  2. 支持同步和异步请求
  3. 使用 builders 模式,使复杂的请求变的流畅,良好的支持 lambda 表达式,简化代码,增强可读性

尝鲜

构建 ElasticsearchClient

maven 依赖

<dependency>
  <groupId>co.elastic.clients</groupId>
  <artifactId>elasticsearch-java</artifactId>
  <version>7.16.2</version>
</dependency>
复制代码

采坑指南:

    这个包默认已经引入了 jakarta.json-api 但是笔者在搭建环境的时候,发现没有依赖进去,如果报相关类找不到的问题可以尝试导入如下依赖:

<dependency>
  <groupId>jakarta.json</groupId>
  <artifactId>jakarta.json-api</artifactId>
  <version>2.0.1</version>
</dependency>
复制代码

yaml 配置

yaml 配置和 RestHighLevelClient 配置没有发生改变

## ElasticSearch 配置
elasticsearch:
  schema: http
  address: 139.198.152.90:9200
  connectTimeout: 10000
  socketTimeout: 15000
  connectionRequestTimeout: 20000
  maxConnectNum: 100
  maxConnectPerRoute: 100
  index: "aha"
复制代码

配置类

package com.aha.es.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * 连接 es 配置类
 *
 * @author WT
 * @date 2021/12/23 15:13:39
 */
@Data
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig {
    /**
     * 协议
     */
    private String schema;

    /**
     * 集群地址,如果有多个用“,”隔开
     */
    private String address;

    /**
     * 连接超时时间
     */
    private int connectTimeout;

    /**
     * Socket 连接超时时间
     */
    private int socketTimeout;

    /**
     * 获取连接的超时时间
     */
    private int connectionRequestTimeout;

    /**
     * 最大连接数
     */
    private int maxConnectNum;

    /**
     * 最大路由连接数
     */
    private int maxConnectPerRoute;

    /**
     * 连接ES的用户名
     */
    private String username;

    /**
     * 数据查询的索引
     */
    private String index;

    /**
     * 密码
     */
    private String passwd;

}
复制代码

构建 ElasticsearchClient

package com.aha.es.config;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * es java client
 * @author WT
 * @date 2021/12/27 15:34:09
 */
@Configuration
public class ElasticsearchClientConfig {

    private final ElasticSearchConfig elasticSearchConfig;

    public ElasticsearchClientConfig (ElasticSearchConfig elasticSearchConfig) {
        this.elasticSearchConfig = elasticSearchConfig;
    }

    @Bean
    public RestClient restClient() {

        // 拆分地址
        List<HttpHost> hostLists = new ArrayList<>();
        String[] hostArray = elasticSearchConfig.getAddress().split(",");
        for (String temp : hostArray) {
            String host = temp.split(":")[0];
            String port = temp.split(":")[1];
            hostLists.add(new HttpHost(host, Integer.parseInt(port), elasticSearchConfig.getSchema()));
        }

        // 转换成 HttpHost 数组
        HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});
        // 构建连接对象
        RestClientBuilder builder = RestClient.builder(httpHost);
        // 异步连接延时配置
        builder.setRequestConfigCallback(requestConfigBuilder -> {
            requestConfigBuilder.setConnectTimeout(elasticSearchConfig.getConnectTimeout());
            requestConfigBuilder.setSocketTimeout(elasticSearchConfig.getSocketTimeout());
            requestConfigBuilder.setConnectionRequestTimeout(elasticSearchConfig.getConnectionRequestTimeout());
            return requestConfigBuilder;
        });

        // 异步连接数配置
        builder.setHttpClientConfigCallback(httpClientBuilder -> {
            httpClientBuilder.setMaxConnTotal(elasticSearchConfig.getMaxConnectNum());
            httpClientBuilder.setMaxConnPerRoute(elasticSearchConfig.getMaxConnectPerRoute());
            return httpClientBuilder;
        });

        return builder.build();
    }

    @Bean
    public ElasticsearchTransport elasticsearchTransport (RestClient restClient) {
        return new RestClientTransport(
                restClient, new JacksonJsonpMapper());
    }

    @Bean
    public ElasticsearchClient elasticsearchClient (ElasticsearchTransport transport) {
        return new ElasticsearchClient(transport);
    }


}
复制代码

索引相关 API 示例

package com.aha.es;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.mapping.Property;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.DeleteIndexResponse;
import co.elastic.clients.elasticsearch.indices.GetIndexResponse;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import com.aha.es.pojo.AhaIndex;
import lombok.extern.slf4j.Slf4j;
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.Map;

/**
 * 测试 elasticSearch client 索引相关的操作
 *
 * @author WT
 * @date 2021/12/28 16:27:45
 */
@Slf4j
@SpringBootTest
public class ElasticsearchClientIndexTest {

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    // 创建索引 - 不指定 mapping
    @Test
    public void createIndex () throws IOException {
        CreateIndexResponse createIndexResponse = elasticsearchClient.indices()
                .create(createIndexRequest ->
                        createIndexRequest.index("elasticsearch-client")
                );
        log.info("== {} 索引创建是否成功: {}", "elasticsearch-client", createIndexResponse.acknowledged());
    }

    // 创建索引 - 指定 mapping
    @Test
    public void createIndexWithMapping () throws IOException {

        CreateIndexResponse createIndexResponse = elasticsearchClient.indices()
                .create(createIndexRequest ->
                        createIndexRequest.index("elasticsearch-client")
                        // 用 lambda 的方式 下面的 mapping 会覆盖上面的 mapping
                                .mappings(
                                        typeMapping ->
                                                typeMapping.properties("name", objectBuilder ->
                                                        objectBuilder.text(textProperty -> textProperty.fielddata(true))
                                                ).properties("age", objectBuilder ->
                                                        objectBuilder.integer(integerNumberProperty -> integerNumberProperty.index(true))
                                                )
                                )
                );

        log.info("== {} 索引创建是否成功: {}", "elasticsearch-client", createIndexResponse.acknowledged());
    }

    // 判断索引是否存在
    @Test
    public void indexIsExist () throws IOException {
        BooleanResponse booleanResponse = elasticsearchClient.indices()
                .exists(existsRequest ->
                        existsRequest.index("elasticsearch-client")
                );

        log.info("== {} 索引创建是否存在: {}", "elasticsearch-client", booleanResponse.value());
    }

    // 查看索引的相关信息
    @Test
    public void indexDetail () throws IOException {
        GetIndexResponse getIndexResponse = elasticsearchClient.indices()
                .get(getIndexRequest ->
                        getIndexRequest.index("elasticsearch-client")
                );

        Map<String, Property> properties = getIndexResponse.get("elasticsearch-client").mappings().properties();

        for (String key : properties.keySet()) {
            log.info("== {} 索引的详细信息为: == key: {}, Property: {}", "elasticsearch-client", key, properties.get(key)._kind());
        }

    }

    // 删除索引
    @Test
    public void deleteIndex () throws IOException {
        DeleteIndexResponse deleteIndexResponse = elasticsearchClient.indices()
                .delete(deleteIndexRequest ->
                        deleteIndexRequest.index("elasticsearch-client")
                );

        log.info("== {} 索引创建是否删除成功: {}", "elasticsearch-client", deleteIndexResponse.acknowledged());
    }

    @Test
    public void testRestClient () throws IOException {

        SearchResponse<AhaIndex> search = elasticsearchClient.search(s -> s.index("aha-batch")
                        .query(q -> q.term(t -> t
                                        .field("name")
                                        .value(v -> v.stringValue("1aha"))
                                )),
                AhaIndex.class);

        for (Hit<AhaIndex> hit: search.hits().hits()) {
            log.info("== hit: {}", hit.source());
        }

    }

}
复制代码

文档相关 API 示例

package com.aha.es;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.search.Hit;
import com.aha.es.pojo.AhaIndex;
import lombok.extern.slf4j.Slf4j;
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.ArrayList;
import java.util.List;

/**
 * 测试 elasticSearch client 文档相关的操作
 *
 * @author WT
 * @date 2021/12/28 16:27:45
 */
@Slf4j
@SpringBootTest
public class ElasticsearchClientDocumentTest {

    @Autowired
    private ElasticsearchClient elasticsearchClient;
    private static final String INDEX_NAME = "elasticsearch-client";

    // 添加文档
    @Test
    public void testAddDocument () throws IOException {

        IndexResponse indexResponse = elasticsearchClient.index(indexRequest ->
                indexRequest.index(INDEX_NAME).document(new AhaIndex().setName("wangWu").setAge(21))
        );
        log.info("== response: {}, responseStatus: {}", indexResponse, indexResponse.result());

    }

    // 获取文档信息
    @Test
    public void testGetDocument () throws IOException {
        GetResponse<AhaIndex> getResponse = elasticsearchClient.get(getRequest ->
                getRequest.index(INDEX_NAME).id("1"), AhaIndex.class
        );
        log.info("== document source: {}, response: {}", getResponse.source(), getResponse);
    }

    // 更新文档信息
    @Test
    public void testUpdateDocument () throws IOException {
        UpdateResponse<AhaIndex> updateResponse = elasticsearchClient.update(updateRequest ->
                updateRequest.index(INDEX_NAME).id("tU4YAH4B395pyiY3b46F")
                        .doc(new AhaIndex().setName("lisi1").setAge(22)), AhaIndex.class
        );
        log.info("== response: {}, responseStatus: {}", updateResponse, updateResponse.result());
    }

    // 删除文档信息
    @Test
    public void testDeleteDocument () throws IOException {
        DeleteResponse deleteResponse = elasticsearchClient.delete(deleteRequest ->
                deleteRequest.index(INDEX_NAME).id("1")
        );
        log.info("== response: {}, result:{}", deleteResponse, deleteResponse.result());

    }

    // 批量插入文档
    @Test
    public void testBatchInsert () throws IOException {

        List<BulkOperation> bulkOperationList = new ArrayList<>();

        for (int i=0; i<10; i++) {
            AhaIndex ahaIndex = new AhaIndex().setName("lisi" + i).setAge(20 + i);
            bulkOperationList.add(new BulkOperation.Builder().create(e -> e.document(ahaIndex)).build());
        }

        BulkResponse bulkResponse = elasticsearchClient.bulk(bulkRequest ->
                bulkRequest.index(INDEX_NAME).operations(bulkOperationList)
        );

        // 这边插入成功的话显示的是 false
        log.info("== errors: {}", bulkResponse.errors());
    }


    @Test
    public void testRestClient () throws IOException {

        SearchResponse<AhaIndex> search = elasticsearchClient.search(s -> s.index("aha-batch")
                        .query(q -> q.term(t -> t
                                        .field("name")
                                        .value(v -> v.stringValue("1aha"))
                                )),
                AhaIndex.class);

        for (Hit<AhaIndex> hit: search.hits().hits()) {
            log.info("== hit: {}", hit.source());
        }

    }

}
复制代码

\

搜索相关 API 示例

package com.aha.es;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.json.JsonData;
import com.aha.es.pojo.AhaIndex;
import lombok.extern.slf4j.Slf4j;
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;

/**
 * 测试 elasticSearch search API 相关的操作
 *
 * @author WT
 * @date 2021/12/28 16:27:45
 */
@Slf4j
@SpringBootTest
public class ElasticsearchClientSearchTest {

    @Autowired
    private ElasticsearchClient elasticsearchClient;
    private static final String INDEX_NAME = "elasticsearch-client";

    /**
     * 根据 name 查询相应的文档, search api 才是 elasticsearch-client 的优势,可以看出使用 lambda 大大简化了代码量,
     * 可以与 restHighLevelClient 形成鲜明的对比,但是也有可读性较差的问题,所以 lambda 的基础要扎实
     */
    @Test
    public void testRestClient () throws IOException {

        SearchResponse<AhaIndex> search = elasticsearchClient.search(s -> s.index(INDEX_NAME)
                        .query(q ->
                                q.term(t ->
                                        t.field("name").value(v -> v.stringValue("lisi1"))
                                )
                        ),
                AhaIndex.class);

        for (Hit<AhaIndex> hit: search.hits().hits()) {
            log.info("== hit: source: {}, id: {}", hit.source(), hit.id());
        }

    }

    // 多条件 返回查询
    @Test
    public void testMultipleCondition () throws IOException {

        SearchRequest request = SearchRequest.of(searchRequest ->
                searchRequest.index(INDEX_NAME).from(0).size(20).sort(s -> s.field(f -> f.field("age").order(SortOrder.Desc)))
                        // 如果有多个 .query 后面的 query 会覆盖前面的 query
                        .query(query ->
                                query.bool(boolQuery ->
                                        boolQuery
                                                // 在同一个 boolQuery 中 must 会将 should 覆盖
                                                .must(must -> must.range(
                                                        e -> e.field("age").gte(JsonData.of("21")).lte(JsonData.of("25"))
                                                ))
                                                .mustNot(mustNot -> mustNot.term(
                                                        e -> e.field("name").value(value -> value.stringValue("lisi1"))
                                                ))
                                                .should(must -> must.term(
                                                        e -> e.field("name").value(value -> value.stringValue("lisi2"))
                                                ))
                                )
                        )

        );

        SearchResponse<AhaIndex> searchResponse = elasticsearchClient.search(request, AhaIndex.class);


        log.info("返回的总条数有:{}", searchResponse.hits().total().value());
        List<Hit<AhaIndex>> hitList = searchResponse.hits().hits();
        for (Hit<AhaIndex> hit : hitList) {
            log.info("== hit: {}, id: {}", hit.source(), hit.id());
        }

    }

}
复制代码

should 条件示例

SearchRequest request = SearchRequest.of(searchRequest ->
                searchRequest.index(INDEX_NAME).from(0).size(20).sort(s -> s.field(f -> f.field("age").order(SortOrder.Desc)))
                        .query(query ->
                                query.bool(boolQuery ->
                                        boolQuery
                                                // 两个 should 连用是没有问题的
                                                .should(must -> must.term(
                                                        e -> e.field("age").value(value -> value.stringValue("22"))
                                                ))
                                                .should(must -> must.term(
                                                        e -> e.field("age").value(value -> value.stringValue("23"))
                                                ))
                                )
                        ));
复制代码

小结

从示例代码中可以看出,新版客户端的特点便是 构造器的使用泛型的支持以及 Lambda 的支持了。对于 lambda 而言代码简化是毋庸置疑的,但是可读性和调试代码方面是众说纷纭的。关于新版客户端官网给出的文档也较少,本文也只是尝鲜,可以待它稳定之后在考虑生产环境的使用。