一、整体检索方案选型
1. 为什么不能只用 MySQL 检索
- 多规格筛选:颜色、尺寸、配置组合查询,MySQL 字符串匹配 / 联表查询极慢;
- 模糊搜索:商品标题、详情、参数全文匹配,MySQL like % xx% 不走索引;
- 多条件组合筛选:分类 + 品牌 + 价格区间 + 多个规格值 + 属性参数混合过滤;
- 排序加权:销量、价格、匹配度、新品综合排序;
- 分页、高亮、同义词、拼音搜索、分词能力数据库原生不支持。
标准架构:MySQL 存业务源数据,Elasticsearch 做商品检索引擎
MySQL 负责增删改、库存、订单、事务;ES 承接所有前台商品列表、搜索、筛选。
2. 数据同步两种主流方案
方案 1:Canal binlog 实时同步(生产最常用)
- MySQL 开启 binlog=ROW;
- Canal 监听 MySQL binlog,捕获 spu/sku/spec/attr 增、改、删事件;
- Canal 把变更数据推送至 MQ (RocketMQ/Kafka);
- 消费服务拉取消息,组装完整商品文档,写入 / 更新 ES 索引;
- 延迟极低(几十 ms 级),准实时检索。
方案 2:定时全量 + 增量补偿(小型商城)
- 凌晨全量一次性把所有 SPU、关联 SKU、规格、属性查出来批量灌入 ES;
- 业务更新时,代码里同步调用 ES 更新接口(双写);
- 定时增量任务每 5 分钟比对更新时间戳,补发遗漏数据;
缺点:有代码侵入,容易出现 MySQL 与 ES 数据不一致。
二、ES 索引文档结构设计(扁平化大文档,一次查询全带出)
不拆多个 ES 索引,一个 spu 对应一个 ES 文档,把所有关联数据平铺嵌入 JSON,避免 ES 多表 Join(ES 不擅长关联)。
索引名:goods_spu_index
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| json { "spuId": 10001, "spuName": "iPhone 15", "brandId": 101, "brandName": "Apple", "categoryId": 302, "categoryName": "手机", "categoryPath": "数码>手机>苹果手机", "coverImg": "xxx.jpg", "marketPrice": 5999.00, "minSalePrice": 4999.00, "totalSales": 12000, "totalStock": 500, "status": 1, "createTime": "2026-01-01 10:00:00", // 1. 公共属性数组(用于参数筛选) "attrList": {"attrName":"适配车型","attrValue":"享道E7"}, {"attrName":"产地","attrValue":"中国"} , // 2. 规格维度定义(页面渲染规格选择面板) "specKeyList": {"specKeyId":1,"keyName":"内存","values":\["128G","256G"}, {"specKeyId":2,"keyName":"颜色","values":"黑色","白色"} ], // 3. 全部SKU平铺数组(价格、库存、规格组合) "skuList": { "skuId": 20001, "price": 4999, "stock": 120, "specCombine": \["128G","黑色", "specValueIds": "1,3", "skuImg": "black.jpg" }, { "skuId": 20002, "price": 5499, "stock": 80, "specCombine": "256G","黑色", "specValueIds": "2,3", "skuImg": "black.jpg" } ], // 扁平化规格值集合,用于快速筛选过滤 "allSpecValues": "128G","256G","黑色","白色", // 用于分词搜索的大文本字段 "searchContent": "iPhone 15 Apple 苹果 手机 128G 256G 黑色 白色 国行 全网通" } |
字段类型关键配置
- spuName、searchContent:text 类型,ik 分词器,支持模糊全文检索;
- brandId、categoryId、status:keyword 精确匹配;
- minSalePrice、marketPrice:double,区间范围筛选;
- allSpecValues、specCombine:keyword,规格多选过滤;
- attrList.attrName/attrValue:嵌套 nested 类型,用于参数精准筛选;
- skuList:nested 嵌套结构,可过滤有库存、价格区间 SKU。
三、常见商城检索场景 ES DSL 实现
场景 1:关键词模糊搜索(首页搜索框)
输入:苹果手机黑色
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| json { "query": { "multi_match": { "query": "苹果手机黑色", "fields": "spuName\^3","searchContent\^1" } }, "sort": {"_score":"desc"},{"totalSales":"desc"} } |
场景 2:分类 + 品牌 + 价格区间基础筛选
分类 = 手机,品牌 = Apple,售价 4000~6000
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| json { "query": { "bool": { "filter": {"term":{"categoryId":302}}, {"term":{"brandId":101}}, {"range":{"minSalePrice":{"gte":4000,"lte":6000}}}, {"term":{"status":1}} } } } |
场景 3:规格多选筛选(核心难点)
筛选:内存 = 256G + 颜色 = 黑色
利用allSpecValues多关键词同时命中文档:
|-----------------------------------------------------------------------------------------------|
| json { "query": { "bool": { "filter": {"terms":{"allSpecValues":\["256G","黑色"}} ] } } } |
|--------------------------------------------------------------|
| 精准匹配 SKU 存在该组合:如需严格校验组合存在,改用 nested 查 skuList 内 specCombine。 |
场景 4:公共属性筛选
筛选:适配车型 = 享道 E7
attrList 是 nested 嵌套,必须嵌套查询:
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| json { "query": { "nested": { "path": "attrList", "query": { "bool": { "filter": {"term":{"attrList.attrName":"适配车型"}}, {"term":{"attrList.attrValue":"享道E7"}} } } } } } |
场景 5:只展示有库存的商品
过滤 skuList 里任意 sku 库存 > 0:
|------------------------------------------------------------------------------------------------------|
| json { "query": { "nested": { "path": "skuList", "query": {"range":{"skuList.stock":{"gt":0}}} } } } |
四、同步更新逻辑(增 / 改 / 删)
- 新增 SPU
MySQL 插入 spu、sku、spec、attr → Canal 捕获 binlog → 组装完整大文档 → ES index 创建文档。 - 修改 SPU 基础信息(标题、图片、详情)
局部更新 ES 对应字段,使用_update部分更新,不用全量覆盖。 - 修改 SKU 价格 / 库存
只更新 ES 内 skuList 数组对应 sku 节点;库存高频变动场景:可 MQ 高频小粒度更新,不会重建整个文档。 - 新增 / 修改规格、属性
重新拉取该 SPU 全部关联数据,全量覆盖更新整条 ES 文档(单 SPU 数据量很小,性能无压力)。 - 商品下架删除
逻辑删除优先:status 置 0,ES 过滤直接屏蔽;物理删除执行 ES delete。
五、大小两套架构适配
1)小型内部商城(轻量)
- 不用 Canal,业务代码双写 MySQL+ES;
- 每日一次全量重刷兜底一致性;
- ES 单节点即可,文档量几万以内毫无压力。
2)C 端公域大电商(高并发百万商品)
- Canal + Kafka 解耦同步,削峰填谷;
- ES 集群 3 节点以上,分片合理拆分;
- 冷热分离:新品热分片、滞销老商品冷分片;
- 增加搜索限流、结果缓存(Redis 缓存热门搜索页);
- 定时一致性校验任务:对比 MySQL 统计值与 ES 数量,修复差异数据。
六、避坑关键点
- 禁止 ES 做事务、库存扣减
库存、下单、价格锁定必须以 MySQL 为准,ES 只做展示检索;库存变动 ES 仅做同步展示用。 - 文档扁平化,杜绝多索引关联查询
ES Join 性能极差,把所有子数据内嵌到 SPU 主文档是行业标准写法。 - nested 嵌套不宜过深
skuList、attrList 两层嵌套足够,层级越多查询性能越低。 - 分词器统一用 IK-Analyzer
中文商品名称、搜索词必备,自带热词可扩展品牌、车型自定义词库。 - 不要实时依赖 ES 做精确库存判断
用户下单前页面展示库存看 ES,下单扣减锁库存严格走 MySQL。