在电商、商品搜索或内容平台中,搜索结果去重与合并展示(如将多个 SKU 合并为一个 SPU 展示,或将多个套餐合并为一个商品)是提升用户体验的关键功能。
例如:
用户搜索“iPhone 15”,不希望看到 10 个不同的 SKU(不同颜色、容量)重复出现,而是希望看到 1 个商品卡片,下方展示可选规格。
本文将为你设计一套完整的 Elasticsearch 搜索结果去重与合并(如套餐合并)的解决方案,涵盖数据建模、查询策略、聚合逻辑、前端渲染等环节。
一、核心需求
| 需求 | 说明 |
|---|---|
| 结果去重 | 相同商品(SPU)只展示一次 |
| 规格聚合 | 展示该商品下的价格区间、库存状态、图片等 |
| 最优排序 | 按销量最高、价格最低等策略选择主展示 SKU |
| 支持筛选 | 用户选择“仅显示有货”时,过滤无库存 SPU |
| 高亮与相关性 | 保留关键词高亮,不影响搜索评分 |
| 性能可接受 | 大数据量下响应时间 < 500ms |
二、去重方案选型对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
collapse(折叠) | ES 原生支持,简单易用 | 不聚合子文档信息,功能有限 | 基础去重 |
terms + top_hits 聚合 | 灵活,可自定义聚合逻辑 | 查询复杂,性能较低 | 需要丰富子信息 |
| 应用层合并 | 完全可控,逻辑灵活 | 增加网络开销,实现复杂 | 多源数据合并 |
| 双索引(SPU + SKU) | 职责分离,性能好 | 维护两套数据同步 | 大型电商平台 |
✅ 推荐组合方案:
terms + top_hits聚合(搜索页) +collapse(列表页) + 双索引架构(最优)
三、方案一:使用 collapse 实现基础去重(推荐用于列表页)
1. 数据准备
确保 SKU 文档中包含 spu_id 字段:
{
"sku_id": "sku_1001",
"spu_id": "spu_2001",
"title": "iPhone 15 Pro 256GB 白色",
"price": 899900,
"stock_status": "in_stock",
"image": "https://...",
"sales_count": 1500
}
2. 使用 collapse 按 spu_id 折叠
GET /sku-catalog/_search
{
"query": {
"match": { "title": "iPhone 15" }
},
"collapse": {
"field": "spu_id",
"inner_hits": {
"name": "top_sku",
"size": 1,
"sort": [
{ "sales_count": "desc" } // 每个 SPU 展示销量最高的 SKU
],
"_source": ["sku_id", "price", "image", "stock_status"]
}
},
"sort": [
{ "sales_count": "desc" } // 整体按销量排序
],
"size": 20
}
3. 返回结果示例
"hits": {
"hits": [
{
"id": "spu_2001",
"fields": { "spu_id": "spu_2001" },
"inner_hits": {
"top_sku": {
"hits": {
"hits": [
{
"_source": {
"sku_id": "sku_1001",
"price": 899900,
"image": "https://...",
"stock_status": "in_stock"
}
}
]
}
}
}
}
]
}
✅ 优点
- 原生支持,语法简洁。
- 支持
inner_hits展示代表 SKU。 - 可结合
sort控制排序。
❌ 局限
- 无法展示所有规格(如颜色、容量列表)。
- 不支持复杂聚合(如价格区间)。
四、方案二:使用 terms + top_hits 聚合(推荐用于搜索页)
1. 查询:按 spu_id 分组,聚合规格信息
GET /sku-catalog/_search
{
"size": 0,
"query": {
"match": { "title": "iPhone 15" }
},
"aggs": {
"products": {
"terms": {
"field": "spu_id",
"size": 20,
"order": { "max_sales": "desc" }
},
"aggs": {
"max_sales": {
"max": { "field": "sales_count" } // 用于排序
},
"price_stats": {
"stats": { "field": "price" } // 价格区间
},
"in_stock_count": {
"filter": { "term": { "stock_status": "in_stock" } }
},
"top_sku": {
"top_hits": {
"size": 1,
"sort": [{ "sales_count": "desc" }],
"_source": ["image", "price", "sku_id"]
}
},
"spec_agg": {
"nested": { "path": "specifications" },
"aggs": {
"colors": {
"filter": { "term": { "specifications.name": "颜色" } },
"aggs": {
"values": { "terms": { "field": "specifications.value" } }
}
},
"capacities": {
"filter": { "term": { "specifications.name": "容量" } },
"aggs": {
"values": { "terms": { "field": "specifications.value" } }
}
}
}
}
}
}
}
}
2. 应用层处理聚合结果
{
"key": "spu_2001",
"doc_count": 3,
"price_stats": { "min": 899900, "max": 999900 },
"in_stock_count": { "doc_count": 2 },
"top_sku": {
"hits": {
"hits": [ { "_source": { "image": "...", "price": 899900 } } ]
}
},
"spec_agg": {
"colors": { "values": { "buckets": [ { "key": "白色", "doc_count": 2 }, { "key": "黑色", "doc_count": 1 } ] } },
"capacities": { "buckets": [ { "key": "256GB" }, { "key": "512GB" } ] }
}
}
✅ 优点
- 可聚合价格区间、库存数量、规格列表。
- 支持复杂排序与筛选。
- 适合构建完整的商品卡片。
❌ 缺点
- 性能开销大,尤其在
nested字段上。 - 返回的是
aggregations,不是hits,需应用层拼装。
五、方案三:双索引架构(SPU + SKU)—— 最佳实践
架构设计
两个索引:
- sku-catalog-*:存储所有 SKU,用于库存、价格、规格查询
- spu-catalog-*:存储 SPU 汇总信息,用于搜索展示
1. SPU 索引 Mapping 示例
PUT /spu-catalog-2024-10
{
"mappings": {
"properties": {
"spu_id": { "type": "keyword" },
"title": { "type": "text", "analyzer": "ik_max_word" },
"brand": { "type": "keyword" },
"category_path": { "type": "keyword" },
"price_min": { "type": "scaled_float", "scaling_factor": 100 },
"price_max": { "type": "scaled_float", "scaling_factor": 100 },
"stock_status": { "type": "keyword" }, // in_stock, out_of_stock
"sales_count": { "type": "long" },
"image": { "type": "keyword" }, // 主图
"spec_summary": { // 规格摘要
"type": "text",
"fields": {
"keyword": { "type": "keyword" } // "颜色:白/黑, 容量:256/512"
}
},
"sku_count": { "type": "integer" } // 可选 SKU 数量
}
}
}
2. 数据同步逻辑
- 当 SKU 变更时(新增、价格、库存),触发更新 SPU 汇总:
- 重新计算
price_min/max - 更新
stock_status(任一有货则in_stock) - 汇总
spec_summary - 选择
sales_count最高的 SKU 作为主图
- 重新计算
可通过 Kafka 事件驱动自动更新。
3. 搜索查询(直接查 SPU)
GET /spu-read/_search
{
"query": {
"bool": {
"must": [ { "match": { "title": "iPhone 15" } } ],
"filter": [ { "term": { "stock_status": "in_stock" } } ]
}
},
"sort": [ { "sales_count": "desc" } ],
"highlight": { "fields": { "title": {} } }
}
✅ 优势
- 查询性能极高(无聚合、无折叠)。
- 易于排序、过滤、高亮。
- 支持分页、滚动。
❌ 缺点
- 需维护两套数据同步。
- 存在短暂延迟(最终一致性)。
六、去重与合并的前端渲染建议
| 字段 | 渲染方式 |
|---|---|
| 主标题 | SPU 标题 + 高亮 |
| 价格 | ¥8999 ~ ¥9999 或 “起售价 ¥8999” |
| 库存状态 | “有货” / “仅剩 X 件” / “无货” |
| 规格标签 | “颜色:白、黑” “容量:256GB、512GB” |
| 主图 | 销量最高的 SKU 图片 |
| 角标 | “爆款”、“新品”、“限时折扣” |
七、性能优化建议
| 场景 | 优化方案 |
|---|---|
collapse 性能 | 避免 nested 字段参与排序 |
top_hits 性能 | 减少 size,仅取必要字段 |
| 双索引延迟 | 使用 refresh=wait_for 保证强一致性(关键页面) |
| 内存占用 | 关闭不必要的字段 doc_values 或 index |
八、总结:三种方案对比与选型建议
| 方案 | 适用场景 | 推荐指数 |
|---|---|---|
collapse | 列表页、简单去重 | ⭐⭐⭐⭐ |
terms + top_hits | 搜索页、需丰富聚合信息 | ⭐⭐⭐⭐ |
| 双索引(SPU + SKU) | 大型电商、高性能要求 | ⭐⭐⭐⭐⭐ |
✅ 推荐架构:
- 搜索页 → 查询
spu-catalog-*(双索引)- 商品详情页 → 查询
sku-catalog-*(具体规格)- 后台管理 → 同时操作两个索引
九、扩展场景
| 场景 | 实现方式 |
|---|---|
| 套餐合并 | 将“手机+耳机”作为一个虚拟 SPU,单独建索引 |
| 品牌聚合 | 搜索“苹果”时,合并 iPhone、Mac、iPad |
| 搜索去重 + 展开 | 先折叠展示,点击后展开所有 SKU |
| 个性化去重 | 按用户偏好(如常买品牌)调整排序 |
1102

被折叠的 条评论
为什么被折叠?



