Elasticsearch 9.x Java 异步客户端

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() ** 阻塞主线程 ,应通过 thenAcceptthenApply 等回调处理结果。

  • 示例:

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字段聚合报错或结果不符合预期。

  • 聚合结果解析:提前定义聚合名称常量,避免硬编码错误;解析时做非空判断,避免空指针异常。


六、运行说明

  1. 确保 Elasticsearch 9.x 服务已启动(本地 localhost:9200)。

  2. 调整连接配置(如 HTTPS、API Key、集群地址)以匹配你的环境。

  3. 运行 EsAsyncOperationsmain 方法,观察控制台输出的全流程执行结果。

相关推荐
马猴烧酒.2 小时前
【JAVA算法|hot100】哈希类型题目详解笔记
java·笔记
毕设源码-邱学长2 小时前
【开题答辩全过程】以 果蔬销售管理系统为例,包含答辩的问题和答案
java
Elastic 中国社区官方博客2 小时前
推出 Elastic Serverless Plus 附加组件,支持 AWS PrivateLink 功能
大数据·elasticsearch·搜索引擎·云原生·serverless·全文检索·aws
Drifter_yh2 小时前
「JVM」 Java 类加载机制与双亲委派模型深度解析
java·开发语言·jvm
马猴烧酒.2 小时前
【JAVA算法|hot100】数组类型题目详解笔记
java·笔记
范什么特西2 小时前
Tomcat加Maven配置
java·tomcat·maven
人生导师yxc2 小时前
IDE缓存配置等位置更改(自存)
java·ide·intellij-idea
indexsunny2 小时前
互联网大厂Java面试实战:Spring Boot与微服务在电商场景的应用
java·spring boot·微服务·面试·kafka·prometheus·电商
甲枫叶2 小时前
【claude产品经理系列13】核心功能实现——需求的增删改查全流程
java·前端·人工智能·python·产品经理·ai编程