SpringBoot 整合 ElasticSearch,给搜索插上"光速翅膀"

传统搜索的痛点

在我们的日常开发工作中,经常会遇到这样的需求:

  • 用户输入一个关键词,要在海量数据中找到相关信息
  • 要支持模糊匹配、多字段搜索、高亮显示等功能
  • 搜索结果要有合理的排序和分页
  • 还要支持复杂的查询条件组合

如果用传统的关系型数据库来做搜索,你会发现性能越来越差,用户体验也越来越糟糕。特别是在数据量达到百万、千万级别时,普通的LIKE查询简直就是灾难。

解决方案思路

今天我们要解决的,就是如何用ElasticSearch来提升搜索性能,让它飞起来。

核心思路是:

  1. 全文检索:利用ES的倒排索引机制实现高效的文本搜索
  2. 分布式架构:利用ES天然的分布式特性处理海量数据
  3. 智能分析:利用ES的分析器实现更智能的搜索体验
  4. 实时搜索:数据变更后能快速反映到搜索结果中

技术选型

  • SpringBoot:快速搭建应用
  • ElasticSearch:全文搜索引擎
  • Spring Data Elasticsearch:Spring生态集成
  • Kibana:可视化管理和调试

核心实现思路

1. 环境搭建与配置

首先,我们需要配置ES连接:

# application.yml
spring:
  elasticsearch:
    uris: http://localhost:9200
    connection-timeout: 5s
    socket-timeout: 60s
  data:
    elasticsearch:
      cluster-nodes: localhost:9300
      cluster-name: elasticsearch

2. 数据实体映射

定义ES中的文档结构:

@Document(indexName = "products")
public class ProductDocument {
    
    @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.Text, analyzer = "ik_max_word")
    private String description;
    
    @Field(type = FieldType.Date)
    private Date createTime;
    
    // getter/setter...
}

3. 搜索服务实现

创建搜索服务类:

@Service
public class ProductSearchService {
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    /**
     * 简单关键词搜索
     */
    public Page<ProductDocument> searchByKeyword(String keyword, Pageable pageable) {
        // 构建查询条件
        Criteria criteria = new Criteria("name").contains(keyword)
                .or(new Criteria("description").contains(keyword));
        
        CriteriaQuery query = new CriteriaQuery(criteria);
        query.setPageable(pageable);
        
        SearchHits<ProductDocument> searchHits = 
            elasticsearchTemplate.search(query, ProductDocument.class);
        
        return SearchHitSupport.pageResultsFor(searchHits, pageable);
    }
    
    /**
     * 复杂条件搜索
     */
    public Page<ProductDocument> complexSearch(ProductSearchCriteria criteria, Pageable pageable) {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        // 名称模糊匹配
        if (StringUtils.hasText(criteria.getKeyword())) {
            boolQuery.should(QueryBuilders.matchQuery("name", criteria.getKeyword())
                    .boost(2.0f)); // 提高名称匹配的权重
            boolQuery.should(QueryBuilders.matchQuery("description", criteria.getKeyword()));
        }
        
        // 价格范围
        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 (StringUtils.hasText(criteria.getCategory())) {
            boolQuery.filter(QueryBuilders.termQuery("category.keyword", criteria.getCategory()));
        }
        
        // 构建搜索请求
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withPageable(pageable)
                .build();
        
        SearchHits<ProductDocument> searchHits = 
            elasticsearchTemplate.search(searchQuery, ProductDocument.class);
        
        return SearchHitSupport.pageResultsFor(searchHits, pageable);
    }
}

4. 实时同步机制

确保MySQL数据与ES索引保持同步:

@Component
@Slf4j
public class ProductSyncService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchTemplate;
    
    /**
     * 新增或更新产品时同步到ES
     */
    @EventListener
    public void handleProductSaved(ProductSavedEvent event) {
        Product product = event.getProduct();
        ProductDocument document = convertToDocument(product);
        
        elasticsearchTemplate.save(document);
        log.info("产品数据已同步到ES: {}", product.getId());
    }
    
    /**
     * 删除产品时同步删除ES记录
     */
    @EventListener
    public void handleProductDeleted(ProductDeletedEvent event) {
        String productId = event.getProductId();
        elasticsearchTemplate.delete(productId, ProductDocument.class);
        log.info("产品数据已从ES删除: {}", productId);
    }
    
    /**
     * 全量数据同步(用于初始化或修复)
     */
    public void syncAllProducts() {
        Pageable pageable = PageRequest.of(0, 1000);
        Page<Product> page;
        int pageNum = 0;
        
        do {
            page = productRepository.findAll(pageable);
            List<ProductDocument> documents = page.getContent().stream()
                    .map(this::convertToDocument)
                    .collect(Collectors.toList());
            
            if (!documents.isEmpty()) {
                elasticsearchTemplate.save(documents);
                log.info("同步第{}页数据,共{}条", pageNum++, documents.size());
            }
            
            pageable = page.nextPageable();
        } while (page.hasNext());
        
        log.info("全量数据同步完成");
    }
    
    private ProductDocument convertToDocument(Product product) {
        ProductDocument document = new ProductDocument();
        document.setId(product.getId());
        document.setName(product.getName());
        document.setCategory(product.getCategory());
        document.setPrice(product.getPrice());
        document.setDescription(product.getDescription());
        document.setCreateTime(product.getCreateTime());
        return document;
    }
}

5. 搜索API接口

提供RESTful API供前端调用:

@RestController
@RequestMapping("/api/search")
public class SearchController {
    
    @Autowired
    private ProductSearchService productSearchService;
    
    @GetMapping("/products")
    public Result<Page<ProductDocument>> searchProducts(
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) String category,
            @RequestParam(required = false) Double minPrice,
            @RequestParam(required = false) Double maxPrice,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        ProductSearchCriteria criteria = new ProductSearchCriteria();
        criteria.setKeyword(keyword);
        criteria.setCategory(category);
        criteria.setMinPrice(minPrice);
        criteria.setMaxPrice(maxPrice);
        
        Pageable pageable = PageRequest.of(page, size);
        Page<ProductDocument> result = productSearchService.complexSearch(criteria, pageable);
        
        return Result.success(result);
    }
    
    /**
     * 搜索建议(自动补全)
     */
    @GetMapping("/suggest")
    public Result<List<String>> suggest(@RequestParam String keyword) {
        // 使用ES的completion suggester实现自动补全
        CompletionQueryBuilder completionQuery = 
            new CompletionQueryBuilder("name.suggest", keyword);
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(completionQuery)
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        SearchHits<ProductDocument> searchHits = 
            elasticsearchTemplate.search(searchQuery, ProductDocument.class);
        
        List<String> suggestions = searchHits.getSearchHits().stream()
                .map(hit -> hit.getContent().getName())
                .distinct()
                .limit(10)
                .collect(Collectors.toList());
        
        return Result.success(suggestions);
    }
}

性能优化策略

1. 索引优化

// 设置索引模板以优化性能
@PostConstruct
public void setupIndexTemplate() {
    IndexOperations indexOps = elasticsearchTemplate.indexOps(ProductDocument.class);
    
    if (!indexOps.exists()) {
        indexOps.createWithMapping();
        
        // 设置索引设置
        Map<String, Object> settings = new HashMap<>();
        settings.put("number_of_shards", 3);
        settings.put("number_of_replicas", 1);
        settings.put("refresh_interval", "30s"); // 提高写入性能
        
        indexOps.updateSettings(settings);
    }
}

2. 查询优化

  • 使用Filter Context而非Query Context来过滤数据
  • 合理设置分页大小,避免深度分页
  • 使用聚合查询替代多次独立查询

3. 缓存策略

@Service
public class CachedSearchService {
    
    @Cacheable(value = "productSearch", key = "#keyword + '_' + #pageable.pageNumber")
    public Page<ProductDocument> searchWithCache(String keyword, Pageable pageable) {
        // 带缓存的搜索实现
        return productSearchService.searchByKeyword(keyword, pageable);
    }
}

优势分析

相比传统数据库搜索,ES方案的优势显而易见:

  1. 性能卓越:毫秒级响应,即使面对亿级数据
  2. 功能丰富:支持复杂查询、聚合分析、相关性排序等
  3. 扩展性强:天然分布式架构,易于水平扩展
  4. 生态完善:丰富的分析器、插件和可视化工具

注意事项

  1. 数据一致性:ES与数据库之间可能存在短暂不一致
  2. 资源消耗:ES对内存和磁盘要求较高
  3. 运维复杂:需要专门的ES集群运维知识
  4. 成本考量:云服务费用可能较高

总结

通过SpringBoot整合ElasticSearch,我们可以轻松构建出高性能的搜索功能。ES的强大之处不仅在于搜索速度,更在于它提供了丰富的分析和查询能力,能够满足各种复杂的业务场景。

在实际项目中,建议根据具体需求选择合适的部署方案,并做好监控和运维工作。搜索功能作为用户体验的关键环节,值得我们投入足够的精力去优化和完善。


服务端技术精选,专注分享后端开发实战技术,助力你的技术成长!


标题:SpringBoot 整合 ElasticSearch,给搜索插上"光速翅膀"
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/08/1767868407740.html

    0 评论
avatar