文章目錄
一、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 Elasticsearches = 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 動態變化,需獨立管理 |
應用層關聯 | 低 | 高 | 弱 | 跨索引關聯,數據量大 |
預連接數據 | 最高 | 低 | 弱 | 讀多寫少,實時性要求低 |
優化建議
- 嵌套對象深度限制:避免超過 1000 個嵌套文檔,否則性能下降。
- 父子關系分片一致性:父子文檔必須在同一分片(通過
routing
參數保證)。 - 應用層關聯批量查詢:使用
terms
查詢替代循環單條查詢。 - 預連接數據更新策略:通過異步任務(如消息隊列)更新冗余字段。
根據實際業務場景(如 SKU 數量、讀寫比例、實時性要求)選擇合適的方案,或組合使用多種方案。