搜索慢了?SpringBoot+Elasticsearch让查询快100倍!

40 阅读9分钟

《搜索慢了?SpringBoot+Elasticsearch让查询快100倍!》

我是小坏,今天咱们聊搜索。用户搜个东西,转圈圈等半天,这体验可不行。数据库like查询是方便,但数据多了能把你卡死。今天教你用Elasticsearch(后面就叫ES),让搜索快到飞起。

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

一、数据库搜索的痛

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

场景:1000万商品,搜索"华为手机"

SELECT * FROM products 
WHERE name LIKE '%华为%' 
   OR description LIKE '%华为%';

问题

  • 全表扫描,不走索引
  • 返回慢,用户等3秒
  • 不能分词("华为手机"搜不到"华为P40手机")
  • 不能按相关度排序
  • 不支持拼音搜索

用ES能解决啥

  • 毫秒级返回
  • 自动分词
  • 支持拼音、同义词
  • 智能排序
  • 聚合统计

二、ES快速上手

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

2.1 先明白几个词

  1. 索引:相当于数据库
  2. 文档:相当于一行数据
  3. 字段:相当于列
  4. 分词:把句子拆成词,比如"华为手机"拆成"华为"和"手机"

2.2 3步集成

第一步:加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

第二步:写配置

spring:
  elasticsearch:
    uris: http://localhost:9200
    username: elastic
    password: 123456

第三步:定义商品实体

@Document(indexName = "product")
@Data
public class Product {
    @Id
    private Long id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;        // 商品名,用ik分词
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String description; // 描述
    
    @Field(type = FieldType.Double)
    private Double price;
    
    @Field(type = FieldType.Keyword)
    private String category;    // 分类,不分词
    
    @Field(type = FieldType.Integer)
    private Integer sales;      // 销量
}

三、数据同步:4种方案选哪个?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

3.1 双写(简单直接)

@Service
public class ProductService {
    public void addProduct(Product product) {
        // 1. 写数据库
        productRepository.save(product);
        // 2. 写ES
        elasticsearchRepository.save(product);
    }
}

优点:实时性强
缺点:可能不一致,影响性能

3.2 定时同步(稳定可靠)

@Component
public class SyncTask {
    @Scheduled(fixedDelay = 300000)  // 5分钟一次
    public void syncProducts() {
        // 查最近5分钟修改的商品
        List<Product> products = productRepository.findRecentUpdated();
        elasticsearchRepository.saveAll(products);
    }
}

优点:解耦,稳定
缺点:有延迟

3.3 消息队列(推荐)

商品更新发消息,消费者同步到ES。解耦,实时,但复杂。

3.4 监听binlog(高级玩法)

监听数据库变化自动同步。实时,对业务无侵入,但技术门槛高。

建议:中小项目用双写或定时同步,大项目用消息队列。

四、搜索实战

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

4.1 基础搜索

@Service
public class ProductSearchService {
    
    public Page<Product> search(String keyword, int page, int size) {
        // 构建查询
        NativeSearchQuery query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.multiMatchQuery(keyword, "name", "description"))
            .withPageable(PageRequest.of(page, size))
            .build();
        
        return elasticsearchRestTemplate.search(query, Product.class);
    }
}

4.2 多字段搜索+高亮

public SearchResult searchProducts(String keyword, int page, int size) {
    // 1. 多字段匹配
    MultiMatchQueryBuilder query = QueryBuilders.multiMatchQuery(keyword)
        .field("name", 3.0f)  // name权重3倍
        .field("description", 1.0f);
    
    // 2. 高亮显示
    HighlightBuilder highlightBuilder = new HighlightBuilder()
        .field("name")
        .preTags("<em>").postTags("</em>");
    
    NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
        .withQuery(query)
        .withHighlightBuilder(highlightBuilder)
        .withPageable(PageRequest.of(page, size))
        .build();
    
    // 执行搜索
    SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
    
    // 封装结果
    return convertToResult(searchHits);
}

4.3 拼音搜索

步骤

  1. 安装拼音分词插件
  2. 字段用拼音分词器
  3. 搜索时同时匹配中文和拼音
// 搜索时同时查中文和拼音
String[] fields = {"name", "name.pinyin", "description", "description.pinyin"};
MultiMatchQueryBuilder query = QueryBuilders.multiMatchQuery(keyword, fields);

4.4 同义词搜索

场景:搜"笔记本"也能找到"笔记本电脑"

// 同义词配置
"filter": {
  "my_synonym": {
    "type": "synonym",
    "synonyms": ["笔记本,笔记本电脑", "手机,移动电话"]
  }
}

五、排序和筛选

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

5.1 智能排序

// 按相关度、销量、价格综合排序
NativeSearchQuery query = new NativeSearchQueryBuilder()
    .withSort(SortBuilders.scoreSort())  // 相关度
    .withSort(SortBuilders.fieldSort("sales").order(SortOrder.DESC))  // 销量
    .withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC))   // 价格
    .build();

5.2 条件筛选

// 价格区间
RangeQueryBuilder priceFilter = QueryBuilders.rangeQuery("price")
    .gte(minPrice).lte(maxPrice);

// 分类筛选
TermQueryBuilder categoryFilter = QueryBuilders.termQuery("category", category);

// 组合查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
    .must(QueryBuilders.multiMatchQuery(keyword, "name", "description"))
    .filter(priceFilter)
    .filter(categoryFilter);

六、聚合分析

场景:搜索结果的分类统计,用于生成筛选条件

public Map<String, Long> getCategoryStats(String keyword) {
    // 按分类聚合
    TermsAggregationBuilder aggregation = AggregationBuilders.terms("by_category")
        .field("category")
        .size(10);
    
    NativeSearchQuery query = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.multiMatchQuery(keyword, "name", "description"))
        .addAggregation(aggregation)
        .build();
    
    SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
    
    // 解析聚合结果
    Terms terms = searchHits.getAggregations().get("by_category");
    Map<String, Long> result = new HashMap<>();
    for (Terms.Bucket bucket : terms.getBuckets()) {
        result.put(bucket.getKeyAsString(), bucket.getDocCount());
    }
    
    return result;
}

七、性能优化

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

7.1 索引优化

@Document(indexName = "product",
         createIndex = true,
         shards = 3,      // 分片数
         replicas = 1)    // 副本数
public class Product {
    // 字段优化
    @Field(type = FieldType.Keyword)
    private String sku;  // 精确查找用keyword
    
    @Field(type = FieldType.Integer, index = false)
    private Integer stock;  // 不索引库存,只用于显示
}

7.2 查询优化

// 避免深度分页
SearchAfterBuilder searchAfter = new SearchAfterBuilder();
searchAfter.setSortValues(new Object[]{lastScore, lastId});

// 使用filter替代query(不计算分数,可缓存)
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
    .must(QueryBuilders.matchQuery("name", keyword))
    .filter(QueryBuilders.termQuery("status", 1));  // 状态过滤用filter

7.3 缓存优化

@Service
public class ProductSearchService {
    
    @Cacheable(value = "search", key = "#keyword + ':' + #page")
    public Page<Product> search(String keyword, int page, int size) {
        // 热门搜索结果缓存
        return doSearch(keyword, page, size);
    }
}

八、监控告警

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

8.1 健康检查

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics
  elasticsearch:
    health:
      enabled: true

8.2 关键指标监控

  • 查询响应时间
  • 索引速度
  • 节点负载
  • 磁盘使用率

8.3 告警规则

@Component
public class ESMetricsMonitor {
    
    @Scheduled(fixedRate = 60000)
    public void checkHealth() {
        // 检查集群状态
        ClusterHealth health = elasticsearchRestTemplate.cluster().health();
        
        if (health.getStatus() == ClusterHealthStatus.RED) {
            // 发送告警
            alertService.send("ES集群异常,状态:" + health.getStatus());
        }
        
        // 检查查询延迟
        double avgQueryTime = getAverageQueryTime();
        if (avgQueryTime > 1000) {  // 超过1秒
            alertService.send("ES查询延迟过高:" + avgQueryTime + "ms");
        }
    }
}

九、实战:电商搜索完整流程

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

9.1 搜索建议

public List<String> getSuggestions(String prefix) {
    CompletionSuggestionBuilder suggestion = SuggestBuilders
        .completionSuggestion("suggest")
        .prefix(prefix)
        .size(5);
    
    SuggestBuilder suggestBuilder = new SuggestBuilder()
        .addSuggestion("product_suggest", suggestion);
    
    SearchRequest request = new SearchRequest("product")
        .source(new SearchSourceBuilder().suggest(suggestBuilder));
    
    // 执行搜索建议
    SearchResponse response = elasticsearchRestTemplate.suggest(request, RequestOptions.DEFAULT);
    
    // 解析结果
    return parseSuggestions(response);
}

9.2 搜索结果封装

@Data
public class SearchResult {
    private List<ProductVO> products;      // 商品列表
    private List<CategoryAgg> categories;  // 分类统计
    private List<BrandAgg> brands;         // 品牌统计
    private PriceRange priceRange;         // 价格区间
    private long total;                    // 总条数
    private int totalPage;                 // 总页数
    private List<String> suggestions;      // 搜索建议
}

// 搜索接口
@GetMapping("/search")
public SearchResult search(
        @RequestParam String keyword,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(required = false) String category,
        @RequestParam(required = false) Double minPrice,
        @RequestParam(required = false) Double maxPrice) {
    
    return productSearchService.search(keyword, page, size, category, minPrice, maxPrice);
}

十、今日要点

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

  1. ES比数据库like快得多,支持分词、拼音、同义词
  2. 数据同步4种方案:双写简单,定时稳定,消息队列推荐
  3. 搜索要智能:多字段、高亮、拼音、同义词
  4. 排序和筛选是电商搜索的刚需
  5. 聚合分析用于生成筛选条件
  6. 性能优化:索引设计、查询优化、缓存
  7. 监控告警不能少,出问题早知道

十一、避坑指南

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

坑1:分词器选错

// ❌ 错误:用默认分词器分中文
@Field(type = FieldType.Text)  // 默认分词器处理不好中文

// ✅ 正确:用ik分词器
@Field(type = FieldType.Text, analyzer = "ik_max_word")

坑2:字段类型用错

// ❌ 错误:分类用Text,搜索时会分词
@Field(type = FieldType.Text)
private String category;

// ✅ 正确:分类用Keyword,精确匹配
@Field(type = FieldType.Keyword)
private String category;

坑3:分页太深

// ❌ 错误:深度分页性能差
from: 10000, size: 20  // 性能差

// ✅ 正确:用search_after
search_after: [lastScore, lastId]

十二、思考题

场景:你要做一个商品搜索,要求:

  1. 支持中文、拼音、同义词
  2. 结果按综合排序(相关度+销量+价格)
  3. 左侧有分类、品牌、价格筛选
  4. 搜索框有智能提示
  5. 热门搜索词推荐

问题

  1. 你会如何设计ES索引?
  2. 如何实现拼音和同义词搜索?
  3. 如何优化搜索性能?

评论区聊聊你的方案,明儿咱们讲MyBatis-Plus。


明天预告:《SpringBoot+MyBatis-Plus:开发效率提升10倍》

今日福利:关注后回复"ES搜索",获取电商搜索完整源码和ik分词器配置。


运营小贴士:

💡 互动

  1. 你的项目搜索用的是什么方案?
  2. 遇到过哪些搜索性能问题?
  3. 留言提问,明天文章解答

👥 进群零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目

资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。