前言
在现代搜索与数据分析场景中,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的性能瓶颈:
- Search After(推荐实时深度分页) :基于上一页最后一条数据的排序值作为“锚点”,避免计算偏移量,支持实时查询,适合前端列表分页(如APP商品列表翻页)。
- 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();
}
}
}