Elasticsearch 从 0 到 1(七):电商搜索架构实践
前六篇分别讲了:
最后这一篇,把它们串成一个完整的电商商品搜索方案。
商品搜索要解决什么问题
一个商品搜索页通常不只是输入关键词。
它可能包含:
- 关键词搜索;
- 分类筛选;
- 品牌筛选;
- 价格区间;
- 上架状态;
- 销量排序;
- 价格排序;
- 最新排序;
- 标题高亮;
- 品牌聚合;
- 分类聚合;
- 分页;
- 搜索词纠错和推荐。
刚开始做,不要一口吃成复杂搜索系统。
第一版可以先做:
关键词 + 分类 + 品牌 + 价格 + 排序 + 分页 + 高亮整体架构
一个常见架构:
用户搜索 ↓商品搜索接口 ↓Elasticsearch ↓返回商品列表
MySQL 商品数据 ↓数据同步任务 / MQ / Binlog ↓Elasticsearch 商品索引MySQL 保存真实商品数据。
ES 保存搜索文档。
搜索接口只查 ES。
下单和库存判断仍然查 MySQL 或库存系统。
商品搜索文档设计
ES 文档要围绕搜索页设计。
示例:
{ "productId": 1001, "title": "无线蓝牙耳机", "subtitle": "低延迟,长续航", "brand": "SoundGo", "categoryId": 200, "categoryName": "数码耳机", "price": 199, "sales": 3000, "stock": 80, "status": "ON_SALE", "coverUrl": "https://example.com/cover.webp", "createdAt": "2026-07-02T10:00:00"}文档设计原则:
- 搜索页要展示的字段,尽量直接放进去;
- 筛选字段用
keyword或数值; - 排序字段用数值或日期;
- 标题和描述用
text; - 不要在搜索接口里再查一堆 MySQL 补字段。
搜索要快,文档就要适当冗余。
商品索引 Mapping
{ "mappings": { "properties": { "productId": { "type": "long" }, "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "subtitle": { "type": "text" }, "brand": { "type": "keyword" }, "categoryId": { "type": "long" }, "categoryName": { "type": "keyword" }, "price": { "type": "integer" }, "sales": { "type": "integer" }, "stock": { "type": "integer" }, "status": { "type": "keyword" }, "coverUrl": { "type": "keyword", "index": false }, "createdAt": { "type": "date" } } }}这里 coverUrl 只是展示,不需要搜索,所以可以关闭索引。
搜索请求设计
public class ProductSearchRequest {
private String keyword; private Long categoryId; private String brand; private Integer minPrice; private Integer maxPrice; private String sort; private Integer pageNo = 1; private Integer pageSize = 20;}排序建议做白名单:
default:默认相关性sales_desc:销量倒序price_asc:价格升序price_desc:价格倒序newest:最新不要让前端直接传 ES 字段名。
搜索 DSL 组合
{ "from": 0, "size": 20, "query": { "bool": { "must": [ { "match": { "title": "蓝牙耳机" } } ], "filter": [ { "term": { "status": "ON_SALE" } }, { "term": { "categoryId": 200 } }, { "term": { "brand": "SoundGo" } }, { "range": { "price": { "gte": 100, "lte": 300 } } } ] } }, "sort": [ { "sales": "desc" } ], "highlight": { "fields": { "title": {} } }}这就是商品搜索最核心的一段。
关键词影响相关性。
筛选条件不影响评分。
排序根据用户选择调整。
高亮用于前端展示。
筛选项怎么做
搜索页左侧常见:
品牌分类价格区间这些可以用聚合生成。
品牌聚合:
{ "size": 0, "aggs": { "brands": { "terms": { "field": "brand", "size": 20 } } }}价格区间聚合:
{ "aggs": { "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 100 }, { "from": 100, "to": 300 }, { "from": 300, "to": 500 }, { "from": 500 } ] } } }}聚合可以和查询条件一起用,但要注意性能,不要无脑加很多复杂聚合。
数据同步流程
推荐流程:
商品新增 / 修改 / 上下架 ↓更新 MySQL ↓写本地消息表或发送 MQ ↓消费者查询 MySQL 最新数据 ↓构建 ProductDocument ↓写入 ES为什么消费者要查 MySQL 最新数据?
因为消息里带的字段可能不完整,也可能已经过期。
消息只需要告诉你:
哪个商品变了发生了什么变更最终文档最好从 MySQL 当前状态重新构建。
上下架和库存
商品下架后,搜索接口必须过滤:
{ "term": { "status": "ON_SALE" } }库存字段可以展示,但不要把 ES 库存作为下单扣减依据。
因为 ES 和 MySQL 通常是最终一致,库存变化又很频繁。
下单时仍然要走真实库存系统或数据库条件更新。
搜索体验优化
基础搜索跑通后,可以逐步优化体验:
- 搜索高亮;
- 搜索建议;
- 热门搜索词;
- 同义词;
- 拼音搜索;
- 品牌词识别;
- 类目权重;
- 销量和相关性综合排序;
- 无结果时推荐热门商品。
不要第一版就全部做。
第一版目标应该是:
能搜到能筛选能排序能分页能高亮数据能同步出问题能排查排查问题看哪里
商品搜索出问题,一般按这个顺序排查:
- MySQL 里商品是否存在;
- ES 里文档是否存在;
- 文档字段是否正确;
- Mapping 类型是否正确;
- 查询 DSL 是否正确;
- 分词结果是否符合预期;
- 是否被 status、category、brand、price 过滤掉;
- 是否排序导致结果靠后;
- 同步消息是否失败;
- ES 查询是否超时或慢查询。
很多“搜不到”的问题,不是 ES 坏了,而是 Mapping、分词、过滤条件或数据同步出了问题。
生产注意事项
上线前至少确认:
- ES 不是核心交易数据源;
- 搜索接口限制 pageSize;
- 深分页有限制;
- 有慢查询日志;
- 有同步失败重试;
- 有商品重建索引任务;
- 有 ES 集群监控;
- Mapping 有版本管理;
- 索引重建有别名切换方案;
- 下架商品不会被搜到;
- ES 故障时有降级方案。
降级方案可以是:
- 返回热门商品;
- 只查 MySQL 简单列表;
- 提示搜索服务繁忙;
- 关闭复杂聚合。
总结
电商搜索不是只写一条 match 查询。
完整链路包括:
- 商品文档设计;
- Mapping 设计;
- DSL 查询组合;
- Java 接口封装;
- 高亮和分页;
- 聚合筛选;
- MySQL 到 ES 的数据同步;
- 性能优化;
- 监控和故障降级。
ES 的定位要始终清楚:
MySQL 管真实业务数据,ES 管搜索体验。
只要这个边界不乱,ES 就会成为搜索能力的放大器,而不是新的数据一致性麻烦源。
参考
If this article helped you, please share it with others!
Some information may be outdated






