ElasticsearchAsyncClient 完整示例,涵盖异步连接、核心操作(索引/文档 CRUD、搜索)、异常处理及最佳实践,批量异步操作 与聚合查询异步实现的完整示例。
一、环境与依赖
1. 环境要求
-
Java 17+
-
Elasticsearch 服务端 9.x(与客户端版本一致)
2. Maven 依赖
XML
<dependencies>
<!-- Elasticsearch Java 客户端(包含异步支持) -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.3.0</version>
</dependency>
<!-- JSON 映射(Jackson) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- 日志(可选,用于调试) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
二、异步客户端连接配置
1. 基础连接(HTTP)
Java
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
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;
public class EsAsyncClientConfig {
// 1. 创建底层 RestClient(配置超时、连接池等)
private static RestClient createRestClient() {
return RestClient.builder(new HttpHost("localhost", 9200, "http"))
.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
.setConnectTimeout(5000) // 连接超时:5秒
.setSocketTimeout(60000) // 套接字超时:60秒
.setConnectionRequestTimeout(5000) // 从连接池获取连接超时
)
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
.setMaxConnTotal(100) // 总连接数
.setMaxConnPerRoute(50) // 每个路由的最大连接数
)
.build();
}
// 2. 创建异步客户端
public static ElasticsearchAsyncClient createAsyncClient() {
RestClient restClient = createRestClient();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper()
);
return new ElasticsearchAsyncClient(transport);
}
// 3. 关闭资源
public static void closeClient(ElasticsearchAsyncClient client) {
try {
client._transport().close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2. HTTPS 安全连接(生产环境)
Java
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import javax.net.ssl.SSLContext;
private static RestClient createHttpsRestClient() throws Exception {
// 1. 构建 SSL 上下文(信任自签名证书,仅用于开发!)
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial(new TrustSelfSignedStrategy())
.build();
// 2. 创建 RestClient
return RestClient.builder(new HttpHost("localhost", 9200, "https"))
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
.setSSLContext(sslContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // 跳过主机名验证
)
.build();
}
三、数据模型定义
Java
// Product.java(必须有无参构造函数和 Getter/Setter)
public class Product {
private String sku;
private String name;
private Double price;
private String description;
private String category;
// 无参构造函数(Jackson 反序列化必需)
public Product() {}
public Product(String sku, String name, Double price, String category) {
this.sku = sku;
this.name = name;
this.price = price;
this.category = category;
}
// Getter & Setter
public String getSku() { return sku; }
public void setSku(String sku) { this.sku = sku; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
}
四、异步核心操作完整示例
Java
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.DeleteIndexResponse;
import co.elastic.clients.elasticsearch._types.Refresh;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.elasticsearch._types.aggregations.*;
import co.elastic.clients.json.JsonData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class EsAsyncOperations {
public static void main(String[] args) {
// 1. 创建异步客户端
ElasticsearchAsyncClient asyncClient = EsAsyncClientConfig.createAsyncClient();
String indexName = "products_async";
try {
// 2. 执行异步操作(链式调用示例)
runAllAsyncOperations(asyncClient, indexName)
.thenRun(() -> System.out.println("所有异步操作完成!"))
.exceptionally(e -> {
System.err.println("异步操作异常:" + e.getMessage());
return null;
})
.join(); // 阻塞等待所有操作完成(仅用于演示,生产环境避免)
} finally {
// 3. 关闭资源
EsAsyncClientConfig.closeClient(asyncClient);
}
}
// 组合所有异步操作
private static CompletableFuture<Void> runAllAsyncOperations(
ElasticsearchAsyncClient client, String indexName) {
return createIndexAsync(client, indexName)
.thenCompose(v -> batchIndexDocumentsAsync(client, indexName)) // 批量插入测试数据
.thenCompose(v -> indexDocumentAsync(client, indexName)) // 单条文档索引
.thenCompose(v -> getDocumentAsync(client, indexName)) // 单条文档查询
.thenCompose(v -> searchDocumentsAsync(client, indexName)) // 条件搜索
.thenCompose(v -> aggregateDocumentsAsync(client, indexName)) // 聚合查询
.thenCompose(v -> updateDocumentAsync(client, indexName)) // 文档更新
.thenCompose(v -> deleteDocumentAsync(client, indexName)) // 单条文档删除
.thenCompose(v -> deleteIndexAsync(client, indexName)); // 索引删除
}
// ------------------------------
// 1. 异步创建索引
// ------------------------------
private static CompletableFuture<Void> createIndexAsync(
ElasticsearchAsyncClient client, String indexName) {
// 定义字段映射
Map<String, co.elastic.clients.elasticsearch._types.mapping.Property> properties = new HashMap<>();
properties.put("sku", co.elastic.clients.elasticsearch._types.mapping.Property.of(
p -> p.keyword(k -> k)
));
properties.put("name", co.elastic.clients.elasticsearch._types.mapping.Property.of(
p -> p.text(t -> t)
));
properties.put("price", co.elastic.clients.elasticsearch._types.mapping.Property.of(
p -> p.double_(d -> d)
));
properties.put("category", co.elastic.clients.elasticsearch._types.mapping.Property.of(
p -> p.keyword(k -> k)
));
properties.put("description", co.elastic.clients.elasticsearch._types.mapping.Property.of(
p -> p.text(t -> t)
));
return client.indices().create(c -> c
.index(indexName)
.settings(s -> s.numberOfShards("3").numberOfReplicas("1"))
.mappings(m -> m.properties(properties))
)
.thenAccept(response -> System.out.println("索引创建成功:" + response.acknowledged()))
.exceptionally(e -> {
System.err.println("索引创建失败:" + e.getMessage());
return null;
});
}
// ------------------------------
// 2. 异步批量文档操作(Bulk 生产级核心)
// ------------------------------
private static CompletableFuture<Void> batchIndexDocumentsAsync(
ElasticsearchAsyncClient client, String indexName) {
// 1. 构建批量操作列表(支持插入、更新、删除混合操作)
List<BulkOperation> bulkOperations = new ArrayList<>();
// 批量插入测试数据
List<Product> productList = List.of(
new Product("bk-1", "City Bike", 123.0, "bike"),
new Product("bk-2", "Mountain Bike", 299.0, "bike"),
new Product("bk-3", "Electric Bike", 899.0, "ebike"),
new Product("helmet-1", "Safety Helmet", 49.0, "accessory"),
new Product("helmet-2", "Professional Helmet", 89.0, "accessory"),
new Product("lock-1", "Anti-theft Lock", 29.0, "accessory")
);
// 循环添加批量索引操作
for (Product product : productList) {
bulkOperations.add(BulkOperation.of(op -> op
.index(idx -> idx
.index(indexName)
.id(product.getSku())
.document(product)
)
));
}
// 可选:添加批量更新/删除操作示例
// bulkOperations.add(BulkOperation.of(op -> op.update(u -> u.index(indexName).id("bk-1").doc(Map.of("price", 150.0)))));
// bulkOperations.add(BulkOperation.of(op -> op.delete(d -> d.index(indexName).id("lock-1"))));
// 2. 执行异步批量操作
return client.bulk(b -> b
.operations(bulkOperations)
.refresh(Refresh.WaitFor) // 等待刷新保证数据可见
)
.thenAccept(response -> {
// 3. 生产级必做:遍历每个操作的结果,判断是否失败
long successCount = 0;
long failCount = 0;
for (BulkResponseItem item : response.items()) {
if (item.error() == null) {
successCount++;
} else {
failCount++;
System.err.println("批量操作失败,文档ID:" + item.id() + ",失败原因:" + item.error().reason());
}
}
System.out.println("批量操作完成,成功:" + successCount + ",失败:" + failCount);
})
.exceptionally(e -> {
System.err.println("批量操作整体失败:" + e.getMessage());
return null;
});
}
// ------------------------------
// 3. 异步单条索引文档
// ------------------------------
private static CompletableFuture<Void> indexDocumentAsync(
ElasticsearchAsyncClient client, String indexName) {
Product product = new Product("bk-4", "Folding Bike", 199.0, "bike");
return client.index(i -> i
.index(indexName)
.id(product.getSku())
.document(product)
.refresh(Refresh.WaitFor) // 等待刷新后返回
)
.thenAccept(response -> System.out.println("单条文档索引成功,版本:" + response.version()))
.exceptionally(e -> {
System.err.println("单条文档索引失败:" + e.getMessage());
return null;
});
}
// ------------------------------
// 4. 异步获取文档
// ------------------------------
private static CompletableFuture<Void> getDocumentAsync(
ElasticsearchAsyncClient client, String indexName) {
return client.get(g -> g
.index(indexName)
.id("bk-1")
.source(s -> s.filter(f -> f.includes("name", "price", "category"))) // 字段过滤
, Product.class)
.thenAccept(response -> {
if (response.found()) {
Product product = response.source();
System.out.println("获取文档成功:" + product.getName() + ",价格:" + product.getPrice() + ",分类:" + product.getCategory());
} else {
System.out.println("文档不存在");
}
})
.exceptionally(e -> {
System.err.println("获取文档失败:" + e.getMessage());
return null;
});
}
// ------------------------------
// 5. 异步搜索文档(组合查询 + 分页 + 高亮)
// ------------------------------
private static CompletableFuture<Void> searchDocumentsAsync(
ElasticsearchAsyncClient client, String indexName) {
// 构建组合查询:名称包含 "Bike" 且价格在 100-1000 之间
Query matchQuery = Query.of(q -> q.match(m -> m.field("name").query("Bike")));
Query rangeQuery = Query.of(q -> q.range(r -> r.field("price").gte(JsonData.of(100)).lte(JsonData.of(1000))));
Query boolQuery = Query.of(q -> q.bool(b -> b.must(matchQuery).filter(rangeQuery)));
return client.search(s -> s
.index(indexName)
.query(boolQuery)
.from(0)
.size(10)
.sort(sort -> sort.field(f -> f.field("price").order(SortOrder.Asc)))
.highlight(h -> h.fields("name", f -> f))
, Product.class)
.thenAccept(response -> {
System.out.println("搜索命中总数:" + response.hits().total().value());
for (Hit<Product> hit : response.hits().hits()) {
Product product = hit.source();
System.out.println("搜索结果:" + product.getName() + ",价格:" + product.getPrice());
if (hit.highlight() != null) {
System.out.println("高亮名称:" + hit.highlight().get("name").get(0));
}
}
})
.exceptionally(e -> {
System.err.println("搜索失败:" + e.getMessage());
return null;
});
}
// ------------------------------
// 6. 异步聚合查询(生产级完整实现)
// ------------------------------
private static CompletableFuture<Void> aggregateDocumentsAsync(
ElasticsearchAsyncClient client, String indexName) {
// 聚合名称常量(避免硬编码,方便解析)
String AGG_CATEGORY_TERMS = "category_terms";
String AGG_PRICE_STATS = "price_stats";
String AGG_PRICE_RANGE = "price_range";
String AGG_RANGE_AVG_PRICE = "range_avg_price";
return client.search(s -> s
.index(indexName)
.size(0) // 聚合查询无需返回原始文档,设置size=0提升性能
// 1. 分类分桶聚合:按商品分类统计文档数量
.aggregations(AGG_CATEGORY_TERMS, Aggregation.of(a -> a
.terms(t -> t.field("category").size(10))
))
// 2. 指标聚合:统计全量商品价格的总和、平均值、最大值、最小值
.aggregations(AGG_PRICE_STATS, Aggregation.of(a -> a
.stats(st -> st.field("price"))
))
// 3. 范围分桶+嵌套聚合:按价格区间分桶,同时统计每个区间的平均价格
.aggregations(AGG_PRICE_RANGE, Aggregation.of(a -> a
.range(r -> r
.field("price")
.ranges(rr -> rr.to(JsonData.of(100)).key("0-100"))
.ranges(rr -> rr.from(JsonData.of(100)).to(JsonData.of(500)).key("100-500"))
.ranges(rr -> rr.from(JsonData.of(500)).key("500+"))
)
// 嵌套聚合:每个价格区间内的平均价格
.aggregations(AGG_RANGE_AVG_PRICE, Aggregation.of(aa -> aa
.avg(av -> av.field("price"))
))
))
, Product.class)
.thenAccept(response -> {
System.out.println("==================== 聚合查询结果 ====================");
// 解析分类分桶聚合结果
TermsAggregate categoryAgg = response.aggregations().get(AGG_CATEGORY_TERMS).terms();
System.out.println("\n【商品分类统计】");
for (TermsBucket bucket : categoryAgg.buckets().array()) {
System.out.println("分类:" + bucket.key().stringValue() + ",商品数量:" + bucket.docCount());
}
// 解析价格指标聚合结果
StatsAggregate priceStats = response.aggregations().get(AGG_PRICE_STATS).stats();
System.out.println("\n【全量商品价格统计】");
System.out.println("商品总数:" + priceStats.count());
System.out.println("最低价格:" + priceStats.min());
System.out.println("最高价格:" + priceStats.max());
System.out.println("平均价格:" + priceStats.avg());
System.out.println("价格总和:" + priceStats.sum());
// 解析价格范围分桶+嵌套聚合结果
RangeAggregate priceRangeAgg = response.aggregations().get(AGG_PRICE_RANGE).range();
System.out.println("\n【价格区间统计】");
for (RangeBucket bucket : priceRangeAgg.buckets().array()) {
double avgPrice = bucket.aggregations().get(AGG_RANGE_AVG_PRICE).avg().value();
System.out.println("价格区间:" + bucket.key() + ",商品数量:" + bucket.docCount() + ",区间平均价格:" + avgPrice);
}
System.out.println("======================================================");
})
.exceptionally(e -> {
System.err.println("聚合查询失败:" + e.getMessage());
return null;
});
}
// ------------------------------
// 7. 异步更新文档(部分更新 + 脚本更新)
// ------------------------------
private static CompletableFuture<Void> updateDocumentAsync(
ElasticsearchAsyncClient client, String indexName) {
// 7.1 部分更新:新增 description 字段,更新价格
Map<String, Object> updateFields = new HashMap<>();
updateFields.put("price", 150.0);
updateFields.put("description", "A comfortable city bike for daily commuting");
CompletableFuture<Void> partialUpdate = client.update(u -> u
.index(indexName)
.id("bk-1")
.doc(updateFields)
, Product.class)
.thenAccept(response -> System.out.println("部分更新成功:" + response.result()))
.exceptionally(e -> {
System.err.println("部分更新失败:" + e.getMessage());
return null;
});
// 7.2 脚本更新:价格上涨 10%
String script = "ctx._source.price *= 1.1";
CompletableFuture<Void> scriptUpdate = client.update(u -> u
.index(indexName)
.id("bk-1")
.script(s -> s.inline(i -> i.lang("painless").source(script)))
, Product.class)
.thenAccept(response -> System.out.println("脚本更新成功:" + response.result()))
.exceptionally(e -> {
System.err.println("脚本更新失败:" + e.getMessage());
return null;
});
// 组合两个更新操作(并行执行)
return CompletableFuture.allOf(partialUpdate, scriptUpdate);
}
// ------------------------------
// 8. 异步删除文档
// ------------------------------
private static CompletableFuture<Void> deleteDocumentAsync(
ElasticsearchAsyncClient client, String indexName) {
return client.delete(d -> d.index(indexName).id("bk-1"))
.thenAccept(response -> System.out.println("文档删除结果:" + response.result()))
.exceptionally(e -> {
System.err.println("文档删除失败:" + e.getMessage());
return null;
});
}
// ------------------------------
// 9. 异步删除索引
// ------------------------------
private static CompletableFuture<Void> deleteIndexAsync(
ElasticsearchAsyncClient client, String indexName) {
return client.indices().exists(e -> e.index(indexName))
.thenCompose(exists -> {
if (exists.value()) {
return client.indices().delete(d -> d.index(indexName))
.thenAccept(response -> System.out.println("索引删除成功:" + response.acknowledged()));
} else {
System.out.println("索引不存在,无需删除");
return CompletableFuture.completedFuture(null);
}
})
.exceptionally(e -> {
System.err.println("索引删除失败:" + e.getMessage());
return null;
});
}
}
五、异步客户端最佳实践
1. 避免阻塞
-
生产环境不要使用 **
join()** 或 **get()** 阻塞主线程 ,应通过thenAccept、thenApply等回调处理结果。 -
示例:
Java
// ❌ 错误:阻塞等待
response.join();
// ✅ 正确:异步回调
response.thenAccept(result -> processResult(result));
2. 异常处理
-
使用
exceptionally()捕获单个操作异常,使用handle()同时处理成功和失败。 -
示例:
Java
client.index(...)
.handle((response, e) -> {
if (e != null) {
logError(e);
return null;
}
return processResponse(response);
});
3. 异步操作组合
-
串行执行 :使用
thenCompose(),保证操作顺序。 -
并行执行 :使用
CompletableFuture.allOf(),提升批量操作吞吐量。 -
示例:
Java
// 并行执行多个索引操作
CompletableFuture<Void> index1 = indexDocumentAsync(client, "product1");
CompletableFuture<Void> index2 = indexDocumentAsync(client, "product2");
CompletableFuture.allOf(index1, index2)
.thenRun(() -> System.out.println("所有索引操作完成"));
4. 资源管理
-
使用
try-with-resources或在finally块中关闭客户端,避免连接泄漏。 -
示例:
Java
try (ElasticsearchAsyncClient client = createAsyncClient()) {
// 执行操作
} // 自动关闭底层连接
5. 线程池配置
-
异步回调默认在
ForkJoinPool.commonPool()中执行,IO密集型操作建议通过thenAcceptAsync()指定自定义线程池。 -
示例:
Java
ExecutorService customExecutor = Executors.newFixedThreadPool(10);
client.index(...)
.thenAcceptAsync(response -> processResponse(response), customExecutor);
6. 批量操作最佳实践
-
批次大小控制:单批次文档总量建议控制在 1000-5000 条,单批次总大小不超过 10MB,避免OOM和服务端压力过大。
-
失败处理 :必须遍历
BulkResponseItem检查单条操作结果,不能仅依赖整体请求是否成功。 -
刷新策略 :批量操作完成后统一使用
refresh=WaitFor,避免单条操作频繁刷新导致性能下降。 -
限流保护 :高并发批量写入时,结合
Semaphore控制并发批次数量,避免压垮ES集群。
7. 聚合查询最佳实践
-
关闭原始文档返回 :纯聚合查询设置
size=0,减少网络传输和内存开销。 -
聚合层级控制:避免超过3层嵌套聚合,减少服务端计算压力。
-
关键字段聚合 :分桶聚合必须使用
keyword类型字段,避免text字段聚合报错或结果不符合预期。 -
聚合结果解析:提前定义聚合名称常量,避免硬编码错误;解析时做非空判断,避免空指针异常。
六、运行说明
-
确保 Elasticsearch 9.x 服务已启动(本地
localhost:9200)。 -
调整连接配置(如 HTTPS、API Key、集群地址)以匹配你的环境。
-
运行
EsAsyncOperations的main方法,观察控制台输出的全流程执行结果。