SpringBoot 深度集成 Elasticsearch 搜索引擎指南
1. Elasticsearch 简介与 SpringBoot 集成概述
Elasticsearch 是一个基于 Lucene 构建的开源、分布式、RESTful 搜索引擎。它能够实现近乎实时的搜索和分析功能,适用于处理各种类型的数据,包括结构化/非结构化文本、数字数据、地理空间数据等。
1.1 Elasticsearch 核心特性
- 分布式架构:自动分片数据并在多个节点上复制
- 近实时搜索:数据索引后几乎立即可搜索
- 多租户支持:通过索引逻辑隔离不同数据集
- 丰富的查询DSL:支持全文检索、结构化查询、复杂聚合等
- RESTful API:所有操作都通过 HTTP REST 接口完成
1.2 SpringBoot 集成 Elasticsearch 的优势
- 简化配置:SpringBoot 自动配置减少了样板代码
- Repository 抽象:类似 JPA 的操作方式,降低学习曲线
- 对象映射:自动将 Java 对象与 Elasticsearch 文档相互转换
- 事务支持:虽然不是 ACID,但提供了类似的事务抽象
- 与 Spring 生态无缝集成:可与 Spring Data、Spring Security 等协同工作
2. 环境准备与依赖配置
2.1 版本兼容性矩阵
在开始集成前,必须确保 Spring Data Elasticsearch、Elasticsearch 服务器和 SpringBoot 版本兼容:
SpringBoot 版本 | Spring Data Elasticsearch 版本 | Elasticsearch 版本 |
---|---|---|
2.7.x | 4.4.x | 7.17.x |
3.0.x | 5.0.x | 8.5.x |
3.1.x | 5.1.x | 8.7.x |
2.2 详细依赖配置
<!-- 基础依赖 -->
<dependencies>
<!-- SpringBoot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- 高级 REST 客户端 -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.17.3</version>
<exclusions>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 如果使用响应式编程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch-reactive</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.3 详细配置说明
2.3.1 单节点配置
spring:
elasticsearch:
rest:
uris: ["http://localhost:9200"]
username: "elastic" # 默认用户名
password: "your-password"
connection-timeout: 1000ms # 连接超时
socket-timeout: 30000ms # 套接字超时
max-conn-per-route: 10 # 每路由最大连接数
max-conn-total: 30 # 总最大连接数
2.3.2 集群配置
spring:
elasticsearch:
rest:
uris:
- "http://node1:9200"
- "http://node2:9200"
- "http://node3:9200"
sniffer:
enabled: true # 启用节点嗅探
interval: 10m # 嗅探间隔
delay-after-failure: 1m # 失败后延迟
3. 实体映射与索引管理
3.1 详细实体类注解
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
@Document(indexName = "products", createIndex = true)
@Setting(settingPath = "elasticsearch/settings/product-settings.json")
@Mapping(mappingPath = "elasticsearch/mappings/product-mapping.json")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
@Field(type = FieldType.Text, analyzer = "english")
private String description;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private Date createTime;
@Field(type = FieldType.Nested)
private List<Specification> specifications;
@Field(type = FieldType.Object)
private Manufacturer manufacturer;
@Field(type = FieldType.Boolean)
private Boolean available;
@Field(type = FieldType.Integer_Range)
private IntegerRange ageRange;
// 省略 getter/setter
}
public class Specification {
@Field(type = FieldType.Keyword)
private String key;
@Field(type = FieldType.Text)
private String value;
}
public class Manufacturer {
@Field(type = FieldType.Keyword)
private String name;
@Field(type = FieldType.Text)
private String address;
}
3.2 自定义映射文件
resources/elasticsearch/mappings/product-mapping.json:
{
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"specifications": {
"type": "nested",
"properties": {
"key": {
"type": "keyword"
},
"value": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
}
3.3 自定义设置文件
resources/elasticsearch/settings/product-settings.json:
{
"index": {
"number_of_shards": 3,
"number_of_replicas": 2,
"analysis": {
"analyzer": {
"ik_smart": {
"type": "custom",
"tokenizer": "ik_smart"
},
"ik_max_word": {
"type": "custom",
"tokenizer": "ik_max_word"
}
}
}
}
}
3.4 索引管理操作
import org.springframework.data.elasticsearch.core.IndexOperations;
@Service
public class IndexService {
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
// 创建索引
public boolean createIndex(Class<?> clazz) {
IndexOperations indexOps = elasticsearchTemplate.indexOps(clazz);
return indexOps.create();
}
// 删除索引
public boolean deleteIndex(Class<?> clazz) {
IndexOperations indexOps = elasticsearchTemplate.indexOps(clazz);
return indexOps.delete();
}
// 索引是否存在
public boolean indexExists(Class<?> clazz) {
IndexOperations indexOps = elasticsearchTemplate.indexOps(clazz);
return indexOps.exists();
}
// 刷新索引
public void refreshIndex(Class<?> clazz) {
IndexOperations indexOps = elasticsearchTemplate.indexOps(clazz);
indexOps.refresh();
}
// 更新映射
public boolean putMapping(Class<?> clazz) {
IndexOperations indexOps = elasticsearchTemplate.indexOps(clazz);
return indexOps.putMapping();
}
}
4. 数据操作详解
4.1 Repository 接口扩展
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
// 基本查询
List<Product> findByName(String name);
List<Product> findByPriceBetween(Double from, Double to);
List<Product> findByCategoryOrderByPriceDesc(String category);
// 分页查询
Page<Product> findByDescription(String description, Pageable pageable);
// 使用 @Query 注解
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
List<Product> findByNameCustom(String name);
// 多条件查询
List<Product> findByNameAndCategory(String name, String category);
// 使用聚合
@Aggregation(pipeline = {
"{\"$match\": {\"category\": \"?0\"}}",
"{\"$group\": {\"_id\": \"$manufacturer.name\", \"avgPrice\": {\"$avg\": \"$price\"}}}"
})
List<AveragePriceByManufacturer> averagePriceByManufacturer(String category);
// 原生查询
@Query("{\"bool\": {\"must\": [{\"match\": {\"name\": \"?0\"}}, {\"range\": {\"price\": {\"gte\": ?1, \"lte\": ?2}}}]}}")
List<Product> findByNameAndPriceRange(String name, Double minPrice, Double maxPrice);
}
public interface AveragePriceByManufacturer {
String getManufacturerName();
Double getAvgPrice();
}
4.2 ElasticsearchRestTemplate 高级操作
4.2.1 索引文档
public String indexProduct(Product product) {
IndexQuery indexQuery = new IndexQueryBuilder()
.withObject(product)
.withId(product.getId())
.withRefreshPolicy(RefreshPolicy.IMMEDIATE)
.build();
return elasticsearchTemplate.index(indexQuery, IndexCoordinates.of("products"));
}
4.2.2 批量操作
public List<String> bulkIndex(List<Product> products) {
List<IndexQuery> queries = products.stream()
.map(product -> new IndexQueryBuilder()
.withObject(product)
.withId(product.getId())
.build())
.collect(Collectors.toList());
return elasticsearchTemplate.bulkIndex(queries, IndexCoordinates.of("products"));
}
4.2.3 复杂查询构建
public List<Product> complexSearch(ProductSearchCriteria criteria) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 构建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词查询
if (StringUtils.isNotBlank(criteria.getKeyword())) {
boolQuery.must(QueryBuilders.multiMatchQuery(criteria.getKeyword(), "name", "description")
.operator(Operator.AND)
.minimumShouldMatch("75%"));
}
// 分类过滤
if (criteria.getCategories() != null && !criteria.getCategories().isEmpty()) {
boolQuery.filter(QueryBuilders.termsQuery("category", criteria.getCategories()));
}
// 价格范围
if (criteria.getMinPrice() != null || criteria.getMaxPrice() != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (criteria.getMinPrice() != null) {
rangeQuery.gte(criteria.getMinPrice());
}
if (criteria.getMaxPrice() != null) {
rangeQuery.lte(criteria.getMaxPrice());
}
boolQuery.filter(rangeQuery);
}
// 可用性
if (criteria.getAvailable() != null) {
boolQuery.filter(QueryBuilders.termQuery("available", criteria.getAvailable()));
}
// 嵌套查询
if (criteria.getSpecKey() != null && criteria.getSpecValue() != null) {
boolQuery.must(QueryBuilders.nestedQuery("specifications",
QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("specifications.key", criteria.getSpecKey()))
.must(QueryBuilders.matchQuery("specifications.value", criteria.getSpecValue())),
ScoreMode.Avg));
}
queryBuilder.withQuery(boolQuery);
// 排序
if (criteria.getSortBy() != null) {
SortOrder order = criteria.isAscending() ? SortOrder.ASC : SortOrder.DESC;
queryBuilder.withSort(SortBuilders.fieldSort(criteria.getSortBy()).order(order));
}
// 分页
queryBuilder.withPageable(PageRequest.of(criteria.getPage(), criteria.getSize()));
// 高亮
if (criteria.isHighlight()) {
queryBuilder.withHighlightFields(
new HighlightBuilder.Field("name").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("description").preTags("<em>").postTags("</em>")
);
}
// 聚合
if (criteria.isAggregate()) {
queryBuilder.addAggregation(AggregationBuilders.terms("categories").field("category"));
queryBuilder.addAggregation(AggregationBuilders.avg("avg_price").field("price"));
}
SearchHits<Product> searchHits = elasticsearchTemplate.search(queryBuilder.build(), Product.class);
// 处理结果
return searchHits.stream()
.map(hit -> {
Product product = hit.getContent();
// 处理高亮
if (hit.getHighlightFields().containsKey("name")) {
product.setName(hit.getHighlightFields().get("name").get(0));
}
if (hit.getHighlightFields().containsKey("description")) {
product.setDescription(hit.getHighlightFields().get("description").get(0));
}
return product;
})
.collect(Collectors.toList());
}
5. 高级特性与最佳实践
5.1 自定义转换器
@Configuration
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
@Override
public RestHighLevelClient elasticsearchClient() {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.withBasicAuth("elastic", "password")
.build();
return RestClients.create(clientConfiguration).rest();
}
@Bean
@Override
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(
elasticsearchMappingContext(), new DefaultConversionService()
);
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
@Bean
@Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(
Arrays.asList(
new ProductToMapConverter(),
new MapToProductConverter()
)
);
}
public static class ProductToMapConverter implements Converter<Product, Map<String, Object>> {
@Override
public Map<String, Object> convert(Product source) {
// 自定义转换逻辑
}
}
public static class MapToProductConverter implements Converter<Map<String, Object>, Product> {
@Override
public Product convert(Map<String, Object> source) {
// 自定义转换逻辑
}
}
}
5.2 异步与响应式编程
5.2.1 异步操作
@Service
public class AsyncProductService {
@Autowired
private AsyncElasticsearchRestTemplate asyncElasticsearchTemplate;
public CompletableFuture<String> indexProductAsync(Product product) {
IndexQuery indexQuery = new IndexQueryBuilder()
.withObject(product)
.build();
return asyncElasticsearchTemplate.index(indexQuery, IndexCoordinates.of("products"));
}
public CompletableFuture<SearchHits<Product>> searchAsync(String query) {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.queryStringQuery(query))
.build();
return asyncElasticsearchTemplate.search(searchQuery, Product.class);
}
}
5.2.2 响应式编程
@Repository
public interface ReactiveProductRepository extends ReactiveElasticsearchRepository<Product, String> {
Flux<Product> findByName(String name);
Mono<Page<Product>> findByCategory(String category, Pageable pageable);
}
@Service
public class ReactiveProductService {
@Autowired
private ReactiveProductRepository repository;
@Autowired
private ReactiveElasticsearchClient reactiveClient;
public Mono<Product> saveProduct(Product product) {
return repository.save(product);
}
public Flux<Product> searchProducts(String query) {
return repository.search(searchQuery(query));
}
public Mono<Long> countAvailableProducts() {
return repository.countByAvailable(true);
}
private Query searchQuery(String query) {
return new CriteriaQuery(new Criteria("name").matches(query)
.and("description").matches(query));
}
public Flux<Product> complexReactiveSearch(ProductSearchCriteria criteria) {
return reactiveClient.search(searchRequestBuilder -> {
SearchRequest request = new SearchRequest("products");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 构建查询条件...
sourceBuilder.query(boolQuery);
request.source(sourceBuilder);
return request;
}, Product.class).map(SearchHit::getContent);
}
}
5.3 性能优化策略
- 批量操作:使用 bulk API 进行批量索引/更新
- 合理分片:根据数据量设置合适的分片数(建议每个分片不超过50GB)
- 索引刷新间隔:对于写入频繁但实时性要求不高的场景,可以增加刷新间隔
@Document(indexName = "logs", refreshInterval = "30s")
public class LogEntry { ... }
- 使用过滤器缓存:对频繁使用的过滤条件使用 filter 而非 query
- 字段数据加载:对排序/聚合字段使用 doc_values
- 查询优化:
- 使用 bool filter 替代 bool must 对不计算分数的查询
- 合理使用 terminate_after 限制返回文档数
- 避免使用 script 查询
5.4 安全配置
5.4.1 基本认证
spring:
elasticsearch:
rest:
uris: ["https://elasticsearch.example.com:9200"]
username: "elastic"
password: "securepassword"
path-prefix: "/api" # 如果有路径前缀
ssl:
verification-mode: full # 证书验证模式
trust-store-path: classpath:elasticsearch/truststore.p12
trust-store-password: changeit
key-store-path: classpath:elasticsearch/keystore.p12
key-store-password: changeit
5.4.2 API 密钥认证
@Configuration
public class ElasticsearchSecurityConfig extends AbstractElasticsearchConfiguration {
@Value("${elasticsearch.api-key}")
private String apiKey;
@Override
public RestHighLevelClient elasticsearchClient() {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("elasticsearch.example.com:9200")
.usingSsl()
.withDefaultHeaders(new HttpHeaders() {{
add("Authorization", "ApiKey " + apiKey);
}})
.build();
return RestClients.create(clientConfiguration).rest();
}
}
6. 监控与维护
6.1 健康检查
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.core.MainResponse;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
@Component
public class ElasticsearchHealthIndicator implements HealthIndicator {
@Autowired
private RestHighLevelClient client;
@Override
public Health health() {
try {
MainResponse response = client.info(RequestOptions.DEFAULT);
return Health.up()
.withDetail("cluster_name", response.getClusterName())
.withDetail("version", response.getVersion().toString())
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
6.2 性能监控
@Service
public class ElasticsearchMetricsService {
@Autowired
private RestHighLevelClient client;
public Map<String, Object> getClusterStats() {
try {
ClusterStatsRequest request = new ClusterStatsRequest();
ClusterStatsResponse response = client.cluster().stats(request, RequestOptions.DEFAULT);
Map<String, Object> stats = new HashMap<>();
stats.put("nodes", response.getNodesStats().getCounts().getTotal());
stats.put("indices", response.getIndicesStats().getIndexCount());
stats.put("docs", response.getIndicesStats().getDocs().getCount());
stats.put("storeSize", response.getIndicesStats().getStore().getSize());
stats.put("queryLatency", response.getIndicesStats().getQueryCache().getHitCount());
return stats;
} catch (IOException e) {
throw new RuntimeException("Failed to get cluster stats", e);
}
}
public Map<String, Object> getIndexStats(String indexName) {
try {
IndicesStatsRequest request = new IndicesStatsRequest().indices(indexName);
IndicesStatsResponse response = client.indices().stats(request, RequestOptions.DEFAULT);
Map<String, Object> stats = new HashMap<>();
IndexStats indexStats = response.getIndex(indexName);
stats.put("totalDocs", indexStats.getPrimaries().getDocs().getCount());
stats.put("sizeInBytes", indexStats.getPrimaries().getStore().getSizeInBytes());
stats.put("queryCount", indexStats.getTotal().getSearch().getTotal().getQueryCount());
stats.put("fetchLatency", indexStats.getTotal().getSearch().getTotal().getFetchTimeInMillis());
return stats;
} catch (IOException e) {
throw new RuntimeException("Failed to get index stats", e);
}
}
}
6.3 索引生命周期管理 (ILM)
@Service
public class IndexLifecycleService {
@Autowired
private RestHighLevelClient client;
public void setupIlmPolicy() throws IOException {
// 创建生命周期策略
Map<String, LifecycleAction> hotPhaseActions = Map.of(
"rollover", new RolloverLifecycleAction(null, null, null, null),
"set_priority", new SetPriorityLifecycleAction(100)
);
Map<String, LifecycleAction> warmPhaseActions = Map.of(
"shrink", new ShrinkLifecycleAction(1),
"forcemerge", new ForceMergeLifecycleAction(1),
"allocate", new AllocateLifecycleAction(1, null, Map.of("data", "warm"), null),
"set_priority", new SetPriorityLifecycleAction(50)
);
Map<String, LifecycleAction> deletePhaseActions = Map.of(
"delete", new DeleteLifecycleAction()
);
Phases phases = new Phases();
phases.setHot(new Phase("hot", TimeValue.timeValueHours(1), hotPhaseActions));
phases.setWarm(new Phase("warm", TimeValue.timeValueDays(7), warmPhaseActions));
phases.setDelete(new Phase("delete", TimeValue.timeValueDays(30), deletePhaseActions));
LifecyclePolicy policy = new LifecyclePolicy("log_policy", phases);
PutLifecyclePolicyRequest request = new PutLifecyclePolicyRequest(policy);
client.indexLifecycle().putLifecyclePolicy(request, RequestOptions.DEFAULT);
// 应用策略到索引模板
Map<String, Object> settings = Map.of(
"index.lifecycle.name", "log_policy",
"index.lifecycle.rollover_alias", "logs"
);
PutComposableIndexTemplateRequest templateRequest = new PutComposableIndexTemplateRequest()
.name("log_template")
.patterns(List.of("logs-*"))
.settings(settings)
.alias(new Alias("logs").writeIndex(true));
client.indices().putIndexTemplate(templateRequest, RequestOptions.DEFAULT);
}
}
7. 实战案例:电商商品搜索系统
7.1 系统架构设计
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 前端应用/API │───▶│ SpringBoot应用 │───▶│ Elasticsearch集群 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
▲ ▲ ▲
│ │ │
┌──────┴───────┐ ┌─────────┴─────────┐ ┌──────┴───────┐
│ 缓存(Redis) │ │ 关系数据库(MySQL) │ │ 消息队列(Kafka) │
└──────────────┘ └───────────────────┘ └───────────────┘
7.2 核心功能实现
7.2.1 商品索引服务
@Service
@Slf4j
public class ProductIndexServiceImpl implements ProductIndexService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
@Autowired
private KafkaTemplate<String, ProductEvent> kafkaTemplate;
@KafkaListener(topics = "product-events")
public void handleProductEvent(ProductEvent event) {
try {
switch (event.getType()) {
case CREATE:
case UPDATE:
indexProduct(event.getProduct());
break;
case DELETE:
deleteProduct(event.getProduct().getId());
break;
case BULK_INDEX:
bulkIndex(event.getProducts());
break;
}
} catch (Exception e) {
log.error("Failed to process product event: {}", event, e);
}
}
@Override
@Transactional
public void indexProduct(Product product) {
// 确保数据库和ES同步
productRepository.save(product);
// 发送索引事件
kafkaTemplate.send("product-events",
new ProductEvent(ProductEvent.Type.UPDATE, product));
}
@Override
public void bulkIndex(List<Product> products) {
if (products == null || products.isEmpty()) {
return;
}
List<IndexQuery> queries = products.stream()
.map(p -> new IndexQueryBuilder()
.withObject(p)
.withId(p.getId())
.build())
.collect(Collectors.toList());
elasticsearchTemplate.bulkIndex(queries, IndexCoordinates.of("products"));
}
@Override
public void deleteProduct(String productId) {
productRepository.deleteById(productId);
}
@Override
public void reindexAll() {
// 从数据库获取所有商品
List<Product> products = productRepository.findAllFromDb();
// 删除现有索引
elasticsearchTemplate.indexOps(Product.class).delete();
// 创建新索引
elasticsearchTemplate.indexOps(Product.class).create();
elasticsearchTemplate.indexOps(Product.class).putMapping();
// 批量索引
bulkIndex(products);
}
}
7.2.2 商品搜索服务
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public SearchResult<Product> search(ProductSearchRequest request) {
String cacheKey = buildCacheKey(request);
// 尝试从缓存获取
SearchResult<Product> cachedResult = (SearchResult<Product>) redisTemplate.opsForValue().get(cacheKey);
if (cachedResult != null) {
return cachedResult;
}
// 构建查询
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 关键词查询
if (StringUtils.isNotBlank(request.getQuery())) {
queryBuilder.withQuery(QueryBuilders.multiMatchQuery(request.getQuery(),
"name^3", "description^2", "specifications.value")
.operator(Operator.AND)
.minimumShouldMatch("75%"));
}
// 分类过滤
if (CollectionUtils.isNotEmpty(request.getCategories())) {
queryBuilder.withFilter(QueryBuilders.termsQuery("category", request.getCategories()));
}
// 价格范围
if (request.getMinPrice() != null || request.getMaxPrice() != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (request.getMinPrice() != null) {
rangeQuery.gte(request.getMinPrice());
}
if (request.getMaxPrice() != null) {
rangeQuery.lte(request.getMaxPrice());
}
queryBuilder.withFilter(rangeQuery);
}
// 排序
if (StringUtils.isNotBlank(request.getSortBy())) {
SortOrder order = request.isAscending() ? SortOrder.ASC : SortOrder.DESC;
queryBuilder.withSort(SortBuilders.fieldSort(request.getSortBy()).order(order));
}
// 分页
queryBuilder.withPageable(PageRequest.of(request.getPage(), request.getSize()));
// 高亮
if (StringUtils.isNotBlank(request.getQuery())) {
queryBuilder.withHighlightFields(
new HighlightBuilder.Field("name").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("description").preTags("<em>").postTags("</em>")
);
}
// 聚合
queryBuilder.addAggregation(AggregationBuilders.terms("categories").field("category"));
queryBuilder.addAggregation(AggregationBuilders.avg("avg_price").field("price"));
queryBuilder.addAggregation(AggregationBuilders.range("price_ranges")
.field("price")
.addRange(0, 50)
.addRange(50, 100)
.addRange(100, 200)
.addRange(200, 500)
.addRange(500, 1000)
.addRange(1000, Double.MAX_VALUE));
// 执行查询
SearchHits<Product> searchHits = elasticsearchTemplate.search(queryBuilder.build(), Product.class);
// 处理结果
List<Product> products = searchHits.stream()
.map(hit -> {
Product product = hit.getContent();
// 处理高亮
if (hit.getHighlightFields().containsKey("name")) {
product.setName(hit.getHighlightFields().get("name").get(0));
}
if (hit.getHighlightFields().containsKey("description")) {
product.setDescription(hit.getHighlightFields().get("description").get(0));
}
return product;
})
.collect(Collectors.toList());
// 处理聚合
Aggregations aggregations = searchHits.getAggregations();
Map<String, Long> categoryCounts = ((Terms) aggregations.get("categories"))
.getBuckets().stream()
.collect(Collectors.toMap(
Terms.Bucket::getKeyAsString,
Terms.Bucket::getDocCount
));
Map<String, Long> priceRangeCounts = ((Range) aggregations.get("price_ranges"))
.getBuckets().stream()
.collect(Collectors.toMap(
b -> b.getKeyAsString(),
Range.Bucket::getDocCount
));
double avgPrice = ((Avg) aggregations.get("avg_price")).getValue();
// 构建结果
SearchResult<Product> result = new SearchResult<>();
result.setProducts(products);
result.setTotal(searchHits.getTotalHits());
result.setCategoryCounts(categoryCounts);
result.setPriceRangeCounts(priceRangeCounts);
result.setAvgPrice(avgPrice);
// 缓存结果
redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES);
return result;
}
private String buildCacheKey(ProductSearchRequest request) {
return String.format("product_search:%s:%s:%s:%s:%d:%d",
request.getQuery(),
String.join(",", request.getCategories()),
request.getMinPrice(),
request.getMaxPrice(),
request.getPage(),
request.getSize());
}
}
8. 常见问题与解决方案
8.1 性能问题排查
问题现象:查询响应慢
排查步骤:
- 检查 Elasticsearch 集群健康状态
ClusterHealthRequest request = new ClusterHealthRequest();
ClusterHealthResponse response = client.cluster().health(request, RequestOptions.DEFAULT);
String status = response.getStatus().name(); // 应为 GREEN 或 YELLOW
- 分析慢查询日志
SearchSlowLogResponse slowLogResponse = client.indices()
.searchSlowLog(new SearchSlowLogRequest("products"), RequestOptions.DEFAULT);
- 使用 Profile API 分析查询执行情况
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
.query(QueryBuilders.matchAllQuery())
.profile(true);
SearchRequest request = new SearchRequest("products").source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
String profileResults = response.getProfileResults().toString();
- 检查索引统计信息
IndicesStatsRequest statsRequest = new IndicesStatsRequest().indices("products");
IndicesStatsResponse statsResponse = client.indices().stats(statsRequest, RequestOptions.DEFAULT);
8.2 数据一致性问题
解决方案:
- 使用双写策略确保数据库和ES同步
- 实现基于事件驱动的最终一致性
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleProductChange(ProductChangeEvent event) {
kafkaTemplate.send("product-events", event);
}
- 定期全量同步
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void fullSync() {
reindexAll();
}
8.3 映射冲突问题
解决方案:
- 使用明确的映射定义而非自动推断
- 实现自定义转换器处理复杂类型
- 使用动态模板处理未知字段
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
9. 未来发展与扩展
9.1 向量搜索集成
@Document(indexName = "vector_products")
public class VectorProduct {
@Id
private String id;
@Field(type = FieldType.Text)
private String name;
@Field(type = FieldType.Dense_Vector, dims = 128)
private float[] vector;
}
public interface VectorProductRepository extends ElasticsearchRepository<VectorProduct, String> {
@Query("{\"knn\": {\"field\": \"vector\", \"query_vector\": ?0, \"k\": 10, \"num_candidates\": 100}}")
List<VectorProduct> findSimilarProducts(float[] queryVector);
}
9.2 机器学习集成
public List<Product> recommendProducts(String userId) {
TrainedModelConfig model = client.machineLearning()
.getTrainedModel(new GetTrainedModelsRequest("product_recommender"), RequestOptions.DEFAULT)
.getTrainedModelConfigs().get(0);
InferTrainedModelRequest request = new InferTrainedModelRequest(
"product_recommender",
Collections.singletonList(Collections.singletonMap("user_id", userId)),
new InferenceConfigUpdate.Builder().build()
);
InferTrainedModelResponse response = client.machineLearning()
.inferTrainedModel(request, RequestOptions.DEFAULT);
List<String> productIds = response.getInferenceResults().stream()
.map(r -> (String) r.getPredictedValue())
.collect(Collectors.toList());
return productRepository.findAllById(productIds);
}
10. 总结
本文详细介绍了 SpringBoot 集成 Elasticsearch 的完整方案,从基础配置到高级特性,涵盖了:
- 环境准备与版本兼容性
- 实体映射与索引管理
- 数据操作与复杂查询构建
- 高级特性如异步、响应式编程
- 性能优化与安全配置
- 监控维护与实战案例
- 常见问题解决方案
- 未来发展方向
通过本指南,您应该能够在 SpringBoot 项目中高效地集成 Elasticsearch,构建强大的搜索功能,并根据业务需求进行定制化开发。