最近有讀者問我能不能寫下如何使用 Spring Boot 開發 Elasticsearch(以下簡稱 ES) 相關應用,今天就講解下如何使用 Spring Boot 結合 ES。
可以在 ES 官方文檔中發現,ES 為 Java REST Client 提供了兩種方式的 Client:Java Low Level Client
和 Java 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 主要分為以下三步:
- 加入 ES 依賴
- 配置 ES
- 演示 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-Learn
的 elasticsearch
目錄下。
Spring Boot 結合 ES 還是比較簡單的,大家可以下載項目源碼,自己在本地運行調試這個項目,更好地理解如何在 Spring Boot 中構建基於 ES 的應用。
最好的關系就是互相成就,大家的點贊、在看、分享、留言就是我創作的最大動力。
參考
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html