Elasticsearch 9.X Java客户端API实战指南(入门教学版)

0 阅读20分钟

前言

在现代搜索与数据分析场景中,Elasticsearch(以下简称ES)早已成为核心组件,广泛应用于日志分析、全文检索数据可视化等领域。随着ES 9.X版本的正式发布,其官方Java客户端API迎来了重大更新——RestHighLevelClient被彻底废弃,取而代之的是全新的elasticsearch-java客户端。

对于习惯了低版本ES与旧客户端的开发者而言,这次更新无疑增加了上手成本。官方新客户端采用了更简洁的Fluent DSL语法,更轻量的依赖设计,且能与ES服务端API实时同步演进,从根本上解决了旧客户端依赖耦合重、请求构建臃肿、与REST文档脱节的痛点。

本文专为ES 9.X新手打造,摒弃复杂理论,聚焦实际开发场景,详细讲解新Java客户端的基础依赖配置与核心API使用(含文档CRUD、检索、聚合、分页排序),所有代码均可直接复制运行,助力开发者快速上手新客户端。

一、环境准备与依赖配置

使用ES 9.X Java客户端前,需确保本地/服务器已部署ES 9.X版本(客户端版本需与服务端版本严格一致,避免版本冲突引发异常),本文以9.1.0版本为例进行演示。

1.1 Maven依赖(更推荐)

在项目pom.xml中引入以下依赖,核心依赖为elasticsearch-java,额外引入lombok简化实体类开发(可选,若不使用可手动实现getter/setter):

<!-- ES 9.X Java客户端核心依赖 -->
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>9.1.0</version>
</dependency>

<!-- Lombok依赖(简化实体类,可选) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <optional>true</optional>
</dependency>

<!-- 可选:Jackson依赖(若JSON序列化异常可添加) -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>

1.2 Gradle依赖

若项目使用Gradle构建,添加以下依赖至build.gradle

// ES 9.X Java客户端核心依赖
implementation 'co.elastic.clients:elasticsearch-java:9.1.0'
// Lombok依赖(可选)
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
// 可选:Jackson依赖
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'

1.3 依赖注意事项

  • 核心依赖elasticsearch-java版本必须与ES服务端版本一致(如ES服务端为9.1.0,客户端也需为9.1.0),否则会出现版本兼容异常(如NoSuchMethodError、未知参数异常)。
  • 无需额外引入ES服务端核心包,新客户端已彻底解耦旧客户端的强依赖问题,体积更轻量。
  • 多模块项目中,需确保所有子模块的ES客户端版本统一,避免出现多个客户端实例冲突的情况。

二、核心API实战(基础必学)

本文以「商品(Product)」为示例实体,模拟实际业务场景,讲解新客户端的各项基础操作。所有代码均包含完整注释,新手可直接复制到项目中测试(需替换ES服务端地址、账号密码)。

2.1 定义示例实体类

创建商品实体类Product,对应ES中的products索引,字段含义:sku(商品唯一标识)、name(商品名称)、price(商品价格)。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 商品实体类(对应ES的products索引)
 * 注:@Data注解等价于@Getter+@Setter+@ToString+@EqualsAndHashCode+@RequiredArgsConstructor
 * 若不使用Lombok,需手动实现getter/setter方法
 */
@Data
@AllArgsConstructor  // 全参构造方法
@NoArgsConstructor   // 无参构造方法(JSON反序列化必须)
public class Product {
    // 商品唯一标识(对应ES文档id)
    private String sku;
    // 商品名称(可用于全文检索、精确检索)
    private String name;
    // 商品价格(可用于范围查询、聚合统计)
    private double price;
}

2.2 创建ES客户端(核心步骤)

ES 9.X Java客户端通过ElasticsearchClient类实现与服务端的连接,支持配置服务端地址、账号密码、超时时间等参数。客户端为重量级对象,建议全局单例创建,避免频繁创建/销毁导致资源泄露。

2.2.1 基础创建(简洁版)

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.elasticsearch.client.RestClient;

import java.net.URI;

public class EsClientUtil {
    // 单例客户端实例
    private static ElasticsearchClient esClient;

    // 初始化客户端(静态代码块,项目启动时执行一次)
    static {
        try {
            // 1. 配置ES服务端地址(单机:单个URI;集群:多个URI用逗号分隔)
            String serverUrl = "http://127.0.0.1:9200"; // 替换为你的ES服务端地址
            // 2. 创建RestClient(底层HTTP连接)
            RestClient restClient = RestClient.builder(URI.create(serverUrl))
                    // 配置账号密码(若ES未开启权限验证,可省略此步骤)
                    .setBasicAuth("elastic", "123456") // 替换为你的ES账号密码
                    .build();

            // 3. 创建传输层(指定JSON序列化方式,Jackson为默认推荐)
            RestClientTransport transport = new RestClientTransport(
                    restClient, new JacksonJsonpMapper()
            );

            // 4. 创建最终的ES客户端
            esClient = new ElasticsearchClient(transport);
            System.out.println("ES 9.X客户端初始化成功!");
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("ES客户端初始化失败,请检查服务端地址和账号密码");
        }
    }

    // 获取客户端实例(对外提供统一访问入口)
    public static ElasticsearchClient getEsClient() {
        return esClient;
    }

    // 关闭客户端(项目停止时调用,释放资源)
    public static void closeClient() {
        try {
            if (esClient != null) {
                // 关闭传输层和RestClient
                esClient._transport().close();
                esClient._transport().restClient().close();
                System.out.println("ES客户端已关闭");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 使用方式(任何地方直接调用)
// ElasticsearchClient client = EsClientUtil.getEsClient();

2.2.2 进阶配置(超时、集群)

若需配置连接超时、读取超时,或连接ES集群,可修改客户端初始化逻辑,示例如下:

// 集群配置+超时配置(替换上面的静态代码块)
static {
    try {
        // 集群地址(多个节点用逗号分隔)
        String[] serverUrls = {"http://127.0.0.1:9200", "http://127.0.0.1:9201", "http://127.0.0.1:9202"};
        // 构建RestClientBuilder
        RestClient.Builder restClientBuilder = RestClient.builder(
                Arrays.stream(serverUrls).map(URI::create).toArray(URI[]::new)
        );

        // 配置账号密码
        restClientBuilder.setBasicAuth("elastic", "123456");

        // 配置超时时间(可选,根据业务调整)
        restClientBuilder.setRequestConfigCallback(requestConfigBuilder -> 
                requestConfigBuilder
                        .setConnectTimeout(5000)  // 连接超时:5秒
                        .setSocketTimeout(10000) // 读取超时:10秒
        );

        // 后续步骤(传输层、客户端创建)与基础版一致
        RestClient restClient = restClientBuilder.build();
        RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
        esClient = new ElasticsearchClient(transport);
        System.out.println("ES集群客户端初始化成功!");
    } catch (Exception e) {
        e.printStackTrace();
        throw new RuntimeException("ES客户端初始化失败");
    }
}

2.3 文档操作(CRUD核心)

ES文档操作是基础,新客户端提供两种请求构建方式:Fluent DSL(简洁优雅,推荐)和Builder模式(灵活,适合复杂场景),以下分别演示。

2.3.1 插入文档(单条)

插入文档即向ES索引中添加一条数据,可指定文档id(推荐用业务唯一标识,如商品sku),也可让ES自动生成id。

方式1:Fluent DSL(推荐)

import co.elastic.clients.elasticsearch.core.IndexRequest;
import co.elastic.clients.elasticsearch.core.IndexResponse;

/**
 * 单条插入文档(Fluent DSL方式)
 */
public class EsDocumentInsert {
    public static void main(String[] args) {
        try {
            // 1. 获取ES客户端
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 2. 构建商品数据(模拟业务数据)
            Product product = new Product("sku001", "苹果14 Pro", 7999.0);

            // 3. 构建插入请求(Fluent DSL语法,链式调用)
            IndexRequest<Product> request = IndexRequest.of(i -> i
                    .index("products")          // 指定索引名称(若索引不存在,ES会自动创建,建议提前创建)
                    .id(product.getSku())       // 指定文档id(与商品sku一致,便于后续查询和修改)
                    .document(product)          // 传入要插入的实体对象(自动序列化为JSON)
                    .refresh(true)              // 插入后立即刷新索引(便于立即查询到,生产环境可根据性能调整)
            );

            // 4. 执行插入操作,获取响应结果
            IndexResponse response = client.index(request);

            // 5. 解析响应结果
            System.out.println("文档插入成功!");
            System.out.println("索引名称:" + response.index());
            System.out.println("文档id:" + response.id());
            System.out.println("插入状态:" + response.result()); // created(新增)/ updated(覆盖)

        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("文档插入失败:" + e.getMessage());
        } finally {
            // 项目停止时关闭客户端(实际开发中可在Spring销毁方法中调用)
            // EsClientUtil.closeClient();
        }
    }
}

方式2:Builder模式

Builder模式更灵活,可动态设置请求参数(如条件判断后设置refresh、timeout等):

import co.elastic.clients.elasticsearch.core.IndexRequest;

/**
 * 单条插入文档(Builder模式)
 */
public class EsDocumentInsertByBuilder {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建商品数据
            Product product = new Product("sku002", "华为Mate 60", 6999.0);

            // 2. 构建插入请求Builder
            IndexRequest.Builder<Product> requestBuilder = new IndexRequest.Builder<Product>();
            requestBuilder.index("products");          // 指定索引
            requestBuilder.id(product.getSku());       // 指定文档id
            requestBuilder.document(product);          // 传入实体对象
            requestBuilder.refresh(true);              // 立即刷新

            // 3. 构建请求并执行
            IndexResponse response = client.index(requestBuilder.build());

            // 4. 解析响应
            System.out.println("Builder模式插入成功,文档id:" + response.id());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.3.2 批量插入文档

当需要插入多条文档时(如数据导入),使用批量插入可大幅提升效率,避免单条插入频繁请求ES服务端。

import co.elastic.clients.elasticsearch.core.BulkRequest;
import co.elastic.clients.elasticsearch.core.BulkResponse;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;

import java.util.ArrayList;
import java.util.List;

/**
 * 批量插入文档(推荐用于多条数据导入场景)
 */
public class EsDocumentBulkInsert {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 模拟批量商品数据(实际开发中可从数据库、文件中读取)
            List<Product> productList = new ArrayList<>();
            productList.add(new Product("sku003", "小米14", 4999.0));
            productList.add(new Product("sku004", "OPPO Find X7", 5999.0));
            productList.add(new Product("sku005", "vivo X100", 5499.0));

            // 2. 构建批量请求Builder
            BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();

            // 3. 遍历商品列表,添加批量操作(每条数据对应一个index操作)
            for (Product product : productList) {
                bulkBuilder.operations(op -> op
                        .index(idx -> idx          // 每条操作都是插入操作
                                .index("products")  // 指定索引
                                .id(product.getSku()) // 指定文档id
                                .document(product)  // 传入商品数据
                        )
                );
            }

            // 4. 执行批量插入,获取响应
            BulkResponse bulkResponse = client.bulk(bulkBuilder.build());

            // 5. 解析批量响应结果(批量操作可能部分成功、部分失败,需校验)
            if (bulkResponse.errors()) {
                System.out.println("批量插入存在失败项,详情如下:");
                bulkResponse.items().forEach(item -> {
                    if (item.error() != null) {
                        System.out.println("文档id:" + item.id() + ",插入失败:" + item.error().reason());
                    }
                });
            } else {
                System.out.println("批量插入全部成功!");
                System.out.println("插入总数:" + productList.size());
            }

        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("批量插入失败:" + e.getMessage());
        }
    }
}

2.3.3 补充:创建索引(推荐提前创建)

虽然ES会自动创建不存在的索引,但自动创建的索引字段类型可能不符合预期(如price可能被识别为text类型,无法进行范围查询),建议提前手动创建索引,指定字段类型:

import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;

/**
 * 手动创建products索引(指定字段类型,推荐提前创建)
 */
public class EsIndexCreate {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 构建创建索引请求,指定字段映射(mapping)
            CreateIndexRequest request = CreateIndexRequest.of(c -> c
                    .index("products")          // 索引名称
                    .mappings(m -> m            // 字段映射配置
                            .properties("sku", p -> p.keyword())  // sku:keyword类型(精确匹配,不分词)
                            .properties("name", p -> p.text())     // name:text类型(支持全文检索,分词)
                            .properties("price", p -> p.double_()) // price:double类型(支持数值运算、范围查询)
                    )
            );

            // 执行创建操作
            CreateIndexResponse response = client.indices().create(request);

            if (response.acknowledged()) {
                System.out.println("products索引创建成功!");
            } else {
                System.out.println("products索引创建失败!");
            }

        } catch (Exception e) {
            // 若索引已存在,会抛出异常,可忽略或捕获处理
            if (e.getMessage().contains("resource_already_exists_exception")) {
                System.out.println("products索引已存在,无需重复创建!");
            } else {
                e.printStackTrace();
            }
        }
    }
}

2.4 检索操作(核心重点)

ES的核心优势在于检索,新客户端支持全文检索、精确检索、组合检索等常见场景,以下结合商品场景详细演示,所有示例均基于products索引和批量插入的数据。

2.4.1 全文检索(matchAll/match)

全文检索适用于模糊查询场景(如根据商品名称搜索),ES会对查询关键词进行分词,匹配所有包含分词结果的文档。

场景1:查询所有文档(matchAll)

import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;

import java.util.List;

/**
 * 全文检索 - 查询所有文档(matchAll)
 */
public class EsSearchMatchAll {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建查询请求(查询products索引下所有文档)
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")          // 指定索引
                    .query(q -> q.matchAll(m -> m)) // 全文检索 - 匹配所有文档
            );

            // 2. 执行查询,获取响应
            SearchResponse<Product> response = client.search(request, Product.class);

            // 3. 解析响应结果(获取命中的文档列表)
            List<Hit<Product>> hits = response.hits().hits();
            System.out.println("查询到的文档总数:" + response.hits().total().value());
            System.out.println("查询结果详情:");

            // 遍历文档列表,获取商品信息
            for (Hit<Product> hit : hits) {
                Product product = hit.source(); // 反序列化为Product实体
                System.out.println("文档id:" + hit.id() + ",商品信息:" + product);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

场景2:根据关键词模糊查询(match)

根据商品名称模糊查询(如搜索“14”,会匹配“苹果14 Pro”、“小米14”):

/**
 * 全文检索 - 关键词模糊查询(match)
 */
public class EsSearchMatch {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建查询请求(根据商品名称模糊查询,关键词:14)
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")
                    .query(q -> q
                            .match(m -> m
                                    .field("name")     // 检索的字段(商品名称)
                                    .query("14")       // 检索关键词
                                    .fuzziness("AUTO") // 模糊匹配(允许轻微拼写错误,如14写成15,可根据需求关闭)
                            )
                    )
            );

            // 2. 执行查询
            SearchResponse<Product> response = client.search(request, Product.class);

            // 3. 解析结果
            List<Hit<Product>> hits = response.hits().hits();
            System.out.println("匹配到的商品数量:" + hits.size());
            hits.forEach(hit -> {
                System.out.println("商品:" + hit.source().getName() + ",价格:" + hit.source().getPrice());
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.4.2 精确检索(term/range)

精确检索适用于无需分词、精准匹配的场景,常见两种类型:term(词条精确匹配)和range(范围匹配)。

场景1:term精确匹配(keyword字段推荐)

适用于匹配固定词条(如根据sku查询商品、根据商品名称精确匹配),注意:text类型字段使用term查询可能无法匹配(因text会分词),推荐用于keyword字段。

/**
 * 精确检索 - term词条匹配(适用于keyword字段)
 */
public class EsSearchTerm {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建查询请求(根据sku精确查询商品,sku为keyword字段)
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")
                    .query(q -> q
                            .term(t -> t
                                    .field("sku")      // 检索字段(sku,keyword类型)
                                    .value("sku001")   // 精确匹配的值
                            )
                    )
            );

            // 2. 执行查询
            SearchResponse<Product> response = client.search(request, Product.class);

            // 3. 解析结果
            List<Hit<Product>> hits = response.hits().hits();
            if (hits.isEmpty()) {
                System.out.println("未查询到对应商品!");
            } else {
                Product product = hits.get(0).source();
                System.out.println("精确查询到的商品:" + product);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

场景2:range范围查询(数值字段推荐)

适用于数值、日期等字段的范围匹配(如查询价格在5000-8000之间的商品),ES 9.X中数值类型的range查询需使用number函数,避免踩坑!

import co.elastic.clients.elasticsearch.core.search.RangeQuery;

/**
 * 精确检索 - range范围查询(适用于数值、日期字段)
 */
public class EsSearchRange {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建range查询条件(价格范围:5000 ≤ price ≤ 8000)
            RangeQuery rangeQuery = RangeQuery.of(r -> r
                    .number(n -> n            // 必须使用number函数(数值类型专用),不可直接用field
                            .field("price")  // 检索字段(price,double类型)
                            .gte(5000.0)     // 大于等于(gte = greater than or equal)
                            .lte(8000.0)     // 小于等于(lte = less than or equal)
                            // 可选:gt(大于)、lt(小于)
                    )
            );

            // 2. 构建查询请求,传入range查询条件
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")
                    .query(q -> q.range(rangeQuery)) // 执行range查询
            );

            // 3. 执行查询并解析结果
            SearchResponse<Product> response = client.search(request, Product.class);
            List<Hit<Product>> hits = response.hits().hits();
            System.out.println("价格在5000-8000之间的商品数量:" + hits.size());
            hits.forEach(hit -> {
                Product product = hit.source();
                System.out.println("商品名称:" + product.getName() + ",价格:" + product.getPrice());
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.4.3 组合检索(Bool Query)

实际业务中,检索条件往往是多个条件的组合(如“商品名称包含14”且“价格在5000以上”且“排除sku001”),此时需使用Bool Query组合多个查询条件。

Bool Query核心子句(必懂):

  • must:必须满足该条件(参与评分,影响查询结果排序)
  • mustNot:必须不满足该条件(不参与评分)
  • filter:必须满足该条件(不参与评分,可缓存,提升查询性能)
  • should:可选满足该条件(满足会增加评分,不满足不影响)
import co.elastic.clients.elasticsearch.core.search.BoolQuery;
import co.elastic.clients.elasticsearch.core.search.MatchQuery;
import co.elastic.clients.elasticsearch.core.search.Query;
import co.elastic.clients.elasticsearch.core.search.TermQuery;

/**
 * 组合检索 - Bool Query(多条件组合查询,实际业务高频使用)
 */
public class EsSearchBool {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建子查询条件
            // 子查询1:商品名称包含"14"(must,必须满足,参与评分)
            Query nameQuery = MatchQuery.of(m -> m
                    .field("name")
                    .query("14")
            )._toQuery(); // 转换为通用Query类型

            // 子查询2:价格大于5000(filter,必须满足,不参与评分,提升性能)
            RangeQuery priceRangeQuery = RangeQuery.of(r -> r
                    .number(n -> n
                            .field("price")
                            .gt(5000.0) // 大于5000(gt = greater than)
                    )
            );
            Query priceFilterQuery = priceRangeQuery._toQuery();

            // 子查询3:排除sku001(mustNot,必须不满足)
            Query excludeSkuQuery = TermQuery.of(t -> t
                    .field("sku")
                    .value("sku001")
            )._toQuery();

            // 2. 组合Bool Query(将多个子查询组合起来)
            BoolQuery boolQuery = BoolQuery.of(b -> b
                    .must(nameQuery)           // 必须满足:名称包含14
                    .filter(priceFilterQuery)  // 必须满足:价格>5000(过滤,不评分)
                    .mustNot(excludeSkuQuery)  // 必须不满足:sku != sku001
            );

            // 3. 构建查询请求,执行Bool查询
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")
                    .query(q -> q.bool(boolQuery)) // 传入组合好的Bool查询
            );

            // 4. 执行查询并解析结果
            SearchResponse<Product> response = client.search(request, Product.class);
            List<Hit<Product>> hits = response.hits().hits();
            System.out.println("组合查询匹配到的商品数量:" + hits.size());
            hits.forEach(hit -> {
                Product product = hit.source();
                System.out.println("商品:" + product.getName() + ",sku:" + product.getSku() + ",价格:" + product.getPrice());
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.5 分页与排序

实际开发中,检索结果往往需要分页(如前端列表分页)和排序(如按价格升序/降序),ES 9.X客户端支持灵活的分页排序配置,以下结合商品场景演示。

2.5.1 基础分页排序(from+size)

适用于浅分页场景(数据量较小,分页深度较浅),核心参数:

  • from:起始偏移量(从第几条数据开始查询,默认0)
  • size:每页显示条数
  • sort:排序配置(指定排序字段和排序方向)

注意:from+size方式在深度分页(如from=10000,size=10)时会存在严重性能瓶颈,因为ES需要从每个分片读取10010条数据,再聚合排序后截取结果,大数据量场景建议使用Search After或Scroll API。

import co.elastic.clients.elasticsearch.core.search.SortOrder;

/**
 * 分页与排序(基础from+size方式,适用于浅分页)
 */
public class EsSearchPageSort {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 模拟前端分页参数(实际开发中从前端接收)
            int currentPage = 1;       // 当前页码(前端默认从1开始,ES从0开始)
            int pageSize = 2;          // 每页显示条数
            String sortField = "price";// 排序字段(商品价格)
            String sortOrder = "desc"; // 排序方向(desc:降序;asc:升序)

            // 2. 计算起始偏移量(from = (当前页码-1) * 每页条数)
            int from = (currentPage - 1) * pageSize;

            // 3. 构建查询请求(分页+排序+条件查询)
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")
                    .from(from)                // 起始偏移量
                    .size(pageSize)            // 每页条数
                    .sort(so -> so             // 排序配置
                            .field(f -> f
                                    .field(sortField)  // 排序字段
                                    // 排序方向:根据前端参数动态设置
                                    .order("desc".equals(sortOrder) ? SortOrder.Desc : SortOrder.Asc)
                            )
                    )
                    .query(q -> q              // 可选:添加查询条件(如价格>5000)
                            .range(r -> r
                                    .number(n -> n
                                            .field("price")
                                            .gt(5000.0)
                                    )
                            )
                    )
            );

            // 4. 执行查询并解析结果
            SearchResponse<Product> response = client.search(request, Product.class);
            List<Hit<Product>> hits = response.hits().hits();

            // 解析分页相关信息
            long total = response.hits().total().value(); // 总条数
            long totalPage = (total + pageSize - 1) / pageSize; // 总页数

            System.out.println("分页查询结果:");
            System.out.println("总条数:" + total + ",总页数:" + totalPage + ",当前页码:" + currentPage);
            System.out.println("当前页商品:");
            hits.forEach(hit -> {
                Product product = hit.source();
                System.out.println("商品:" + product.getName() + ",价格:" + product.getPrice());
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.5.2 补充:深度分页解决方案

针对大数据量深度分页场景,推荐使用以下两种方案,避免from+size的性能瓶颈:

  1. Search After(推荐实时深度分页) :基于上一页最后一条数据的排序值作为“锚点”,避免计算偏移量,支持实时查询,适合前端列表分页(如APP商品列表翻页)。
  2. Scroll API(适合批量导出/批处理) :创建查询快照,通过游标批量获取结果,不支持实时数据(快照创建后的数据变更不会体现),适合全量数据导出、ETL流程等离线场景。

2.6 聚合操作(统计分析)

聚合操作适用于数据统计分析场景(如商品价格平均值、不同类别的商品数量统计),ES 9.X客户端支持基础聚合和多级嵌套聚合,以下结合示例演示。

2.6.1 基础聚合(stats/terms)

常见基础聚合类型:

  • stats:统计聚合(计算字段的平均值、最大值、最小值、总和、计数)
  • terms:分组聚合(根据字段分组,统计每组的文档数量)
import co.elastic.clients.elasticsearch.core.search.Aggregate;
import co.elastic.clients.elasticsearch.core.search.StatsAggregate;
import co.elastic.clients.elasticsearch.core.search.StringTermsAggregate;
import co.elastic.clients.elasticsearch.core.search.StringTermsBucket;

import java.util.Map;

/**
 * 基础聚合操作(stats统计聚合 + terms分组聚合)
 */
public class EsAggregationBasic {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建基础查询条件(可选:过滤需要统计的数据)
            RangeQuery rangeQuery = RangeQuery.of(r -> r
                    .number(n -> n
                            .field("price")
                            .gte(5000.0)
                            .lte(10000.0)
                    )
            );

            // 2. 构建查询请求,添加聚合操作
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")
                    .query(q -> q.range(rangeQuery)) // 基础过滤条件(价格5000-10000)
                    .from(0).size(10)               // 分页:只查询前10条文档(聚合结果不受影响)
                    // 聚合1:价格统计(stats),聚合名称:price_stats
                    .aggregations("price_stats", a -> a
                            .stats(st -> st.field("price"))
                    )
                    // 聚合2:按商品类别分组(terms),聚合名称:category_terms(此处假设存在category字段)
                    .aggregations("category_terms", a -> a
                            .terms(t -> t
                                    .field("category.keyword") // category字段需为keyword类型(分组聚合推荐)
                                    .size(10)                  // 只显示前10个分组
                            )
                    )
            );

            // 3. 执行查询,获取响应
            SearchResponse<Product> response = client.search(request, Product.class);

            // 4. 解析聚合结果(核心步骤)
            if (response.aggregations() != null) {
                Map<String, Aggregate> aggregations = response.aggregations();

                // 4.1 解析价格统计聚合(stats)
                StatsAggregate priceStats = aggregations.get("price_stats").stats();
                System.out.println("商品价格统计(5000-10000元):");
                System.out.println("平均价格:" + String.format("%.2f", priceStats.avg()));
                System.out.println("最高价格:" + priceStats.max());
                System.out.println("最低价格:" + priceStats.min());
                System.out.println("价格总和:" + priceStats.sum());
                System.out.println("统计商品数量:" + priceStats.count());

                // 4.2 解析类别分组聚合(terms)
                StringTermsAggregate categoryTerms = aggregations.get("category_terms").sterms();
                System.out.println("\n商品类别分组统计:");
                for (StringTermsBucket bucket : categoryTerms.buckets().array()) {
                    String category = bucket.key(); // 分组名称(类别)
                    long count = bucket.docCount(); // 该类别的商品数量
                    System.out.println("类别:" + category + ",商品数量:" + count);
                }
            }

            // 5. 解析文档结果(可选)
            response.hits().hits().forEach(hit -> {
                System.out.println("\n文档详情:" + hit.source());
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.6.2 多级嵌套聚合(terms+range+avg/sum)

多级嵌套聚合适用于复杂统计场景(如“按商品类别分组 → 每组内按价格区间分组 → 统计每个价格区间的平均评分和销售总量”),以下修复原始代码bug,演示完整嵌套聚合。

import co.elastic.clients.elasticsearch.core.search.AvgAggregate;
import co.elastic.clients.elasticsearch.core.search.RangeAggregate;
import co.elastic.clients.elasticsearch.core.search.RangeBucket;
import co.elastic.clients.elasticsearch.core.search.StringTermsAggregate;
import co.elastic.clients.elasticsearch.core.search.StringTermsBucket;
import co.elastic.clients.elasticsearch.core.search.TopHitsAggregate;
import co.elastic.clients.elasticsearch.core.search.SearchHit;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.elasticsearch.core.search.SortOrder;

import java.util.Map;

/**
 * 多级嵌套聚合(terms分组 → range分组 → avg/sum统计)
 */
public class EsAggregationNested {
    public static void main(String[] args) {
        try {
            ElasticsearchClient client = EsClientUtil.getEsClient();

            // 1. 构建基础过滤条件(调整价格范围,覆盖后续所有区间,避免统计不到数据)
            RangeQuery rangeQuery = RangeQuery.of(r -> r
                    .number(n -> n
                            .field("price")
                            .gte(0.0)       // 覆盖低价区间
                            .lte(200.0)     // 覆盖高价区间
                    )
            );

            // 2. 构建多级嵌套聚合查询请求
            SearchRequest request = SearchRequest.of(s -> s
                    .index("products")
                    .query(q -> q.range(rangeQuery))  // 基础过滤条件,筛选有效商品
                    .size(0)                          // 不返回具体文档,只关注聚合结果(提升查询性能)
                    // 第一级聚合:按商品类别分组(terms),聚合名称:category_analysis
                    .aggregations("category_analysis", a -> a
                            .terms(t -> t
                                    .field("category.keyword") // 按类别分组(keyword类型,确保分组精准)
                                    .size(10)                  // 显示前10个类别,可根据需求调整
                            )
                            // 第二级聚合1:按价格区间分组(range),聚合名称:price_ranges
                            .aggregations("price_ranges", a2 -> a2
                                    .range(r -> r
                                            .field("price")  // 价格区间分组字段(数值类型)
                                            .ranges(         // 定义3个价格区间,清晰区分低/中/高价
                                                    range -> range.key("low").from(0.0).to(50.0),      // 0-50元(低价)
                                                    range -> range.key("medium").from(50.0).to(100.0),  // 50-100元(中价)
                                                    range -> range.key("high").from(100.0).to(200.0)  // 100-200元(高价)
                                            )
                                    )
                                    // 第三级聚合1:每个价格区间的平均评分(avg)
                                    .aggregations("avg_rating", a3 -> a3
                                            .avg(avg -> avg.field("rating")) // 假设存在rating(商品评分)字段
                                    )
                                    // 第三级聚合2:每个价格区间的销售总量(sum)
                                    .aggregations("total_sales", a3 -> a3
                                            .sum(sum -> sum.field("sales")) // 假设存在sales(商品销量)字段
                                    )
                            )
                            // 第二级聚合2:每个类别下的热门商品(topHits),取销量前3
                            .aggregations("top_products", a2 -> a2
                                    .topHits(th -> th
                                            .size(3) // 每个类别显示前3个热门商品
                                            .sort(so -> so
                                                    .field(f -> f
                                                            .field("sales") // 按销量降序排序,筛选热门
                                                            .order(SortOrder.Desc)
                                                    )
                                            )
                                    )
                            )
                    )
            );

            // 3. 执行查询,获取响应结果
            SearchResponse<Product> response = client.search(request, Product.class);

            // 4. 解析多级嵌套聚合结果(修复原始bug,完善路径获取和非空校验)
            if (response.aggregations() != null) {
                Map<String, Aggregate> rootAggregations = response.aggregations();

                // 4.1 获取第一级聚合:按商品类别分组(terms聚合),非空校验避免空指针
                if (rootAggregations.containsKey("category_analysis")) {
                    StringTermsAggregate categoryAgg = rootAggregations.get("category_analysis").sterms();

                    // 4.2 遍历每个类别桶(第一级聚合结果)
                    for (StringTermsBucket categoryBucket : categoryAgg.buckets().array()) {
                        String category = categoryBucket.key(); // 商品类别名称
                        long categoryDocCount = categoryBucket.docCount(); // 该类别的商品总数
                        System.out.println("=== 商品类别:" + category + "(商品总数:" + categoryDocCount + ")===");

                        // 4.3 获取第二级聚合:该类别下的价格区间分组(range聚合)
                        Map<String, Aggregate> categoryAggregations = categoryBucket.aggregations();
                        if (categoryAggregations.containsKey("price_ranges")) {
                            RangeAggregate priceRangeAgg = categoryAggregations.get("price_ranges").range();

                            // 4.4 遍历每个价格区间桶(第二级聚合结果)
                            for (RangeBucket priceBucket : priceRangeAgg.buckets().array()) {
                                String priceRange = priceBucket.key(); // 价格区间标识(low/medium/high)
                                long priceDocCount = priceBucket.docCount(); // 该价格区间的商品数量
                                System.out.println("  价格区间:" + priceRange + "(商品数量:" + priceDocCount + ")");

                                // 4.5 获取第三级聚合1:该价格区间的平均评分(avg聚合)
                                Map<String, Aggregate> priceAggregations = priceBucket.aggregations();
                                if (priceAggregations.containsKey("avg_rating")) {
                                    Aggregate avgRatingAgg = priceAggregations.get("avg_rating");
                                    if (avgRatingAgg != null && avgRatingAgg.isAvg()) {
                                        AvgAggregate avgRating = avgRatingAgg.avg();
                                        // 安全校验,避免null值异常,保留2位小数,提升可读性
                                        double avg = avgRating.value() != null ? avgRating.value() : 0.0;
                                        System.out.println("    平均评分:" + String.format("%.2f", avg));
                                    }
                                }

                                // 4.6 获取第三级聚合2:该价格区间的销售总量(sum聚合)
                                if (priceAggregations.containsKey("total_sales")) {
                                    Aggregate totalSalesAgg = priceAggregations.get("total_sales");
                                    if (totalSalesAgg != null && totalSalesAgg.isSum()) {
                                        // 解析sum聚合结果,兼容数值类型
                                        JsonData salesData = totalSalesAgg.sum().value();
                                        long totalSales = salesData != null ? salesData.toLong() : 0;
                                        System.out.println("    销售总量:" + totalSales);
                                    }
                                }
                            }
                        }

                        // 4.7 获取第二级聚合2:该类别下的热门商品(topHits聚合)
                        if (categoryAggregations.containsKey("top_products")) {
                            Aggregate topProductsAgg = categoryAggregations.get("top_products");
                            if (topProductsAgg != null && topProductsAgg.isTopHits()) {
                                TopHitsAggregate topHits = topProductsAgg.topHits();
                                System.out.println("  该类别热门商品(销量前3):");
                                // 遍历热门商品,反序列化为Product实体
                                for (SearchHit<Product> hit : topHits.hits().hits()) {
                                    Product product = hit.source();
                                    if (product != null) {
                                        System.out.println("    - 商品:" + product.getName() + ",价格:" + product.getPrice() + ",销量:" + hit.fields().get("sales").value());
                                    }
                                }
                            }
                        }
                        System.out.println(); // 换行,区分不同类别,提升可读性
                    }
                } else {
                    System.out.println("未查询到商品类别聚合数据,请检查category字段是否存在且为keyword类型");
                }
            } else {
                System.out.println("未查询到任何聚合结果,请检查索引数据或查询条件");
            }

        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("多级嵌套聚合查询失败:" + e.getMessage());
        } finally {
            // 项目停止时关闭客户端(实际开发中可在Spring销毁方法、Tomcat关闭钩子中调用)
            // EsClientUtil.closeClient();
        }
    }
}