【架构实战】搜索系统架构设计:从精准匹配到智能推荐

一、搜索慢如牛让我流失了30%用户

2017年,我负责的电商网站搜索功能特别慢,搜一个关键词要3-5秒才能出结果。

用户反馈说:"搜个东西这么慢,我还不如去京东买。"

后来用上ElasticSearch后,搜索时间降到了100毫秒以内,转化率提升了30%。

从那以后我就明白:搜索是电商的核心体验,搜索做不好,用户就跑了。


二、搜索系统架构

2.1 整体架构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        搜索系统架构                               │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                      数据源层                               │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐   │ │
│  │  │ 商品数据 │ │ 用户数据 │ │ 订单数据 │ │   内容数据   │   │ │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────────┘   │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                              ↓                                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                      数据同步层                              │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐                   │ │
│  │  │ Canal    │ │  Logstash│ │  定时任务 │                   │ │
│  │  │ 增量同步  │ │  实时同步 │ │  全量同步  │                   │ │
│  │  └──────────┘ └──────────┘ └──────────┘                   │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                              ↓                                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                      索引服务层                              │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐  │ │
│  │  │ 索引管理  │ │ 文档管理  │ │ 分词管理  │ │   别名管理   │  │ │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────────┘  │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                              ↓                                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                      查询服务层                               │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐  │ │
│  │  │ 关键词搜索│ │ 聚合查询 │ │  智能建议 │ │   排序服务   │  │ │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────────┘  │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                              ↓                                   │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                      缓存层                                 │ │
│  │  ┌──────────┐ ┌──────────┐                                  │ │
│  │  │ 查询缓存 │ │ Suggest缓存│                                  │ │
│  │  └──────────┘ └──────────┘                                  │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

2.2 商品索引模型

json 复制代码
{
  "mappings": {
    "properties": {
      "productId": {"type": "keyword"},
      "productName": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": {"type": "keyword"}
        }
      },
      "categoryId": {"type": "keyword"},
      "categoryPath": {"type": "keyword"},
      "brandId": {"type": "keyword"},
      "brandName": {"type": "keyword"},
      "price": {"type": "integer"},
      "originalPrice": {"type": "integer"},
      "stock": {"type": "integer"},
      "salesCount": {"type": "integer"},
      "score": {"type": "float"},
      "tags": {"type": "keyword"},
      "attributes": {
        "type": "nested",
        "properties": {
          "name": {"type": "keyword"},
          "value": {"type": "keyword"}
        }
      },
      "images": {"type": "keyword"},
      "status": {"type": "keyword"},
      "createTime": {"type": "date"},
      "updateTime": {"type": "date"}
    }
  }
}

三、ElasticSearch索引操作

3.1 索引管理

java 复制代码
/**
 * ES索引服务
 */
@Service
@Slf4j
public class EsIndexService {
    
    @Autowired
    private RestHighLevelClient esClient;
    
    /**
     * 创建索引(带别名)
     */
    public void createIndexWithAlias(String indexName, String aliasName, String mapping) 
            throws IOException {
        
        // 1. 创建索引
        CreateIndexRequest request = new CreateIndexRequest(indexName);
        request.settings(Settings.builder()
            .put("number_of_shards", 3)
            .put("number_of_replicas", 1)
            .put("analysis.analyzer.ik_max_word.type", "ik_max_word")
            .put("analysis.analyzer.ik_smart.type", "ik_smart")
        );
        
        // 2. 设置mapping
        request.mapping(mapping, XContentType.JSON);
        
        // 3. 创建索引
        CreateIndexResponse response = esClient.indices().create(request, RequestOptions.DEFAULT);
        
        // 4. 创建别名
        AliasAction addAlias = new AliasAction.Add(indexName, aliasName);
        IndicesAliasesRequest aliasRequest = new IndicesAliasesRequest();
        aliasRequest.addAliasAction(addAlias);
        esClient.indices().alias(aliasRequest, RequestOptions.DEFAULT);
        
        log.info("索引创建成功: index={}, alias={}", indexName, aliasName);
    }
    
    /**
     * 重建索引(切换别名)
     */
    public void rebuildIndex(String aliasName, String newIndexName, String mapping) 
            throws IOException {
        
        // 1. 创建新索引
        createIndex(newIndexName, mapping);
        
        // 2. 迁移数据(如果是增量,可以用scroll API)
        // migrateData(aliasName, newIndexName);
        
        // 3. 切换别名到新索引
        IndicesAliasesRequest request = new IndicesAliasesRequest();
        
        // 删除旧索引别名
        request.addAliasAction(new AliasAction.Remove(
            getActualIndexName(aliasName), aliasName));
        
        // 添加新索引别名
        request.addAliasAction(new AliasAction.Add(newIndexName, aliasName));
        
        esClient.indices().alias(request, RequestOptions.DEFAULT);
        
        log.info("索引重建完成: alias={}, newIndex={}", aliasName, newIndexName);
    }
    
    /**
     * 获取索引别名指向的实际索引名
     */
    private String getActualIndexName(String aliasName) throws IOException {
        GetAliasesRequest request = new GetAliasesRequest(aliasName);
        GetAliasesResponse response = esClient.indices().getAlias(request, RequestOptions.DEFAULT);
        return response.getAliases().keys().iterator().next();
    }
}

3.2 文档操作

java 复制代码
/**
 * ES文档服务
 */
@Service
@Slf4j
public class EsDocumentService {
    
    @Autowired
    private RestHighLevelClient esClient;
    
    private static final String PRODUCT_INDEX = "products";
    
    /**
     * 批量插入文档
     */
    public void bulkInsert(List<ProductDoc> products) throws IOException {
        BulkRequest request = new BulkRequest();
        
        for (ProductDoc product : products) {
            IndexRequest indexRequest = new IndexRequest(PRODUCT_INDEX)
                .id(product.getProductId())
                .source(JSON.toJSONString(product), XContentType.JSON);
            request.add(indexRequest);
        }
        
        BulkResponse response = esClient.bulk(request, RequestOptions.DEFAULT);
        
        if (response.hasFailures()) {
            log.error("批量插入失败: {}", response.buildFailureMessage());
            throw new EsException("批量插入失败");
        }
        
        log.info("批量插入成功: count={}", products.size());
    }
    
    /**
     * 更新文档
     */
    public void update(String productId, Map<String, Object> fields) throws IOException {
        UpdateRequest request = new UpdateRequest(PRODUCT_INDEX, productId)
            .doc(fields)
            .docAsUpsert(true);
        
        esClient.update(request, RequestOptions.DEFAULT);
    }
    
    /**
     * 删除文档
     */
    public void delete(String productId) throws IOException {
        DeleteRequest request = new DeleteRequest(PRODUCT_INDEX, productId);
        esClient.delete(request, RequestOptions.DEFAULT);
    }
    
    /**
     * 批量删除
     */
    public void bulkDelete(List<String> productIds) throws IOException {
        BulkRequest request = new BulkRequest();
        
        for (String productId : productIds) {
            DeleteRequest deleteRequest = new DeleteRequest(PRODUCT_INDEX, productId);
            request.add(deleteRequest);
        }
        
        esClient.bulk(request, RequestOptions.DEFAULT);
    }
}

四、搜索查询实战

4.1 基础搜索

java 复制代码
/**
 * ES搜索服务
 */
@Service
@Slf4j
public class EsSearchService {
    
    @Autowired
    private RestHighLevelClient esClient;
    
    private static final String PRODUCT_INDEX = "products";
    
    /**
     * 关键词搜索
     */
    public SearchResult search(String keyword, SearchQuery query) throws IOException {
        
        // 1. 构建查询条件
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        // 2. 商品名称匹配(分词)
        boolQuery.should(QueryBuilders.matchQuery("productName", keyword)
            .boost(3.0f));
        
        // 3. 品牌名匹配
        boolQuery.should(QueryBuilders.termQuery("brandName.keyword", keyword)
            .boost(2.0f));
        
        // 4. 标签匹配
        boolQuery.should(QueryBuilders.termQuery("tags", keyword));
        
        // 5. 分类路径包含
        boolQuery.should(QueryBuilders.termQuery("categoryPath", keyword));
        
        // 6. 必须是上架状态
        boolQuery.filter(QueryBuilders.termQuery("status", "ON_SALE"));
        
        // 7. 必须有库存
        boolQuery.filter(QueryBuilders.rangeQuery("stock").gt(0));
        
        // 8. 构建搜索请求
        SearchRequest request = new SearchRequest(PRODUCT_INDEX);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        sourceBuilder.query(boolQuery);
        
        // 9. 分页
        sourceBuilder.from(query.getOffset());
        sourceBuilder.size(query.getPageSize());
        
        // 10. 排序
        sourceBuilder.sort(new FieldSortBuilder("salesCount").order(SortOrder.DESC));
        sourceBuilder.sort(new FieldSortBuilder("score").order(SortOrder.DESC));
        
        // 11. 高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("productName")
            .preTags("<em>")
            .postTags("</em>");
        sourceBuilder.highlighter(highlightBuilder);
        
        // 12. 聚合
        sourceBuilder.aggregation(
            AggregationBuilders.terms("brands")
                .field("brandName.keyword")
                .size(10)
        );
        sourceBuilder.aggregation(
            AggregationBuilders.terms("categories")
                .field("categoryId")
                .size(20)
        );
        
        request.source(sourceBuilder);
        
        // 13. 执行搜索
        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        
        // 14. 处理结果
        return buildSearchResult(response, query);
    }
    
    /**
     * 构建搜索结果
     */
    private SearchResult buildSearchResult(SearchResponse response, SearchQuery query) {
        SearchResult result = new SearchResult();
        
        // 命中总数
        result.setTotal(response.getHits().getTotalHits().value);
        
        // 商品列表
        List<ProductDoc> products = new ArrayList<>();
        for (SearchHit hit : response.getHits().getHits()) {
            ProductDoc product = JSON.parseObject(hit.getSourceAsString(), ProductDoc.class);
            // 设置高亮
            if (hit.getHighlightFields().containsKey("productName")) {
                product.setHighlightName(
                    hit.getHighlightFields().get("productName").get(0));
            }
            products.add(product);
        }
        result.setProducts(products);
        
        // 聚合结果
        Map<String, List<Bucket>> aggregations = new HashMap<>();
        for (Map.Entry<String, Aggregations> entry : response.getAggregations().entrySet()) {
            StringTerms terms = entry.getValue().get("brands");
            // 处理聚合结果...
        }
        result.setAggregations(aggregations);
        
        return result;
    }
}

4.2 聚合查询

java 复制代码
/**
 * ES聚合查询
 */
@Service
@Slf4j
public class EsAggService {
    
    /**
     * 聚合统计(价格分布)
     */
    public Map<String, Long> priceHistogram(String categoryId) throws IOException {
        
        SearchRequest request = new SearchRequest(PRODUCT_INDEX);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        // 过滤分类
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.filter(QueryBuilders.termQuery("categoryId", categoryId));
        boolQuery.filter(QueryBuilders.termQuery("status", "ON_SALE"));
        sourceBuilder.query(boolQuery);
        
        // 价格区间聚合
        sourceBuilder.aggregation(
            AggregationBuilders.histogram("price_histogram")
                .field("price")
                .interval(10000)  // 每100元一个区间
                .minDocCount(1)
        );
        
        request.source(sourceBuilder);
        
        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        
        // 处理聚合结果
        Histogram histogram = response.getAggregations().get("price_histogram");
        Map<String, Long> result = new TreeMap<>();
        for (Histogram.Bucket bucket : histogram.getBuckets()) {
            result.put(String.valueOf(bucket.getKey()), bucket.getDocCount());
        }
        
        return result;
    }
    
    /**
     * 多维度聚合(品牌+分类+价格)
     */
    public void complexAggregation() throws IOException {
        
        SearchRequest request = new SearchRequest(PRODUCT_INDEX);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        // 嵌套聚合
        sourceBuilder.aggregation(
            AggregationBuilders.terms("brands")
                .field("brandName.keyword")
                .size(20)
                .subAggregation(
                    AggregationBuilders.terms("categories")
                        .field("categoryId")
                        .size(10)
                        .subAggregation(
                            AggregationBuilders.avg("avg_price")
                                .field("price")
                        )
                )
        );
        
        request.source(sourceBuilder);
        
        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        
        // 处理嵌套聚合结果
        StringTerms brands = response.getAggregations().get("brands");
        for (StringTerms.Bucket brandBucket : brands.getBuckets()) {
            String brandName = brandBucket.getKeyAsString();
            
            StringTerms categories = brandBucket.getAggregations().get("categories");
            for (StringTerms.Bucket categoryBucket : categories.getBuckets()) {
                String categoryId = categoryBucket.getKeyAsString();
                Avg avgPrice = categoryBucket.getAggregations().get("avg_price");
                
                log.info("品牌: {}, 分类: {}, 平均价格: {}", 
                    brandName, categoryId, avgPrice.getValue());
            }
        }
    }
}

五、智能搜索建议

5.1 Suggest接口

java 复制代码
/**
 * 搜索建议服务
 */
@Service
@Slf4j
public class SuggestService {
    
    @Autowired
    private RestHighLevelClient esClient;
    
    private static final String SUGGEST_INDEX = "product_suggest";
    
    /**
     * 搜索建议(Completion Suggester)
     */
    public List<String> suggest(String prefix, int size) throws IOException {
        
        SearchRequest request = new SearchRequest(SUGGEST_INDEX);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        // Completion Suggester
        CompletionSuggestionBuilder suggestion = SuggestBuilders
            .completionSuggestion("suggest")
            .prefix(prefix)
            .size(size)
            .fuzzy(new Fuzziness("AUTO"));
        
        SuggestBuilder suggestBuilder = new SuggestBuilder();
        suggestBuilder.addSuggestion("product_suggest", suggestion);
        sourceBuilder.suggest(suggestBuilder);
        
        request.source(sourceBuilder);
        
        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
        
        // 提取建议词
        List<String> suggestions = new ArrayList<>();
        Suggest.Suggestion<Suggest.Suggestion.Entry<Suggest.Suggestion.Entry.Option>> suggestionResult = 
            response.getSuggest().getSuggestion("product_suggest");
        
        for (Suggest.Suggestion.Entry<Suggest.Suggestion.Entry.Option> entry : suggestionResult) {
            for (Suggest.Suggestion.Entry.Option option : entry.getOptions()) {
                suggestions.add(option.getText().string());
            }
        }
        
        return suggestions;
    }
    
    /**
     * 搜索热词
     */
    public List<HotWord> getHotWords(int size) throws IOException {
        
        // 从Redis获取热词
        Set<String> hotWords = redisTemplate.opsForZSet()
            .reverseRange("search:hot", 0, size - 1);
        
        return hotWords.stream()
            .map(word -> {
                Double score = redisTemplate.opsForZSet()
                    .score("search:hot", word);
                return new HotWord(word, score != null ? score.intValue() : 0);
            })
            .collect(Collectors.toList());
    }
    
    /**
     * 记录搜索热词
     */
    public void recordSearchWord(String word) {
        redisTemplate.opsForZSet()
            .incrementScore("search:hot", word, 1);
        
        // 设置过期时间(每天凌晨过期)
        redisTemplate.expire("search:hot", 
            getExpireSeconds(), TimeUnit.SECONDS);
    }
    
    private long getExpireSeconds() {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime midnight = now.toLocalDate().plusDays(1).atStartOfDay();
        return Duration.between(now, midnight).getSeconds();
    }
}

5.2 搜索引导

java 复制代码
/**
 * 搜索引导服务
 */
@Service
@Slf4j
public class SearchGuideService {
    
    /**
     * 获取搜索引导结果
     */
    public SearchGuide getSearchGuide(String keyword) {
        SearchGuide guide = new SearchGuide();
        
        // 1. 热门搜索
        guide.setHotWords(suggestService.getHotWords(5));
        
        // 2. 联想建议
        if (StringUtils.isNotBlank(keyword)) {
            guide.setSuggestions(suggestService.suggest(keyword, 10));
            
            // 3. 相关分类
            guide.setCategories(getRelatedCategories(keyword));
            
            // 4. 相关品牌
            guide.setBrands(getRelatedBrands(keyword));
        }
        
        return guide;
    }
    
    /**
     * 获取相关分类
     */
    private List<Category> getRelatedCategories(String keyword) {
        // 从ES聚合中获取
        try {
            SearchRequest request = new SearchRequest(PRODUCT_INDEX);
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            
            sourceBuilder.query(QueryBuilders.matchQuery("productName", keyword));
            sourceBuilder.size(0);
            sourceBuilder.aggregation(
                AggregationBuilders.terms("categories")
                    .field("categoryId")
                    .size(5)
            );
            
            request.source(sourceBuilder);
            SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);
            
            // 处理聚合结果...
            return new ArrayList<>();
            
        } catch (IOException e) {
            log.error("获取相关分类失败", e);
            return new ArrayList<>();
        }
    }
}

六、搜索排序优化

6.1 权重排序

java 复制代码
/**
 * 搜索排序服务
 */
@Service
@Slf4j
public class SearchSortService {
    
    /**
     * 智能排序
     */
    public void applySmartSort(SearchSourceBuilder sourceBuilder, String sortType) {
        switch (sortType) {
            case "sales":
                // 销量优先
                sourceBuilder.sort("salesCount", SortOrder.DESC);
                break;
                
            case "price_asc":
                // 价格升序
                sourceBuilder.sort("price", SortOrder.ASC);
                break;
                
            case "price_desc":
                // 价格降序
                sourceBuilder.sort("price", SortOrder.DESC);
                break;
                
            case "score":
                // 评分优先
                sourceBuilder.sort("score", SortOrder.DESC);
                break;
                
            case "newest":
                // 新品优先
                sourceBuilder.sort("createTime", SortOrder.DESC);
                break;
                
            case "relevance":
                // 相关性优先(默认)
                // ES的_score已经代表了相关性
                break;
                
            default:
                // 综合排序(销量+评分+时间)
                sourceBuilder.sort(new ScriptSortBuilder(
                    "doc['salesCount'].value * 0.3 + " +
                    "doc['score'].value * 100 * 0.5 + " +
                    "(now - doc['createTime'].value.getMillis()) / 86400000 * 0.2",
                    ScriptSortBuilder.ScriptSortType.NUMBER
                ).order(SortOrder.DESC));
        }
    }
    
    /**
     * 权重查询
     */
    public void applyWeightQuery(BoolQueryBuilder boolQuery, String keyword) {
        // 商品名称匹配 - 权重最高
        boolQuery.should(QueryBuilders.matchQuery("productName", keyword)
            .boost(10f));
        
        // 标签匹配 - 权重高
        boolQuery.should(QueryBuilders.termQuery("tags", keyword)
            .boost(5f));
        
        // 描述匹配 - 权重中
        boolQuery.should(QueryBuilders.matchQuery("description", keyword)
            .boost(2f));
        
        // 品牌匹配 - 权重较低
        boolQuery.should(QueryBuilders.termQuery("brandName.keyword", keyword)
            .boost(1f));
    }
}

6.2 人群排序

java 复制代码
/**
 * 个性化排序服务
 */
@Service
@Slf4j
public class PersonalizedSortService {
    
    @Autowired
    private UserProfileService userProfileService;
    
    /**
     * 个性化排序
     */
    public void applyPersonalizedSort(SearchSourceBuilder sourceBuilder, 
                                      Long userId, String keyword) {
        if (userId == null) {
            return;  // 未登录用户不做个性化
        }
        
        // 获取用户画像
        UserProfile profile = userProfileService.getUserProfile(userId);
        
        // 获取用户偏好类目
        List<String> preferredCategories = profile.getPreferredCategories();
        
        if (CollectionUtils.isNotEmpty(preferredCategories)) {
            // 提升用户偏好类目的权重
            FunctionScoreQueryBuilder functionScoreQuery = 
                QueryBuilders.functionScoreQuery()
                    .scoreMode(FunctionScoreQuery.ScoreMode.MULTIPLY)
                    .boostMode(FunctionScoreQuery.BoostMode.REPLACE);
            
            // 添加类目偏好函数
            for (String categoryId : preferredCategories) {
                FunctionScoreQueryBuilder.FilterFunctionBuilder filter = 
                    new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        QueryBuilders.termQuery("categoryId", categoryId),
                        new WeightBuilder().setWeight(2.0f)
                    );
                functionScoreQuery.add(filter);
            }
        }
        
        // 价格偏好(用户常买的价格区间)
        List<PriceRange> priceRanges = profile.getPriceRanges();
        if (CollectionUtils.isNotEmpty(priceRanges)) {
            // 提升用户常买价格区间的权重
            for (PriceRange range : priceRanges) {
                // 添加价格范围偏好...
            }
        }
    }
}

七、搜索性能优化

7.1 查询缓存

java 复制代码
/**
 * 查询缓存服务
 */
@Service
@Slf4j
public class SearchCacheService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String SEARCH_CACHE_PREFIX = "search:cache:";
    private static final Duration CACHE_EXPIRE = Duration.ofMinutes(10);
    
    /**
     * 缓存查询结果
     */
    public SearchResult getCachedResult(String cacheKey) {
        String key = SEARCH_CACHE_PREFIX + cacheKey;
        String cached = redisTemplate.opsForValue().get(key);
        
        if (cached != null) {
            log.debug("命中搜索缓存: key={}", cacheKey);
            return JSON.parseObject(cached, SearchResult.class);
        }
        
        return null;
    }
    
    /**
     * 缓存查询结果
     */
    public void cacheResult(String cacheKey, SearchResult result) {
        String key = SEARCH_CACHE_PREFIX + cacheKey;
        redisTemplate.opsForValue().set(key, JSON.toJSONString(result), CACHE_EXPIRE);
        
        log.debug("缓存搜索结果: key={}, total={}", cacheKey, result.getTotal());
    }
    
    /**
     * 生成缓存Key
     */
    public String generateCacheKey(String keyword, SearchQuery query) {
        return keyword + ":" + query.getPage() + ":" + query.getPageSize() + 
               ":" + query.getSortType() + ":" + query.getCategoryId();
    }
    
    /**
     * 搜索时使用缓存
     */
    public SearchResult searchWithCache(String keyword, SearchQuery query) {
        String cacheKey = generateCacheKey(keyword, query);
        
        // 先查缓存
        SearchResult cached = getCachedResult(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // 查ES
        SearchResult result = esSearchService.search(keyword, query);
        
        // 缓存结果
        if (result.getTotal() > 0) {
            cacheResult(cacheKey, result);
        }
        
        return result;
    }
}

7.2 索引优化

java 复制代码
/**
 * ES性能优化配置
 */
@Configuration
public class EsOptimizeConfig {
    
    /**
     * 优化查询速度
     */
    @Bean
    public SettingsBuilder settings() {
        return Settings.builder()
            // 分片数(根据数据量调整)
            .put("number_of_shards", 3)
            // 副本数
            .put("number_of_replicas", 1)
            // 刷新间隔(写多读少可以调大)
            .put("refresh_interval", "5s")
            // 合并调度
            .put("indices.memory.index_buffer_size", "20%")
            // 最大并发连接数
            .put("http.max_initial_line_length", "4k")
            .put("http.max_header_size", "8k")
            // 断路器
            .put("indices.breaker.total.use_memory", "0.7")
            // 查询结果缓存
            .put("indices.requests.cache.enable", "true");
    }
}

八、踩坑实录

坑1:分词器选错

搜索"手机"搜不到"苹果手机",因为分词器把"苹果"和"手机"分成两个词了。

解决:使用ik_max_word分词器,并配置同义词词典。

坑2:聚合结果不准确

分页后聚合结果和预期不符,因为默认只统计当前分片的数据。

解决 :设置shard_size参数,确保聚合准确性。

坑3:深度分页问题

翻到第1000页时特别慢,因为ES默认只支持10000条数据。

解决:使用search_after或scroll API,或者限制最大翻页数。

坑4:数据同步延迟

数据库更新后,ES要等很久才能搜到。

解决:使用Canal实时同步,或者调小refresh_interval。

坑5:搜索结果和数据库不一致

搜索有库存,实际没库存;搜索有货,实际没货。

解决:定时对比ES和数据库,不一致告警并修复。


九、总结

搜索是电商的核心:

  • 索引设计:合理的mapping和分词器
  • 查询优化:BoolQuery + FunctionScore
  • 性能优化:缓存 + 分片策略
  • 智能建议:Completion Suggester + 热词

最佳实践:

  1. 搜索结果要保证和数据库一致
  2. 做好分词器和同义词配置
  3. 核心接口加缓存
  4. 监控搜索性能和体验

血的教训:

搜索体验直接影响转化率。搜索慢1秒,转化率可能下降10%。搜索结果不准确,用户就直接跑了。

思考题: 你的搜索系统有没有做同义词配置?效果怎么样?


个人观点,仅供参考

相关推荐
java_cj1 小时前
数据库范式化设计与性能优化全攻略
数据库·后端·性能优化·架构·开源
程序大视界1 小时前
AI多模态大模型技术全景(2026):从“拼接“到“原生统一“,一文读懂底层架构与主流方案
人工智能·架构·多模态
TDengine (老段)2 小时前
TDengine Commit 与 Flush 机制 — 从内存到磁盘的数据落盘全流程
大数据·数据库·物联网·架构·时序数据库·iot·tdengine
GISer_Jing2 小时前
Claude Code多Agent架构深度剖析
前端·人工智能·架构·自动化
Agent手记2 小时前
医药代表拜访计划能否通过AI自动生成优化?2026Agent自动化实战解析
运维·人工智能·ai·自动化
KaMeidebaby2 小时前
卡梅德生物技术快报|Pull Down 实验在 lncRNA - 蛋白互作机制研究中的应用实例解析
大数据·前端·架构·spark·新浪微博
ID_180079054732 小时前
(淘宝 / 京东)商品评论 API 接口:技术实战案例与架构分析
服务器·数据库·架构
爱莉希雅&&&2 小时前
Zabbix监控初步搭建
linux·运维·数据库·mysql·zabbix
JackSparrow4142 小时前
使用Ansible批量管理+更新产品环境服务器配置
运维·服务器·ci/cd·kubernetes·自动化·ansible·sre