Spring Boot 与 Elasticsearch 8.x 集成实战:从入门到精通
一、引言
在现代应用开发中,全文搜索和数据分析已经成为不可或缺的功能。Elasticsearch 作为一款强大的开源搜索和分析引擎,凭借其分布式架构、实时搜索能力和强大的聚合分析功能,已经成为业界首选的搜索解决方案。
Spring Boot 提供了对 Elasticsearch 的原生支持,通过 Spring Data Elasticsearch 模块,开发者可以轻松地将 Elasticsearch 集成到 Spring Boot 应用中。本文将深入探讨 Spring Boot 与 Elasticsearch 8.x 的集成实践,包括环境配置、核心 API 使用、高级查询以及性能优化等方面。
二、环境准备与依赖配置
2.1 依赖引入
在pom.xml中添加 Spring Data Elasticsearch 依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>2.2 配置文件设置
在application.yml中配置 Elasticsearch 连接信息:
spring: elasticsearch: uris: http://localhost:9200 username: elastic password: changeme connection-timeout: 10s socket-timeout: 30s2.3 客户端配置类
创建自定义的 Elasticsearch 客户端配置:
@Configuration public class ElasticsearchConfig { @Value("${spring.elasticsearch.uris}") private String elasticsearchUris; @Value("${spring.elasticsearch.username}") private String username; @Value("${spring.elasticsearch.password}") private String password; @Bean public RestClient restClient() { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(username, password) ); return RestClient.builder( HttpHost.create(elasticsearchUris) ).setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider) ).build(); } @Bean public ElasticsearchOperations elasticsearchOperations( RestClient restClient ) { ElasticsearchRestTemplate template = new ElasticsearchRestTemplate(restClient); return template; } }三、索引管理与文档操作
3.1 文档实体定义
定义一个商品实体类:
@Document(indexName = "products", createIndex = false) @Setting(shards = 3, replicas = 2) public class Product { @Id private String id; @Field(type = FieldType.Text, analyzer = "ik_max_word") private String name; @Field(type = FieldType.Keyword) private String category; @Field(type = FieldType.Double) private Double price; @Field(type = FieldType.Integer) private Integer stock; @Field(type = FieldType.Date, format = DateFormat.date_time) private LocalDateTime createTime; @Field(type = FieldType.Nested) private List<Review> reviews; // Getters and Setters } public class Review { @Field(type = FieldType.Text) private String content; @Field(type = FieldType.Integer) private Integer rating; @Field(type = FieldType.Date) private LocalDateTime reviewTime; // Getters and Setters }3.2 Repository 接口定义
创建数据访问层接口:
public interface ProductRepository extends ElasticsearchRepository<Product, String> { List<Product> findByNameContaining(String name); List<Product> findByCategory(String category); List<Product> findByPriceBetween(Double minPrice, Double maxPrice); @Query("{\"bool\": {\"must\": [{\"match\": {\"name\": \"?0\"}}]}}") List<Product> searchByName(String name); }3.3 基本 CRUD 操作
@Service public class ProductService { private final ProductRepository productRepository; private final ElasticsearchOperations elasticsearchOperations; public ProductService(ProductRepository productRepository, ElasticsearchOperations elasticsearchOperations) { this.productRepository = productRepository; this.elasticsearchOperations = elasticsearchOperations; } // 创建文档 public Product save(Product product) { return productRepository.save(product); } // 批量创建 public List<Product> saveAll(List<Product> products) { return productRepository.saveAll(products); } // 根据ID查询 public Optional<Product> findById(String id) { return productRepository.findById(id); } // 查询所有 public List<Product> findAll() { List<Product> result = new ArrayList<>(); productRepository.findAll().forEach(result::add); return result; } // 根据ID删除 public void deleteById(String id) { productRepository.deleteById(id); } // 删除所有 public void deleteAll() { productRepository.deleteAll(); } }四、高级查询与聚合分析
4.1 全文搜索
public List<Product> searchProducts(String keyword) { NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.multiMatchQuery(keyword, "name", "category")) .withHighlightFields(new HighlightBuilder.Field("name")) .withHighlightBuilder(new HighlightBuilder() .preTags("<em>") .postTags("</em>")) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class); return hits.stream() .map(SearchHit::getContent) .collect(Collectors.toList()); }4.2 布尔查询组合
public List<Product> advancedSearch(String keyword, String category, Double minPrice, Double maxPrice) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (keyword != null && !keyword.isEmpty()) { boolQuery.must(QueryBuilders.matchQuery("name", keyword)); } if (category != null && !category.isEmpty()) { boolQuery.filter(QueryBuilders.termQuery("category", category)); } 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); } NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(boolQuery) .withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC)) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class); return hits.stream() .map(SearchHit::getContent) .collect(Collectors.toList()); }4.3 聚合分析
public Map<String, Long> getCategoryStatistics() { NativeSearchQuery query = new NativeSearchQueryBuilder() .addAggregation(AggregationBuilders.terms("category_stats").field("category")) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class); Terms categoryStats = hits.getAggregations().get("category_stats"); return categoryStats.getBuckets().stream() .collect(Collectors.toMap( bucket -> bucket.getKeyAsString(), bucket -> bucket.getDocCount() )); } public Map<String, Double> getPriceStatistics() { NativeSearchQuery query = new NativeSearchQueryBuilder() .addAggregation(AggregationBuilders.stats("price_stats").field("price")) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class); Stats priceStats = hits.getAggregations().get("price_stats"); Map<String, Double> stats = new HashMap<>(); stats.put("min", priceStats.getMin()); stats.put("max", priceStats.getMax()); stats.put("avg", priceStats.getAvg()); stats.put("sum", priceStats.getSum()); return stats; }4.4 嵌套文档查询
public List<Product> searchByReviewContent(String reviewContent) { NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.nestedQuery( "reviews", QueryBuilders.matchQuery("reviews.content", reviewContent), ScoreMode.Avg )) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class); return hits.stream() .map(SearchHit::getContent) .collect(Collectors.toList()); }五、分页与排序
5.1 分页查询
public Page<Product> searchWithPagination(String keyword, int page, int size) { NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchQuery("name", keyword)) .withPageable(PageRequest.of(page, size)) .build(); SearchPage<Product> searchPage = elasticsearchOperations.searchForPage(query, Product.class); return searchPage; }5.2 多字段排序
public List<Product> searchWithMultiSort(String keyword) { NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchQuery("name", keyword)) .withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC)) .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class); return hits.stream() .map(SearchHit::getContent) .collect(Collectors.toList()); }六、索引管理
6.1 创建索引
public void createIndex(String indexName) { CreateIndexRequest request = new CreateIndexRequest(indexName); Map<String, Object> settings = new HashMap<>(); settings.put("number_of_shards", 3); settings.put("number_of_replicas", 2); request.settings(settings); Map<String, Object> mapping = new HashMap<>(); Map<String, Object> properties = new HashMap<>(); Map<String, Object> name = new HashMap<>(); name.put("type", "text"); name.put("analyzer", "ik_max_word"); properties.put("name", name); Map<String, Object> category = new HashMap<>(); category.put("type", "keyword"); properties.put("category", category); Map<String, Object> price = new HashMap<>(); price.put("type", "double"); properties.put("price", price); mapping.put("properties", properties); request.mapping(mapping); restClient().indices().create(request, RequestOptions.DEFAULT); }6.2 删除索引
public void deleteIndex(String indexName) { DeleteIndexRequest request = new DeleteIndexRequest(indexName); restClient().indices().delete(request, RequestOptions.DEFAULT); }6.3 索引别名管理
public void addAlias(String indexName, String aliasName) { AliasActions aliasAction = new AliasActions() .add(AliasAction.add().index(indexName).alias(aliasName)); restClient().indices().updateAliases(new UpdateAliasesRequest(aliasAction), RequestOptions.DEFAULT); }七、性能优化策略
7.1 查询优化
public List<Product> optimizedSearch(String keyword) { NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchQuery("name", keyword)) .withFetchSource(new String[]{"name", "price", "category"}, null) .withTrackTotalHits(false) .build(); SearchHits<Product> hits = elasticsearchOperations.search(query, Product.class); return hits.stream() .map(SearchHit::getContent) .collect(Collectors.toList()); }7.2 批量操作
public void bulkIndex(List<Product> products) { BulkOperations bulkOperations = elasticsearchOperations.bulk(); products.forEach(product -> bulkOperations.save(product) ); bulkOperations.execute(); }7.3 索引预热
public void warmUpIndex(String indexName) { NativeSearchQuery query = new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchAllQuery()) .withSize(100) .build(); elasticsearchOperations.search(query, Product.class); }八、错误处理与重试机制
8.1 自定义异常处理
@RestControllerAdvice public class ElasticsearchExceptionHandler { @ExceptionHandler(ElasticsearchException.class) public ResponseEntity<Map<String, String>> handleElasticsearchException( ElasticsearchException ex ) { Map<String, String> response = new HashMap<>(); response.put("error", ex.getMessage()); response.put("status", "FAILED"); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(response); } }8.2 重试配置
@Configuration public class RetryConfig { @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(3); FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); backOffPolicy.setBackOffPeriod(1000L); retryTemplate.setRetryPolicy(retryPolicy); retryTemplate.setBackOffPolicy(backOffPolicy); return retryTemplate; } }九、实战案例:商品搜索服务
9.1 Controller 层
@RestController @RequestMapping("/api/products") public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } @PostMapping public ResponseEntity<Product> create(@RequestBody Product product) { Product saved = productService.save(product); return ResponseEntity.status(HttpStatus.CREATED).body(saved); } @GetMapping("/{id}") public ResponseEntity<Product> getById(@PathVariable String id) { return productService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @GetMapping("/search") public ResponseEntity<List<Product>> search( @RequestParam String keyword, @RequestParam(required = false) String category, @RequestParam(required = false) Double minPrice, @RequestParam(required = false) Double maxPrice ) { List<Product> products = productService.advancedSearch( keyword, category, minPrice, maxPrice ); return ResponseEntity.ok(products); } @GetMapping("/statistics/categories") public ResponseEntity<Map<String, Long>> getCategoryStatistics() { Map<String, Long> statistics = productService.getCategoryStatistics(); return ResponseEntity.ok(statistics); } }9.2 测试用例
@SpringBootTest class ProductServiceTest { @Autowired private ProductService productService; @Test void testCRUDOperations() { Product product = new Product(); product.setName("iPhone 15 Pro"); product.setCategory("Electronics"); product.setPrice(9999.0); product.setStock(100); product.setCreateTime(LocalDateTime.now()); Product saved = productService.save(product); assertNotNull(saved.getId()); Optional<Product> found = productService.findById(saved.getId()); assertTrue(found.isPresent()); assertEquals("iPhone 15 Pro", found.get().getName()); productService.deleteById(saved.getId()); assertFalse(productService.findById(saved.getId()).isPresent()); } @Test void testSearch() { List<Product> results = productService.searchProducts("phone"); assertNotNull(results); } }十、总结
本文详细介绍了 Spring Boot 与 Elasticsearch 8.x 的集成实践,涵盖了从基础配置到高级特性的各个方面:
- 环境配置:依赖引入、连接配置、客户端初始化
- 文档操作:实体定义、Repository 接口、CRUD 操作
- 高级查询:全文搜索、布尔查询、嵌套文档查询
- 聚合分析:统计聚合、分组聚合
- 性能优化:查询优化、批量操作、索引预热
- 错误处理:异常处理、重试机制
通过本文的学习,读者可以掌握 Spring Boot 与 Elasticsearch 集成的核心技能,能够构建高效、可靠的搜索服务。在实际项目中,还需要根据业务需求进行适当的调整和优化,以达到最佳的性能和用户体验。