一、搜索慢如牛让我流失了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秒,转化率可能下降10%。搜索结果不准确,用户就直接跑了。
思考题: 你的搜索系统有没有做同义词配置?效果怎么样?
个人观点,仅供参考