【SpringBoot集成篇】SpringBoot 深度集成 Elasticsearch 搜索引擎指南

1. Elasticsearch 简介与 SpringBoot 集成概述

Elasticsearch 是一个基于 Lucene 构建的开源、分布式、RESTful 搜索引擎。它能够实现近乎实时的搜索和分析功能,适用于处理各种类型的数据,包括结构化/非结构化文本、数字数据、地理空间数据等。

1.1 Elasticsearch 核心特性

  1. 分布式架构:自动分片数据并在多个节点上复制
  2. 近实时搜索:数据索引后几乎立即可搜索
  3. 多租户支持:通过索引逻辑隔离不同数据集
  4. 丰富的查询DSL:支持全文检索、结构化查询、复杂聚合等
  5. RESTful API:所有操作都通过 HTTP REST 接口完成

1.2 SpringBoot 集成 Elasticsearch 的优势

  1. 简化配置:SpringBoot 自动配置减少了样板代码
  2. Repository 抽象:类似 JPA 的操作方式,降低学习曲线
  3. 对象映射:自动将 Java 对象与 Elasticsearch 文档相互转换
  4. 事务支持:虽然不是 ACID,但提供了类似的事务抽象
  5. 与 Spring 生态无缝集成:可与 Spring Data、Spring Security 等协同工作

2. 环境准备与依赖配置

2.1 版本兼容性矩阵

在开始集成前,必须确保 Spring Data Elasticsearch、Elasticsearch 服务器和 SpringBoot 版本兼容:

SpringBoot 版本Spring Data Elasticsearch 版本Elasticsearch 版本
2.7.x4.4.x7.17.x
3.0.x5.0.x8.5.x
3.1.x5.1.x8.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 性能优化策略

  1. 批量操作:使用 bulk API 进行批量索引/更新
  2. 合理分片:根据数据量设置合适的分片数(建议每个分片不超过50GB)
  3. 索引刷新间隔:对于写入频繁但实时性要求不高的场景,可以增加刷新间隔
@Document(indexName = "logs", refreshInterval = "30s")
public class LogEntry { ... }
  1. 使用过滤器缓存:对频繁使用的过滤条件使用 filter 而非 query
  2. 字段数据加载:对排序/聚合字段使用 doc_values
  3. 查询优化:
    • 使用 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 性能问题排查

问题现象:查询响应慢
排查步骤:

  1. 检查 Elasticsearch 集群健康状态
ClusterHealthRequest request = new ClusterHealthRequest();
ClusterHealthResponse response = client.cluster().health(request, RequestOptions.DEFAULT);
String status = response.getStatus().name();  // 应为 GREEN 或 YELLOW
  1. 分析慢查询日志
SearchSlowLogResponse slowLogResponse = client.indices()
    .searchSlowLog(new SearchSlowLogRequest("products"), RequestOptions.DEFAULT);
  1. 使用 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();
  1. 检查索引统计信息
IndicesStatsRequest statsRequest = new IndicesStatsRequest().indices("products");
IndicesStatsResponse statsResponse = client.indices().stats(statsRequest, RequestOptions.DEFAULT);

8.2 数据一致性问题

解决方案:

  1. 使用双写策略确保数据库和ES同步
  2. 实现基于事件驱动的最终一致性
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleProductChange(ProductChangeEvent event) {
    kafkaTemplate.send("product-events", event);
}
  1. 定期全量同步
@Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨3点执行
public void fullSync() {
    reindexAll();
}

8.3 映射冲突问题

解决方案:

  1. 使用明确的映射定义而非自动推断
  2. 实现自定义转换器处理复杂类型
  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 的完整方案,从基础配置到高级特性,涵盖了:

  1. 环境准备与版本兼容性
  2. 实体映射与索引管理
  3. 数据操作与复杂查询构建
  4. 高级特性如异步、响应式编程
  5. 性能优化与安全配置
  6. 监控维护与实战案例
  7. 常见问题解决方案
  8. 未来发展方向
    通过本指南,您应该能够在 SpringBoot 项目中高效地集成 Elasticsearch,构建强大的搜索功能,并根据业务需求进行定制化开发。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜雨hiyeyu.com

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值