Spring Boot 整合 Elasticsearch 實戰

武培軒發表於2020-08-07

最近有讀者問我能不能寫下如何使用 Spring Boot 開發 Elasticsearch(以下簡稱 ES) 相關應用,今天就講解下如何使用 Spring Boot 結合 ES。

可以在 ES 官方文件中發現,ES 為 Java REST Client 提供了兩種方式的 Client:Java Low Level ClientJava High Level REST Client

低階別客戶端,它允許通過 HTTP 請求與 ES 叢集進行通訊,API 本身不負責資料的編碼解碼,由使用者去編碼解碼,它與所有的 ES 版本相容。

高階客戶端基於低階客戶端,是從 6.0 才開始加入的,主要目標是為了暴露各 API 特定的方法,高版本客戶端依賴於 ES 核心專案,將 Request 物件作為引數,返回一個 Response 物件,所有 API 都可以同步或非同步呼叫。

本文就通過 Spring Boot 結合 Java High Level REST Client 來進行一些演示。

ES 環境搭建可以參加文章:全文搜尋引擎 Elasticsearch 入門:叢集搭建

Spring Boot 整合 ES

Spring Boot 整合 ES 主要分為以下三步:

  1. 加入 ES 依賴
  2. 配置 ES
  3. 演示 ES 基本操作

加入依賴

首先建立一個專案,在專案中加入 ES 相關依賴,具體依賴如下所示:

<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.1.0</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.1.0</version>
</dependency>

建立 ES 配置

在配置檔案 application.properties 中配置 ES 的相關引數,具體內容如下:

elasticsearch.host=localhost
elasticsearch.port=9200
elasticsearch.connTimeout=3000
elasticsearch.socketTimeout=5000
elasticsearch.connectionRequestTimeout=500

其中指定了 ES 的 host 和埠以及超時時間的設定,另外我們的 ES 沒有新增任何的安全認證,因此 username 和 password 就沒有設定。

然後在 config 包下建立 ElasticsearchConfiguration 類,會從配置檔案中讀取到對應的引數,接著申明一個 initRestClient 方法,返回的是一個 RestHighLevelClient,同時為它新增 @Bean(destroyMethod = "close") 註解,當 destroy 的時候做一個關閉,這個方法主要是如何初始化並建立一個 RestHighLevelClient

@Configuration
public class ElasticsearchConfiguration {

    @Value("${elasticsearch.host}")
    private String host;

    @Value("${elasticsearch.port}")
    private int port;

    @Value("${elasticsearch.connTimeout}")
    private int connTimeout;

    @Value("${elasticsearch.socketTimeout}")
    private int socketTimeout;

    @Value("${elasticsearch.connectionRequestTimeout}")
    private int connectionRequestTimeout;

    @Bean(destroyMethod = "close", name = "client")
    public RestHighLevelClient initRestClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port))
                .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
                        .setConnectTimeout(connTimeout)
                        .setSocketTimeout(socketTimeout)
                        .setConnectionRequestTimeout(connectionRequestTimeout));
        return new RestHighLevelClient(builder);
    }
}

定義文件實體類

首先在 constant 包下定義常量介面,在介面中定義索引的名字為 user

public interface Constant {
    String INDEX = "user";
}

然後在 document 包下建立一個文件實體類:

public class UserDocument {
    private String id;
    private String name;
    private String sex;
    private Integer age;
    private String city;
    // 省略 getter/setter
}

ES 基本操作

在這裡主要介紹 ES 的索引、文件、搜尋相關的簡單操作,在 service 包下建立 UserService 類。

索引操作

在這裡演示建立索引和刪除索引:

建立索引

在建立索引的時候可以在 CreateIndexRequest 中設定索引名稱、分片數、副本數以及 mappings,在這裡索引名稱為 user,分片數 number_of_shards 為 1,副本數 number_of_replicas 為 0,具體程式碼如下所示:

public boolean createUserIndex(String index) throws IOException {
    CreateIndexRequest createIndexRequest = new CreateIndexRequest(index);
    createIndexRequest.settings(Settings.builder()
            .put("index.number_of_shards", 1)
            .put("index.number_of_replicas", 0)
    );
    createIndexRequest.mapping("{\n" +
            "  \"properties\": {\n" +
            "    \"city\": {\n" +
            "      \"type\": \"keyword\"\n" +
            "    },\n" +
            "    \"sex\": {\n" +
            "      \"type\": \"keyword\"\n" +
            "    },\n" +
            "    \"name\": {\n" +
            "      \"type\": \"keyword\"\n" +
            "    },\n" +
            "    \"id\": {\n" +
            "      \"type\": \"keyword\"\n" +
            "    },\n" +
            "    \"age\": {\n" +
            "      \"type\": \"integer\"\n" +
            "    }\n" +
            "  }\n" +
            "}", XContentType.JSON);
    CreateIndexResponse createIndexResponse = client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
    return createIndexResponse.isAcknowledged();
}

通過呼叫該方法,就可以建立一個索引 user,索引資訊如下:

關於 ES 的 Mapping 可以看下這篇文章:一文搞懂 Elasticsearch 之 Mapping

刪除索引

DeleteIndexRequest 中傳入索引名稱就可以刪除索引,具體程式碼如下所示:

public Boolean deleteUserIndex(String index) throws IOException {
    DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(index);
    AcknowledgedResponse deleteIndexResponse = client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
    return deleteIndexResponse.isAcknowledged();
}

介紹完索引的基本操作,下面介紹文件的相關操作:

文件操作

對 ES 文件還不是很熟悉的可以先看下這篇文章:ElasticSearch 文件的增刪改查都不會?

在這裡演示下建立文件、批量建立文件、檢視文件、更新文件以及刪除文件:

建立文件

建立文件的時候需要在 IndexRequest 中指定索引名稱,id 如果不傳的話會由 ES 自動生成,然後傳入 source,具體程式碼如下:

public Boolean createUserDocument(UserDocument document) throws Exception {
    UUID uuid = UUID.randomUUID();
    document.setId(uuid.toString());
    IndexRequest indexRequest = new IndexRequest(Constant.INDEX)
            .id(document.getId())
            .source(JSON.toJSONString(document), XContentType.JSON);
    IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
    return indexResponse.status().equals(RestStatus.OK);
}

下面通過呼叫這個方法,建立兩個文件,具體內容如下:

批量建立文件

在一個 REST 請求中,重新建立網路開銷是十分損耗效能的,因此 ES 提供 Bulk API,支援在一次 API 呼叫中,對不同的索引進行操作,從而減少網路傳輸開銷,提升寫入速率。

下面方法是批量建立文件,一個 BulkRequest 裡可以新增多個 Request,具體程式碼如下:

public Boolean bulkCreateUserDocument(List<UserDocument> documents) throws IOException {
    BulkRequest bulkRequest = new BulkRequest();
    for (UserDocument document : documents) {
        String id = UUID.randomUUID().toString();
        document.setId(id);
        IndexRequest indexRequest = new IndexRequest(Constant.INDEX)
                .id(id)
                .source(JSON.toJSONString(document), XContentType.JSON);
        bulkRequest.add(indexRequest);
    }
    BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
    return bulkResponse.status().equals(RestStatus.OK);
}

下面通過該方法建立些文件,便於下面的搜尋演示。

檢視文件

檢視文件需要在 GetRequest 中傳入索引名稱和文件 id,具體程式碼如下所示:

public UserDocument getUserDocument(String id) throws IOException {
    GetRequest getRequest = new GetRequest(Constant.INDEX, id);
    GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
    UserDocument result = new UserDocument();
    if (getResponse.isExists()) {
        String sourceAsString = getResponse.getSourceAsString();
        result = JSON.parseObject(sourceAsString, UserDocument.class);
    } else {
        logger.error("沒有找到該 id 的文件");
    }
    return result;
}

下面傳入文件 id 呼叫該方法,結果如下所示:

更新文件

更新文件則是先給 UpdateRequest 傳入索引名稱和文件 id,然後通過傳入新的 doc 來進行更新,具體程式碼如下:

public Boolean updateUserDocument(UserDocument document) throws Exception {
    UserDocument resultDocument = getUserDocument(document.getId());
    UpdateRequest updateRequest = new UpdateRequest(Constant.INDEX, resultDocument.getId());
    updateRequest.doc(JSON.toJSONString(document), XContentType.JSON);
    UpdateResponse updateResponse = client.update(updateRequest, RequestOptions.DEFAULT);
    return updateResponse.status().equals(RestStatus.OK);
}

下面將文件 id 為 9b8d9897-3352-4ef3-9636-afc6fce43b20 的文件的城市資訊改為 handan,呼叫方法結果如下:

刪除文件

刪除文件只需要在 DeleteRequest 中傳入索引名稱和文件 id,然後執行 delete 方法就可以完成文件的刪除,具體程式碼如下:

public String deleteUserDocument(String id) throws Exception {
    DeleteRequest deleteRequest = new DeleteRequest(Constant.INDEX, id);
    DeleteResponse response = client.delete(deleteRequest, RequestOptions.DEFAULT);
    return response.getResult().name();
}

介紹完文件的基本操作,接下來對搜尋進行簡單介紹:

搜尋操作

對 ES 的 DSL 語法還不是很熟悉的可以先看下這篇文章:看完這篇還不會 Elasticsearch 搜尋,那我就哭了!

簡單的搜尋操作需要在 SearchRequest 中設定將要搜尋的索引名稱(可以設定多個索引名稱),然後通過 SearchSourceBuilder 構造搜尋源,下面將 TermQueryBuilder 搜尋查詢傳給 searchSourceBuilder,最後將 searchRequest 的搜尋源設定為 searchSourceBuilder,執行 search 方法實現通過城市進行搜尋,具體程式碼如下所示:

public List<UserDocument> searchUserByCity(String city) throws Exception {
    SearchRequest searchRequest = new SearchRequest();
    searchRequest.indices(Constant.INDEX);
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("city", city);
    searchSourceBuilder.query(termQueryBuilder);
    searchRequest.source(searchSourceBuilder);
    SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    return getSearchResult(searchResponse);
}

該方法的執行結果如圖所示:

聚合搜尋

ES 聚合搜尋相關知識可以看下這篇文章:Elasticsearch 之聚合分析入門

聚合搜尋就是給 searchSourceBuilder 新增聚合搜尋,下面方法是通過 TermsAggregationBuilder 構造一個先通過城市就行分類聚合,其中還包括一個子聚合,是對年齡求平均值,然後在獲取聚合結果的時候,可以使用通過在構建聚合時的聚合名稱獲取到聚合結果,具體程式碼如下所示:

public List<UserCityDTO> aggregationsSearchUser() throws Exception {
    SearchRequest searchRequest = new SearchRequest(Constant.INDEX);
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    TermsAggregationBuilder aggregation = AggregationBuilders.terms("by_city")
            .field("city")
            .subAggregation(AggregationBuilders
                    .avg("average_age")
                    .field("age"));
    searchSourceBuilder.aggregation(aggregation);
    searchRequest.source(searchSourceBuilder);
    SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    Aggregations aggregations = searchResponse.getAggregations();
    Terms byCityAggregation = aggregations.get("by_city");
    List<UserCityDTO> userCityList = new ArrayList<>();
    for (Terms.Bucket buck : byCityAggregation.getBuckets()) {
        UserCityDTO userCityDTO = new UserCityDTO();
        userCityDTO.setCity(buck.getKeyAsString());
        userCityDTO.setCount(buck.getDocCount());
        // 獲取子聚合
        Avg averageBalance = buck.getAggregations().get("average_age");
        userCityDTO.setAvgAge(averageBalance.getValue());
        userCityList.add(userCityDTO);
    }
    return userCityList;
}

下面是執行該方法的結果:

到此為止,ES 的基本操作就簡單介紹完了,大家可以多動手試試,不會的可以看下官方文件。

總結

本文的完整程式碼在 https://github.com/wupeixuan/SpringBoot-Learnelasticsearch 目錄下。

Spring Boot 結合 ES 還是比較簡單的,大家可以下載專案原始碼,自己在本地執行除錯這個專案,更好地理解如何在 Spring Boot 中構建基於 ES 的應用。

最好的關係就是互相成就,大家的點贊、在看、分享、留言就是我創作的最大動力。

參考

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html

https://github.com/wupeixuan/SpringBoot-Learn

相關文章