ElasticSearch:商品SKU+SPU实现join查询,设计及优化

发布于:2025-07-20 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、SPU+SKU

1、商品SPU和SKU

每个店铺中会包含多个商品,每个商品会有多个规格。

红框上面的部分是商品的SPU(Standard Product Unit 标准产品单位),SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。这里会选择对应店铺的名称,填入商品编号、商品名称、上架状态、定时上架时间、商品标签、商品图片等信息。

下面红色的框体是商品的SKU(stock keeping unit 库存量单位),SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位,SKU是物理上不可分割的最小存货单元。包括商品颜色、商品存储容量、商品价格、商品剩余数量。

在这里插入图片描述

2、SPU和SKU的关系

如图所示,一个店铺会对应多个商品的SPU。同时一个商品的SPU会包含多个商品SKU,以手机为例,一款iPhone12手机就是SPU,如果消费者需要购买该手机的时候,还需要选择手机颜色、内存容量,在商家这边还要查看商品的库存是否足够。因此,一个商品SPU会对
应多个商品SKU的描述。
在这里插入图片描述
如果我们建立数据库表的时候会将店铺、商品SPU和商品SKU建立三张表,通过主外键的关系关联。
如果mysql的数据同步到ES中,用户进行搜索的时候,应该怎么实现呢?

在 Elasticsearch(ES)中实现关联查询(join)的方式与传统关系型数据库不同,因为 ES 是分布式搜索引擎,其设计初衷并非处理复杂关联。以下是几种常见的 ES 关联实现方法:

3、实现SPU+SKU父子嵌套查询

1. 嵌套对象(Nested Objects)

场景:SPU 与 SKU 强关联,查询时需同时检索。
示例:一款手机(SPU)有多个颜色和配置(SKU)。

映射与数据

PUT /products
{
  "mappings": {
    "properties": {
      "spu_id": {"type": "keyword"},
      "spu_name": {"type": "text"},
      "brand": {"type": "keyword"},
      "skus": {
        "type": "nested",  // 嵌套类型
        "properties": {
          "sku_id": {"type": "keyword"},
          "color": {"type": "keyword"},
          "size": {"type": "keyword"},
          "price": {"type": "double"},
          "stock": {"type": "integer"}
        }
      }
    }
  }
}

PUT /products/_doc/1
{
  "spu_id": "SPU001",
  "spu_name": "苹果 iPhone 14",
  "brand": "Apple",
  "skus": [
    {
      "sku_id": "SKU001",
      "color": "黑色",
      "size": "128GB",
      "price": 5999.00,
      "stock": 100
    },
    {
      "sku_id": "SKU002",
      "color": "蓝色",
      "size": "256GB",
      "price": 6799.00,
      "stock": 50
    }
  ]
}

查询示例

  • 需求:查找所有品牌为 Apple 且有蓝色 256GB 的手机。
  • 实现
{
  "query": {
    "bool": {
      "must": [
        {"term": {"brand": "Apple"}},
        {
          "nested": {  // 嵌套查询
            "path": "skus",
            "query": {
              "bool": {
                "must": [
                  {"term": {"skus.color": "蓝色"}},
                  {"term": {"skus.size": "256GB"}}
                ]
              }
            }
          }
        }
      ]
    }
  }
}

2. 父子关系(Parent-Child)

场景:SPU 与 SKU 生命周期独立,需灵活管理(如动态添加 SKU)。
示例:一款手机(SPU)的 SKU 库存需实时更新。

映射与数据

PUT /products
{
  "mappings": {
    "properties": {
      "join_field": {
        "type": "join",
        "relations": {
          "spu": "sku"  // 定义父子关系
        }
      }
    }
  }
}

// 索引 SPU(父文档)
PUT /products/_doc/SPU001
{
  "spu_id": "SPU001",
  "name": "苹果 iPhone 14",
  "brand": "Apple",
  "join_field": {"name": "spu"}
}

// 索引 SKU(子文档)
PUT /products/_doc/SKU001?routing=SPU001  // 必须使用父 ID 作为路由
{
  "sku_id": "SKU001",
  "color": "黑色",
  "size": "128GB",
  "price": 5999.00,
  "stock": 100,
  "join_field": {"name": "sku", "parent": "SPU001"}
}

PUT /products/_doc/SKU002?routing=SPU001
{
  "sku_id": "SKU002",
  "color": "蓝色",
  "size": "256GB",
  "price": 6799.00,
  "stock": 50,
  "join_field": {"name": "sku", "parent": "SPU001"}
}

查询示例

  • 需求:查找价格低于 6000 的 SKU 所属的 SPU 信息。
  • 实现
{
  "query": {
    "has_child": {
      "type": "sku",
      "query": {"range": {"price": {"lt": 6000}}},
      "inner_hits": {}  // 返回匹配的子文档
    }
  }
}

3. 应用层关联(Application-Side Join)(推荐)

场景:SPU 和 SKU 存储在不同索引,需跨索引关联。
示例:SPU 数据在 spu_index,SKU 数据在 sku_index

数据结构

// SPU 索引
PUT /spu_index/_doc/SPU001
{
  "spu_id": "SPU001",
  "name": "苹果 iPhone 14",
  "brand": "Apple",
  "category": "手机",
  "description": "2023年新款智能手机..."
}

// SKU 索引
PUT /sku_index/_doc/SKU001
{
  "sku_id": "SKU001",
  "spu_id": "SPU001",  // 关联字段
  "color": "黑色",
  "size": "128GB",
  "price": 5999.00,
  "stock": 100,
  "sales": 2000
}

查询流程(Python)

from elasticsearch import Elasticsearch

es = Elasticsearch()

# 1. 查询手机分类下的所有 SPU
spu_result = es.search(
    index="spu_index",
    body={
        "query": {"term": {"category": "手机"}},
        "_source": ["spu_id", "name", "brand"]
    },
    size=100
)

# 2. 提取 SPU IDs
spu_ids = [hit["_source"]["spu_id"] for hit in spu_result["hits"]["hits"]]

# 3. 批量查询 SKU(按价格排序)
sku_result = es.search(
    index="sku_index",
    body={
        "query": {"terms": {"spu_id": spu_ids}},
        "sort": {"price": "asc"},
        "_source": ["spu_id", "color", "size", "price"]
    },
    size=1000
)

# 4. 应用层组装结果(SPU + 最低价格 SKU)
spu_sku_map = {}
for sku in sku_result["hits"]["hits"]["_source"]:
    spu_id = sku["spu_id"]
    if spu_id not in spu_sku_map or sku["price"] < spu_sku_map[spu_id]["price"]:
        spu_sku_map[spu_id] = sku

# 5. 合并 SPU 和 SKU 信息
merged_result = []
for spu_hit in spu_result["hits"]["hits"]:
    spu = spu_hit["_source"]
    sku = spu_sku_map.get(spu["spu_id"], {})
    merged_result.append({
        "spu": spu,
        "lowest_price_sku": sku
    })

4. 预连接数据(Denormalization)(推荐)

场景:读多写少,需快速查询(如商品列表页)。
示例:将常用 SKU 信息冗余到 SPU 文档中。

数据结构

PUT /products/_doc/SPU001
{
  "spu_id": "SPU001",
  "name": "苹果 iPhone 14",
  "brand": "Apple",
  "category": "手机",
  "min_price": 5999.00,  // 预计算:最低价格
  "max_price": 6799.00,  // 预计算:最高价格
  "default_sku": {  // 预定义:默认 SKU
    "sku_id": "SKU001",
    "color": "黑色",
    "size": "128GB",
    "price": 5999.00
  },
  "sku_summary": [  // 预聚合:SKU 摘要
    {"color": "黑色", "size": "128GB", "price": 5999.00},
    {"color": "蓝色", "size": "256GB", "price": 6799.00}
  ]
}

查询示例

  • 需求:查找价格区间在 5000-6500 的手机,返回 SPU 及默认 SKU。
  • 实现
{
  "query": {
    "range": {"min_price": {"gte": 5000, "lte": 6500}}
  },
  "_source": ["spu_id", "name", "brand", "default_sku"]
}

方案对比

方法 查询性能 写入性能 数据一致性 适用场景
嵌套对象 SKU 数量少,需原子性操作
父子关系 SKU 动态变化,需独立管理
应用层关联 跨索引关联,数据量大
预连接数据 最高 读多写少,实时性要求低

优化建议

  1. 嵌套对象深度限制:避免超过 1000 个嵌套文档,否则性能下降。
  2. 父子关系分片一致性:父子文档必须在同一分片(通过 routing 参数保证)。
  3. 应用层关联批量查询:使用 terms 查询替代循环单条查询。
  4. 预连接数据更新策略:通过异步任务(如消息队列)更新冗余字段。

根据实际业务场景(如 SKU 数量、读写比例、实时性要求)选择合适的方案,或组合使用多种方案。


网站公告

今日签到

点亮在社区的每一天
去签到