本文由 简悦 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 使用 。
特性
- 所有的请求和相应使用强类型,使用泛型增强
- 支持同步和异步请求
- 使用
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 而言代码简化是毋庸置疑的,但是可读性和调试代码方面是众说纷纭的。关于新版客户端官网给出的文档也较少,本文也只是尝鲜,可以待它稳定之后在考虑生产环境的使用。