Appearance
Spring Data Elasticsearch
什么是Elasticsearch?
Elasticsearch是一个基于Lucene的开源分布式搜索和分析引擎,专为水平扩展、高可靠性和易管理而设计。它可以近实时地存储、搜索和分析大量数据,通常用于实现应用程序搜索、网站搜索、企业搜索、日志分析、实时分析等功能。
Spring Data Elasticsearch简介
Spring Data Elasticsearch是Spring Data项目的一部分,旨在为Elasticsearch提供更简单的编程模型。它提供了与Spring Framework集成的高级抽象,简化了与Elasticsearch的交互,使开发者能够专注于业务逻辑而非技术细节。
主要特性
- Repository抽象:简化基本CRUD操作和查询
- 实体映射:Java对象与Elasticsearch文档的自动映射
- ElasticsearchTemplate:提供丰富的API进行复杂操作
- 注解支持:使用注解定义索引和字段映射
- 高级查询DSL:通过API构建复杂查询
- 地理空间查询:支持地理位置搜索
- 聚合分析:支持Elasticsearch的聚合功能
- 响应式API:支持响应式编程模型
依赖配置
Maven
xml
<!-- Spring Boot方式 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- 非Spring Boot方式 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>5.1.0</version>
</dependency>
配置Elasticsearch连接
Spring Boot配置
properties
# application.properties
spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.username=elastic
spring.elasticsearch.password=password
Java配置
java
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.example.repository")
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
@Value("${spring.elasticsearch.uris}")
private String elasticsearchUrl;
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo(elasticsearchUrl.substring(7)) // 移除 "http://"
.withBasicAuth("elastic", "password")
.withConnectTimeout(Duration.ofSeconds(5))
.withSocketTimeout(Duration.ofSeconds(3))
.build();
}
@Bean
@Override
public ElasticsearchOperations elasticsearchOperations() {
return new ElasticsearchRestTemplate(elasticsearchClient());
}
}
实体映射
Elasticsearch文档映射到Java对象:
java
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "standard")
private String name;
@Field(type = FieldType.Text, analyzer = "standard")
private String description;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Boolean)
private Boolean available;
@Field(type = FieldType.Date, format = DateFormat.basic_date_time)
private Date createdAt;
@Field(type = FieldType.Nested)
private List<Review> reviews;
@GeoPointField
private GeoPoint location;
// 构造函数、getter和setter
}
public class Review {
@Field(type = FieldType.Keyword)
private String username;
@Field(type = FieldType.Integer)
private Integer rating;
@Field(type = FieldType.Text)
private String comment;
// 构造函数、getter和setter
}
索引管理
创建索引和映射
java
@Service
public class IndexService {
private final ElasticsearchOperations operations;
public IndexService(ElasticsearchOperations operations) {
this.operations = operations;
}
// 检查索引是否存在
public boolean indexExists(String indexName) {
return operations.indexOps(IndexCoordinates.of(indexName)).exists();
}
// 创建索引
public boolean createIndex(String indexName) {
return operations.indexOps(IndexCoordinates.of(indexName)).create();
}
// 根据Entity类创建索引
public boolean createProductIndex() {
IndexOperations indexOps = operations.indexOps(Product.class);
return indexOps.create();
}
// 创建自定义映射
public void createMapping() {
IndexOperations indexOps = operations.indexOps(Product.class);
Document mapping = indexOps.createMapping();
indexOps.putMapping(mapping);
}
// 删除索引
public boolean deleteIndex(String indexName) {
return operations.indexOps(IndexCoordinates.of(indexName)).delete();
}
}
Repository操作
使用ElasticsearchRepository接口:
java
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
// 基本查询方法
List<Product> findByName(String name);
// 多条件查询
List<Product> findByNameAndCategory(String name, String category);
// 模糊查询
List<Product> findByNameContaining(String name);
// 范围查询
List<Product> findByPriceBetween(double minPrice, double maxPrice);
// 排序
List<Product> findByAvailableTrue(Sort sort);
// 嵌套查询
List<Product> findByReviews_Rating(int rating);
// 地理位置查询
List<Product> findByLocationNear(GeoPoint location, Distance distance);
// 分页
Page<Product> findByCategory(String category, Pageable pageable);
// 自定义查询
@Query("{\"bool\": {\"must\": [{\"match\": {\"name\": \"?0\"}}]}}")
List<Product> findByNameCustomQuery(String name);
}
ElasticsearchOperations和ElasticsearchRestTemplate
对于更复杂的查询和操作:
java
@Service
public class ProductService {
private final ElasticsearchOperations elasticsearchOperations;
private final ProductRepository productRepository;
public ProductService(ElasticsearchOperations elasticsearchOperations,
ProductRepository productRepository) {
this.elasticsearchOperations = elasticsearchOperations;
this.productRepository = productRepository;
}
// 保存文档
public Product save(Product product) {
return productRepository.save(product);
}
// 批量保存
public Iterable<Product> saveAll(List<Product> products) {
return productRepository.saveAll(products);
}
// 根据ID查找
public Optional<Product> findById(String id) {
return productRepository.findById(id);
}
// 查找所有
public Iterable<Product> findAll() {
return productRepository.findAll();
}
// 删除
public void delete(Product product) {
productRepository.delete(product);
}
// 使用NativeQuery
public List<Product> searchByNameAndCategory(String name, String category) {
// 使用Elasticsearch的Query DSL构建查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name", name))
.must(QueryBuilders.termQuery("category", category));
NativeQuery searchQuery = new NativeQuery(boolQuery);
return elasticsearchOperations.search(searchQuery, Product.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
// 全文搜索
public List<Product> searchProducts(String text) {
// 在多个字段中搜索
MultiMatchQueryBuilder multiMatchQuery = QueryBuilders.multiMatchQuery(text)
.field("name", 2.0f) // 提高name字段的权重
.field("description")
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS);
NativeQuery searchQuery = new NativeQuery(multiMatchQuery);
return elasticsearchOperations.search(searchQuery, Product.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
// 分页和排序
public Page<Product> findByCategory(String category, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("price").ascending());
return productRepository.findByCategory(category, pageable);
}
// 复杂搜索带高亮
public List<SearchHit<Product>> searchWithHighlight(String text) {
// 构建查询
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("description", text);
// 配置高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("description");
highlightBuilder.preTags("<strong>");
highlightBuilder.postTags("</strong>");
NativeQuery searchQuery = new NativeQuery(matchQuery);
searchQuery.setHighlightQuery(
new HighlightQuery(
new Highlight(highlightBuilder),
Product.class
)
);
return elasticsearchOperations.search(searchQuery, Product.class).getSearchHits();
}
}
高级查询
复杂条件查询
java
public List<Product> complexSearch(String name, List<String> categories,
Double minPrice, Double maxPrice,
Boolean available) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 动态添加条件
if (name != null && !name.isEmpty()) {
boolQuery.must(QueryBuilders.matchQuery("name", name));
}
if (categories != null && !categories.isEmpty()) {
boolQuery.filter(QueryBuilders.termsQuery("category", categories));
}
if (minPrice != null || maxPrice != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (minPrice != null) {
rangeQuery.gte(minPrice);
}
if (maxPrice != null) {
rangeQuery.lte(maxPrice);
}
boolQuery.filter(rangeQuery);
}
if (available != null) {
boolQuery.filter(QueryBuilders.termQuery("available", available));
}
NativeQuery searchQuery = new NativeQuery(boolQuery);
return elasticsearchOperations.search(searchQuery, Product.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
聚合查询
java
public Map<String, Long> countProductsByCategory() {
TermsAggregationBuilder aggregation = AggregationBuilders.terms("categories")
.field("category");
NativeQuery query = new NativeQuery(
QueryBuilders.matchAllQuery(),
new NativeSearchAggregation(aggregation)
);
SearchHits<Product> searchHits = elasticsearchOperations.search(query, Product.class);
ParsedTerms terms = searchHits.getAggregations().get("categories");
Map<String, Long> results = new HashMap<>();
for (Terms.Bucket bucket : terms.getBuckets()) {
results.put(bucket.getKeyAsString(), bucket.getDocCount());
}
return results;
}
public Map<String, Object> getProductPriceStats() {
StatsAggregationBuilder aggregation = AggregationBuilders.stats("price_stats")
.field("price");
NativeQuery query = new NativeQuery(
QueryBuilders.matchAllQuery(),
new NativeSearchAggregation(aggregation)
);
SearchHits<Product> searchHits = elasticsearchOperations.search(query, Product.class);
ParsedStats stats = searchHits.getAggregations().get("price_stats");
Map<String, Object> results = new HashMap<>();
results.put("avg", stats.getAvg());
results.put("min", stats.getMin());
results.put("max", stats.getMax());
results.put("sum", stats.getSum());
results.put("count", stats.getCount());
return results;
}
地理空间查询
java
public List<Product> findProductsNearLocation(double lat, double lon, String distance) {
GeoDistanceQueryBuilder geoDistanceQuery = QueryBuilders.geoDistanceQuery("location")
.point(lat, lon)
.distance(distance);
NativeQuery searchQuery = new NativeQuery(geoDistanceQuery);
// 按距离排序
GeoDistanceSortBuilder sort = SortBuilders.geoDistanceSort("location", lat, lon)
.order(SortOrder.ASC);
searchQuery.addSort(sort);
return elasticsearchOperations.search(searchQuery, Product.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
模糊匹配和自动完成
java
public List<String> autoComplete(String prefix) {
WildcardQueryBuilder wildcardQuery = QueryBuilders.wildcardQuery("name", prefix + "*");
NativeQuery searchQuery = new NativeQuery(wildcardQuery);
searchQuery.setFields(Collections.singletonList("name"));
return elasticsearchOperations.search(searchQuery, Product.class)
.stream()
.map(hit -> hit.getContent().getName())
.distinct()
.collect(Collectors.toList());
}
public List<Product> fuzzySearch(String term) {
FuzzyQueryBuilder fuzzyQuery = QueryBuilders.fuzzyQuery("name", term)
.fuzziness(Fuzziness.AUTO);
NativeQuery searchQuery = new NativeQuery(fuzzyQuery);
return elasticsearchOperations.search(searchQuery, Product.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
批量操作
java
@Service
public class BulkOperationService {
private final ElasticsearchOperations operations;
public BulkOperationService(ElasticsearchOperations operations) {
this.operations = operations;
}
public void bulkIndex(List<Product> products) {
List<IndexQuery> queries = products.stream()
.map(product -> {
IndexQuery indexQuery = new IndexQuery();
indexQuery.setId(product.getId());
indexQuery.setObject(product);
return indexQuery;
})
.collect(Collectors.toList());
operations.bulkIndex(queries, IndexCoordinates.of("products"));
}
public void bulkUpdate(List<Product> products) {
List<UpdateQuery> queries = products.stream()
.map(product -> {
Map<String, Object> doc = new HashMap<>();
doc.put("price", product.getPrice());
doc.put("available", product.getAvailable());
return UpdateQuery.builder(product.getId())
.withDocument(Document.from(doc))
.build();
})
.collect(Collectors.toList());
operations.bulkUpdate(queries, IndexCoordinates.of("products"));
}
public void bulkDelete(List<String> productIds) {
List<Query> queries = productIds.stream()
.map(id -> new NativeQuery(QueryBuilders.idsQuery().addIds(id)))
.collect(Collectors.toList());
operations.delete(queries, Product.class, IndexCoordinates.of("products"));
}
}
响应式Elasticsearch支持
Spring Data Elasticsearch也提供了响应式编程支持:
java
// 配置
@Configuration
@EnableReactiveElasticsearchRepositories
public class ReactiveElasticsearchConfig extends AbstractReactiveElasticsearchConfiguration {
@Override
public ReactiveElasticsearchClient reactiveElasticsearchClient() {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
return ReactiveRestClients.create(clientConfiguration);
}
}
// 响应式Repository
public interface ReactiveProductRepository extends ReactiveElasticsearchRepository<Product, String> {
Flux<Product> findByName(String name);
Flux<Product> findByCategory(String category);
Mono<Product> findByNameAndCategory(String name, String category);
}
// 服务层
@Service
public class ReactiveProductService {
private final ReactiveProductRepository productRepository;
private final ReactiveElasticsearchOperations operations;
public ReactiveProductService(ReactiveProductRepository productRepository,
ReactiveElasticsearchOperations operations) {
this.productRepository = productRepository;
this.operations = operations;
}
public Mono<Product> save(Product product) {
return productRepository.save(product);
}
public Flux<Product> findAll() {
return productRepository.findAll();
}
public Mono<Product> findById(String id) {
return productRepository.findById(id);
}
public Flux<Product> search(String text) {
NativeQuery searchQuery = new NativeQuery(
QueryBuilders.multiMatchQuery(text, "name", "description")
);
return operations.search(searchQuery, Product.class)
.map(SearchHit::getContent);
}
}
实际应用场景
1. 全文搜索应用
java
@RestController
@RequestMapping("/api/search")
public class SearchController {
private final ProductService productService;
public SearchController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> search(@RequestParam String query,
@RequestParam(required = false) List<String> categories,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice) {
return productService.complexSearch(query, categories, minPrice, maxPrice, true);
}
@GetMapping("/autocomplete")
public List<String> autocomplete(@RequestParam String prefix) {
return productService.autoComplete(prefix);
}
}
2. 日志分析系统
java
@Document(indexName = "logs")
public class LogEntry {
@Id
private String id;
@Field(type = FieldType.Date, format = DateFormat.basic_date_time)
private Date timestamp;
@Field(type = FieldType.Keyword)
private String level;
@Field(type = FieldType.Text)
private String message;
@Field(type = FieldType.Keyword)
private String application;
@Field(type = FieldType.Object)
private Map<String, Object> details;
// 构造函数、getter和setter
}
@Service
public class LogAnalysisService {
private final ElasticsearchOperations operations;
public LogAnalysisService(ElasticsearchOperations operations) {
this.operations = operations;
}
public List<LogEntry> findErrorLogs(Date startDate, Date endDate) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("level", "ERROR"))
.must(QueryBuilders.rangeQuery("timestamp")
.from(startDate.getTime())
.to(endDate.getTime()));
NativeQuery searchQuery = new NativeQuery(boolQuery);
return operations.search(searchQuery, LogEntry.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
public Map<String, Long> countLogsByLevel() {
TermsAggregationBuilder aggregation = AggregationBuilders.terms("log_levels")
.field("level");
NativeQuery query = new NativeQuery(
QueryBuilders.matchAllQuery(),
new NativeSearchAggregation(aggregation)
);
SearchHits<LogEntry> searchHits = operations.search(query, LogEntry.class);
ParsedTerms terms = searchHits.getAggregations().get("log_levels");
Map<String, Long> results = new HashMap<>();
for (Terms.Bucket bucket : terms.getBuckets()) {
results.put(bucket.getKeyAsString(), bucket.getDocCount());
}
return results;
}
}
3. 实时分析仪表板
java
@Service
public class DashboardService {
private final ElasticsearchOperations operations;
public DashboardService(ElasticsearchOperations operations) {
this.operations = operations;
}
public Map<String, Object> getProductSalesDashboard() {
// 销售总额
SumAggregationBuilder totalSales = AggregationBuilders.sum("total_sales")
.field("price");
// 按类别分组
TermsAggregationBuilder categorySales = AggregationBuilders.terms("category_sales")
.field("category")
.subAggregation(AggregationBuilders.sum("category_sum").field("price"));
// 时间趋势
DateHistogramAggregationBuilder salesTrend = AggregationBuilders.dateHistogram("sales_trend")
.field("orderDate")
.calendarInterval(DateHistogramInterval.DAY)
.subAggregation(AggregationBuilders.sum("daily_sales").field("price"));
NativeQuery query = new NativeQuery(QueryBuilders.matchAllQuery());
query.addAggregation(totalSales);
query.addAggregation(categorySales);
query.addAggregation(salesTrend);
SearchHits<Product> searchHits = operations.search(query, Product.class);
Aggregations aggregations = searchHits.getAggregations();
// 解析聚合结果并构建仪表板数据
Map<String, Object> dashboard = new HashMap<>();
dashboard.put("totalSales", ((ParsedSum) aggregations.get("total_sales")).getValue());
// ...解析其他聚合结果
return dashboard;
}
}
性能优化
1. 索引设置优化
java
@Service
public class IndexOptimizationService {
private final ElasticsearchOperations operations;
public IndexOptimizationService(ElasticsearchOperations operations) {
this.operations = operations;
}
public void optimizeProductIndex() {
IndexOperations indexOps = operations.indexOps(Product.class);
// 配置索引设置
Map<String, Object> settings = new HashMap<>();
// 分片和副本设置
settings.put("number_of_shards", 3);
settings.put("number_of_replicas", 1);
// 刷新间隔
settings.put("refresh_interval", "5s");
// 分析器设置
Map<String, Object> analysis = new HashMap<>();
Map<String, Object> analyzer = new HashMap<>();
Map<String, Object> customAnalyzer = new HashMap<>();
customAnalyzer.put("type", "custom");
customAnalyzer.put("tokenizer", "standard");
customAnalyzer.put("filter", List.of("lowercase", "asciifolding"));
analyzer.put("custom_analyzer", customAnalyzer);
analysis.put("analyzer", analyzer);
settings.put("analysis", analysis);
indexOps.create(settings);
}
// 性能监控
public Map<String, Object> getIndexStats(String indexName) {
// 获取索引统计信息
// 需要使用ElasticsearchClient直接访问Elasticsearch API
RestHighLevelClient client = operations.getElasticsearchClient().rest();
IndicesStatsRequest request = new IndicesStatsRequest().indices(indexName);
try {
IndicesStatsResponse response = client.indices().stats(request, RequestOptions.DEFAULT);
// 解析和返回统计信息
Map<String, Object> stats = new HashMap<>();
stats.put("documentCount", response.getTotal().getDocs().getCount());
stats.put("indexSize", response.getTotal().getStore().getSizeInBytes());
// ...其他统计信息
return stats;
} catch (IOException e) {
throw new RuntimeException("Failed to get index stats", e);
}
}
}
2. 查询优化
java
// 使用滚动API处理大结果集
public List<Product> scrollProducts() {
NativeQuery searchQuery = new NativeQuery(QueryBuilders.matchAllQuery());
searchQuery.setPageable(PageRequest.of(0, 100));
ScrolledPage<Product> scroll = (ScrolledPage<Product>) elasticsearchOperations.searchForPage(
searchQuery, Product.class);
List<Product> products = new ArrayList<>(scroll.getContent());
String scrollId = scroll.getScrollId();
while (scroll.hasContent()) {
scroll = (ScrolledPage<Product>) elasticsearchOperations.searchScrollContinue(
scrollId, 1000, Product.class);
products.addAll(scroll.getContent());
}
elasticsearchOperations.searchScrollClear(Collections.singletonList(scrollId));
return products;
}
// 使用过滤器缓存
public List<Product> efficientFilteredSearch(List<String> categories) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.filter(QueryBuilders.termsQuery("category", categories));
NativeQuery searchQuery = new NativeQuery(boolQuery);
return elasticsearchOperations.search(searchQuery, Product.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
3. 批量处理与并发
java
// 并行处理大量数据
public void indexLargeDataset(List<Product> products) {
// 分批处理
int batchSize = 1000;
AtomicInteger counter = new AtomicInteger();
Collection<List<Product>> batches = products.stream()
.collect(Collectors.groupingBy(product -> counter.getAndIncrement() / batchSize))
.values();
// 并行处理每个批次
batches.parallelStream().forEach(batch -> {
List<IndexQuery> indexQueries = batch.stream()
.map(product -> {
IndexQuery indexQuery = new IndexQuery();
indexQuery.setId(product.getId());
indexQuery.setObject(product);
return indexQuery;
})
.collect(Collectors.toList());
elasticsearchOperations.bulkIndex(indexQueries, IndexCoordinates.of("products"));
});
}
测试
Spring Data Elasticsearch提供了测试支持:
java
@SpringBootTest
@TestPropertySource(properties = {
"spring.elasticsearch.rest.uris=localhost:9200"
})
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchOperations operations;
@BeforeEach
public void setUp() {
operations.indexOps(Product.class).delete();
operations.indexOps(Product.class).create();
// 创建测试数据
Product product1 = new Product();
product1.setId("1");
product1.setName("Test Product 1");
product1.setCategory("Electronics");
product1.setPrice(100.0);
product1.setAvailable(true);
Product product2 = new Product();
product2.setId("2");
product2.setName("Test Product 2");
product2.setCategory("Books");
product2.setPrice(50.0);
product2.setAvailable(true);
productRepository.saveAll(Arrays.asList(product1, product2));
// 刷新索引
operations.indexOps(Product.class).refresh();
}
@Test
public void testFindByName() {
List<Product> products = productRepository.findByName("Test Product 1");
assertFalse(products.isEmpty());
assertEquals("1", products.get(0).getId());
assertEquals("Test Product 1", products.get(0).getName());
}
@Test
public void testFindByPriceBetween() {
List<Product> products = productRepository.findByPriceBetween(30.0, 80.0);
assertFalse(products.isEmpty());
assertEquals(1, products.size());
assertEquals("2", products.get(0).getId());
}
}
集成Spring Boot Actuator
Spring Boot Actuator可以提供Elasticsearch健康检查:
properties
# application.properties
management.endpoint.health.show-details=always
management.health.elasticsearch.enabled=true
故障排除与最佳实践
常见问题
连接问题:确保Elasticsearch正在运行,并且连接配置正确
版本兼容性:确保Spring Data Elasticsearch版本与Elasticsearch服务器版本兼容
映射问题:字段类型映射不正确可能导致查询问题
过大的分页请求:避免请求过大的分页,使用scroll API代替
最佳实践
适当的映射:为字段选择合适的类型和分析器
索引配置:根据数据规模和查询模式配置分片和副本
批量操作:使用批量API提高索引性能
查询优化:使用过滤器和缓存提高查询效率
异步操作:对于大量操作,考虑使用异步和响应式API
总结
Spring Data Elasticsearch提供了丰富的功能集,简化了与Elasticsearch的集成和交互。通过其Repository抽象、ElasticsearchOperations API以及丰富的查询和聚合支持,开发者可以轻松构建强大的搜索、分析和实时数据处理应用。
无论是简单的文档存储和检索,还是复杂的全文搜索、地理空间查询和聚合分析,Spring Data Elasticsearch都能提供优雅的解决方案,同时保持与Spring生态系统的无缝集成。