# MySQL 到 Elasticsearch 字段映射说明(业务版)
## 1. 概述
本文档说明 Elasticsearch 中的商品文档(Document)的各个字段是如何从 MySQL 数据库中的相关表获取和转换的。
### 1.1 数据源表
- **SPU 表** (`shoplazza_product_spu`): 商品基本信息
- **SKU 表** (`shoplazza_product_sku`): 商品变体信息(多款式)
- **Option 表** (`shoplazza_product_option`): 选项名称定义(如:颜色、尺寸)
- **类目表** (`product_category`): 类目信息(通过 SPU 表的 category_id 关联)
---
## 2. 基础字段映射
### 2.1 标识字段
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `tenant_id` | SPU 表 | `tenant_id` | 直接使用,转为字符串 |
| `spu_id` | SPU 表 | `id` | 直接使用,转为字符串 |
### 2.2 文本字段(多语言)
文本字段支持中英文双语,根据租户配置自动翻译。
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `title_zh` | SPU 表 | `title` | 如果主语言是中文,直接使用;否则翻译为中文 |
| `title_en` | SPU 表 | `title` | 如果主语言是英文,直接使用;否则翻译为英文 |
| `brief_zh` | SPU 表 | `brief` | 同上 |
| `brief_en` | SPU 表 | `brief` | 同上 |
| `description_zh` | SPU 表 | `description` | 同上 |
| `description_en` | SPU 表 | `description` | 同上 |
| `vendor_zh` | SPU 表 | `vendor` | 同上 |
| `vendor_en` | SPU 表 | `vendor` | 同上 |
**翻译规则:**
- 根据租户配置的 `primary_language` 确定主语言
- 如果配置了 `translate_to_en=true`,且主语言不是英文,则翻译为英文
- 如果配置了 `translate_to_zh=true`,且主语言不是中文,则翻译为中文
### 2.3 标签字段
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `tags` | SPU 表 | `tags` | 逗号分隔的字符串 → 数组
示例:`"标签1,标签2"` → `["标签1", "标签2"]` |
### 2.4 图片字段
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `image_url` | SPU 表 | `image_src` | 直接使用,如果 URL 不以 `http` 开头则添加 `//` 前缀 |
### 2.5 销量字段
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `sales` | SPU 表 | `fake_sales` | 转为整数,如果为空则为 0 |
### 2.6 时间字段
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `create_time` | SPU 表 | `create_time` | 转为 ISO 格式字符串(如:`2024-01-01T00:00:00`) |
| `update_time` | SPU 表 | `update_time` | 转为 ISO 格式字符串 |
---
## 3. 类别(Category)字段映射
### 3.1 类别数据来源
类别信息主要来自 SPU 表的以下字段:
- `category_path`: 类目路径,逗号分隔的类目ID列表(如:`"1,2,3"`)
- `category`: 类目名称(备用字段)
- `category_id`: 当前类目ID
- `category_level`: 类目层级
### 3.2 类别映射流程
**步骤 1:解析 category_path**
从 SPU 表的 `category_path` 字段解析出类目ID列表:
```
category_path = "1,2,3"
→ category_ids = ["1", "2", "3"]
```
**步骤 2:ID 转名称**
通过预加载的类目映射表(从 SPU 表查询 `category_id` 和 `category` 字段构建),将ID转换为名称:
```
映射表:{"1": "电子产品", "2": "手机", "3": "iPhone"}
category_ids = ["1", "2", "3"]
→ category_names = ["电子产品", "手机", "iPhone"]
```
**步骤 3:构建 ES 字段**
| ES 字段 | 数据来源 | 转换说明 |
|---------|----------|----------|
| `category_path_zh` | 类目名称列表 | 用 `/` 连接:`"电子产品/手机/iPhone"` |
| `category1_name` | 类目名称列表[0] | 一级类目:`"电子产品"` |
| `category2_name` | 类目名称列表[1] | 二级类目:`"手机"` |
| `category3_name` | 类目名称列表[2] | 三级类目:`"iPhone"` |
| `category_id` | SPU 表 | `category_id` 转为字符串 |
| `category_level` | SPU 表 | `category_level` 转为整数 |
**备用处理:**
如果 `category_path` 为空,使用 `category` 字段作为备选:
- 如果 `category` 包含 `/`,按 `/` 分割为多级类目
- 否则,直接作为 `category1_name`
**数据质量检查:**
如果 `category_path` 中的类目ID在映射表中不存在,则不写入任何类目字段(视为没有类目)。
---
## 4. 多款式(SKU/Options)字段映射
### 4.1 SKU 嵌套结构
一个 SPU 可以有多个 SKU,每个 SKU 代表一个商品变体(如:红色-L码、蓝色-M码)。
**ES 中的 `skus` 字段结构:**
```json
{
"skus": [
{
"sku_id": "123",
"price": 99.99,
"compare_at_price": 129.99,
"sku_code": "SKU001",
"stock": 100,
"weight": 0.5,
"weight_unit": "kg",
"option1_value": "红色",
"option2_value": "L",
"option3_value": "棉",
"image_src": "https://..."
}
]
}
```
### 4.2 SKU 字段映射
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `skus[].sku_id` | SKU 表 | `id` | 转为字符串 |
| `skus[].price` | SKU 表 | `price` | 转为浮点数 |
| `skus[].compare_at_price` | SKU 表 | `compare_at_price` | 转为浮点数 |
| `skus[].sku_code` | SKU 表 | `sku` | 转为字符串 |
| `skus[].stock` | SKU 表 | `inventory_quantity` | 转为整数 |
| `skus[].weight` | SKU 表 | `weight` | 转为浮点数 |
| `skus[].weight_unit` | SKU 表 | `weight_unit` | 转为字符串 |
| `skus[].image_src` | SKU 表 | `image_src` | 转为字符串 |
| `skus[].option1_value` | SKU 表 | `option1` | 转为字符串 |
| `skus[].option2_value` | SKU 表 | `option2` | 转为字符串 |
| `skus[].option3_value` | SKU 表 | `option3` | 转为字符串 |
### 4.3 选项名称字段
选项名称来自 Option 表,按 `position` 字段排序(1、2、3 对应 option1、option2、option3)。
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `option1_name` | Option 表 | `name` (where position=1) | 第一个选项的名称(如:"颜色") |
| `option2_name` | Option 表 | `name` (where position=2) | 第二个选项的名称(如:"尺寸") |
| `option3_name` | Option 表 | `name` (where position=3) | 第三个选项的名称(如:"材质") |
**查询逻辑:**
```sql
SELECT position, name
FROM shoplazza_product_option
WHERE spu_id = ? AND deleted = 0
ORDER BY position
```
### 4.4 选项值字段
选项值来自所有 SKU 的 `option1`、`option2`、`option3` 字段,去重后形成列表。
| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `option1_values` | SKU 表 | `option1` | 收集所有 SKU 的 option1 值,去重
示例:`["红色", "蓝色", "绿色"]` |
| `option2_values` | SKU 表 | `option2` | 收集所有 SKU 的 option2 值,去重
示例:`["S", "M", "L"]` |
| `option3_values` | SKU 表 | `option3` | 收集所有 SKU 的 option3 值,去重
示例:`["棉", "涤纶"]` |
**转换逻辑:**
```
遍历所有 SKU:
- 收集 option1 值 → option1_values 列表
- 收集 option2 值 → option2_values 列表
- 收集 option3 值 → option3_values 列表
去重后写入 ES
```
**注意:** 只有配置在 `searchable_option_dimensions` 中的选项才会被索引。
### 4.5 规格(Specifications)字段
规格字段用于支持按规格过滤和分面搜索,将 SKU 的选项值结构化存储。
**ES 中的 `specifications` 字段结构:**
```json
{
"specifications": [
{"sku_id": "123", "name": "颜色", "value": "红色"},
{"sku_id": "123", "name": "尺寸", "value": "L"},
{"sku_id": "124", "name": "颜色", "value": "蓝色"},
{"sku_id": "124", "name": "尺寸", "value": "M"}
]
}
```
**构建逻辑:**
1. 从 Option 表获取选项名称映射:
```
position=1 → name="颜色"
position=2 → name="尺寸"
position=3 → name="材质"
```
2. 遍历所有 SKU,为每个 SKU 的每个选项值构建规格记录:
```
SKU.id=123, option1="红色", option2="L"
→ {"sku_id": "123", "name": "颜色", "value": "红色"}
→ {"sku_id": "123", "name": "尺寸", "value": "L"}
```
| ES 字段 | 数据来源 | 转换说明 |
|---------|----------|----------|
| `specifications[].sku_id` | SKU 表 | `id` 转为字符串 |
| `specifications[].name` | Option 表 | `name`(根据 position 匹配) |
| `specifications[].value` | SKU 表 | `option1/2/3` 转为字符串 |
### 4.6 价格聚合字段
从所有 SKU 的价格中计算聚合值。
| ES 字段 | 数据来源 | 转换说明 |
|---------|----------|----------|
| `min_price` | SKU 表 | 所有 SKU 的 `price` 最小值 |
| `max_price` | SKU 表 | 所有 SKU 的 `price` 最大值 |
| `compare_at_price` | SKU 表 | 所有 SKU 的 `compare_at_price` 最大值 |
| `sku_prices` | SKU 表 | 所有 SKU 的 `price` 数组
示例:`[99.99, 99.99, 129.99]` |
**计算逻辑:**
```
遍历所有 SKU:
- 收集 price → prices 列表
- 收集 compare_at_price → compare_prices 列表
min_price = min(prices)
max_price = max(prices)
compare_at_price = max(compare_prices)
sku_prices = prices 列表
```
### 4.7 库存和重量聚合字段
| ES 字段 | 数据来源 | 转换说明 |
|---------|----------|----------|
| `total_inventory` | SKU 表 | 所有 SKU 的 `inventory_quantity` 求和 |
| `sku_weights` | SKU 表 | 所有 SKU 的 `weight` 数组
示例:`[500, 500, 600]` |
| `sku_weight_units` | SKU 表 | 所有 SKU 的 `weight_unit` 去重后的列表
示例:`["kg"]` |
**计算逻辑:**
```
遍历所有 SKU:
- 累加 inventory_quantity → total_inventory
- 收集 weight → sku_weights 列表
- 收集 weight_unit → sku_weight_units 列表
total_inventory = 所有 SKU 库存之和
sku_weights = 所有 SKU 重量数组
sku_weight_units = 去重后的重量单位列表
```
---
## 5. 向量字段映射
### 5.1 标题向量(title_embedding)
| ES 字段 | 数据来源 | 转换说明 |
|---------|----------|----------|
| `title_embedding` | SPU 表 `title` | 使用文本编码器(BGE)将标题转换为 1024 维向量
优先使用 `title_en`,如果没有则使用 `title_zh` |
**生成逻辑:**
```
如果启用向量搜索:
文本 = title_en 或 title_zh
向量 = 文本编码器.encode(文本)
title_embedding = 向量(1024 维浮点数组)
```
---
## 6. 完整字段映射表
### 6.1 字段来源汇总
| ES 字段 | 数据来源表 | 表中字段 | 说明 |
|---------|-----------|----------|------|
| **基础字段** |
| `tenant_id` | SPU | `tenant_id` | 租户ID |
| `spu_id` | SPU | `id` | 商品ID |
| `title_zh/en` | SPU | `title` | 标题(多语言) |
| `brief_zh/en` | SPU | `brief` | 简介(多语言) |
| `description_zh/en` | SPU | `description` | 描述(多语言) |
| `vendor_zh/en` | SPU | `vendor` | 品牌(多语言) |
| `tags` | SPU | `tags` | 标签数组 |
| `image_url` | SPU | `image_src` | 主图URL |
| `sales` | SPU | `fake_sales` | 销量 |
| `create_time` | SPU | `create_time` | 创建时间 |
| `update_time` | SPU | `update_time` | 更新时间 |
| **类别字段** |
| `category_path_zh` | SPU + 类目映射 | `category_path` → 类目名称 | 类目路径 |
| `category1_name` | SPU + 类目映射 | `category_path` → 类目名称[0] | 一级类目 |
| `category2_name` | SPU + 类目映射 | `category_path` → 类目名称[1] | 二级类目 |
| `category3_name` | SPU + 类目映射 | `category_path` → 类目名称[2] | 三级类目 |
| `category_id` | SPU | `category_id` | 类目ID |
| `category_level` | SPU | `category_level` | 类目层级 |
| **SKU 嵌套字段** |
| `skus[]` | SKU | 多个字段 | SKU 数组(见 4.2 节) |
| **选项字段** |
| `option1_name` | Option | `name` (position=1) | 选项1名称 |
| `option2_name` | Option | `name` (position=2) | 选项2名称 |
| `option3_name` | Option | `name` (position=3) | 选项3名称 |
| `option1_values` | SKU | `option1` | 选项1值列表 |
| `option2_values` | SKU | `option2` | 选项2值列表 |
| `option3_values` | SKU | `option3` | 选项3值列表 |
| **规格字段** |
| `specifications[]` | SKU + Option | `option1/2/3` + `name` | 规格数组 |
| **聚合字段** |
| `min_price` | SKU | `price` | 最低价格 |
| `max_price` | SKU | `price` | 最高价格 |
| `compare_at_price` | SKU | `compare_at_price` | 最高原价 |
| `sku_prices` | SKU | `price` | 所有价格数组 |
| `total_inventory` | SKU | `inventory_quantity` | 总库存 |
| `sku_weights` | SKU | `weight` | 所有重量数组 |
| `sku_weight_units` | SKU | `weight_unit` | 重量单位列表 |
| **向量字段** |
| `title_embedding` | SPU | `title` | 标题向量(1024维) |
---
## 7. 数据查询示例
### 7.1 查询 SPU 数据
```sql
SELECT
id, tenant_id, title, brief, description, vendor,
category, category_id, category_path, category_level,
tags, image_src, fake_sales, create_time, update_time
FROM shoplazza_product_spu
WHERE tenant_id = ? AND id = ? AND deleted = 0
```
### 7.2 查询 SKU 数据
```sql
SELECT
id, spu_id, price, compare_at_price, sku,
inventory_quantity, weight, weight_unit,
option1, option2, option3, image_src
FROM shoplazza_product_sku
WHERE tenant_id = ? AND spu_id = ? AND deleted = 0
```
### 7.3 查询 Option 数据
```sql
SELECT
position, name
FROM shoplazza_product_option
WHERE tenant_id = ? AND spu_id = ? AND deleted = 0
ORDER BY position
```
### 7.4 查询类目映射
```sql
SELECT DISTINCT
category_id, category
FROM shoplazza_product_spu
WHERE deleted = 0 AND category_id IS NOT NULL
```
---
## 8. ES 文档示例
```json
{
"tenant_id": "1",
"spu_id": "12345",
"title_zh": "iPhone 15 Pro Max",
"title_en": "iPhone 15 Pro Max",
"brief_zh": "最新款 iPhone",
"brief_en": "Latest iPhone",
"description_zh": "详细描述...",
"description_en": "Detailed description...",
"vendor_zh": "Apple",
"vendor_en": "Apple",
"tags": ["手机", "智能手机", "Apple"],
"category_path_zh": "电子产品/手机/iPhone",
"category1_name": "电子产品",
"category2_name": "手机",
"category3_name": "iPhone",
"category_id": "3",
"category_level": 3,
"option1_name": "颜色",
"option2_name": "存储容量",
"option1_values": ["深空黑色", "原色钛金属", "白色钛金属"],
"option2_values": ["256GB", "512GB", "1TB"],
"image_url": "https://example.com/image.jpg",
"sales": 1000,
"min_price": 8999.0,
"max_price": 12999.0,
"compare_at_price": 12999.0,
"sku_prices": [8999.0, 10999.0, 12999.0],
"sku_weights": [221, 221, 221],
"sku_weight_units": ["g"],
"total_inventory": 500,
"create_time": "2024-01-01T00:00:00",
"update_time": "2024-01-15T10:30:00",
"title_embedding": [0.123, 0.456, ...],
"skus": [
{
"sku_id": "1001",
"price": 8999.0,
"compare_at_price": 9999.0,
"sku_code": "IP15PM-256-BLK",
"stock": 100,
"weight": 221.0,
"weight_unit": "g",
"option1_value": "深空黑色",
"option2_value": "256GB",
"image_src": "https://example.com/sku1.jpg"
}
],
"specifications": [
{"sku_id": "1001", "name": "颜色", "value": "深空黑色"},
{"sku_id": "1001", "name": "存储容量", "value": "256GB"}
]
}
```
---
## 9. 关键映射关系总结
### 9.1 类别映射
```
SPU.category_path ("1,2,3")
↓ [解析ID列表]
category_ids ["1", "2", "3"]
↓ [通过映射表转换]
category_names ["电子产品", "手机", "iPhone"]
↓ [构建字段]
category_path_zh: "电子产品/手机/iPhone"
category1_name: "电子产品"
category2_name: "手机"
category3_name: "iPhone"
```
### 9.2 SKU 和选项映射
```
Option 表 (position=1, name="颜色")
↓
option1_name: "颜色"
SKU 表 (option1="红色", option1="蓝色")
↓ [收集所有值,去重]
option1_values: ["红色", "蓝色"]
SKU 表 + Option 表
↓ [组合构建]
specifications: [
{name: "颜色", value: "红色", sku_id: "123"},
{name: "颜色", value: "蓝色", sku_id: "124"}
]
```
### 9.3 价格聚合
```
SKU 表 (price: 99.99, 109.99, 129.99)
↓ [聚合计算]
min_price: 99.99
max_price: 129.99
sku_prices: [99.99, 109.99, 129.99]
```
---
## 附录:数据表字段对照
### SPU 表主要字段
| 表字段 | ES 字段 | 说明 |
|--------|---------|------|
| `id` | `spu_id` | 商品ID |
| `tenant_id` | `tenant_id` | 租户ID |
| `title` | `title_zh/en` | 标题 |
| `brief` | `brief_zh/en` | 简介 |
| `description` | `description_zh/en` | 描述 |
| `vendor` | `vendor_zh/en` | 品牌 |
| `tags` | `tags` | 标签 |
| `category_path` | `category_path_zh` | 类目路径 |
| `category_id` | `category_id` | 类目ID |
| `category_level` | `category_level` | 类目层级 |
| `image_src` | `image_url` | 主图 |
| `fake_sales` | `sales` | 销量 |
| `create_time` | `create_time` | 创建时间 |
| `update_time` | `update_time` | 更新时间 |
### SKU 表主要字段
| 表字段 | ES 字段 | 说明 |
|--------|---------|------|
| `id` | `skus[].sku_id` | SKU ID |
| `price` | `skus[].price`, `min_price`, `max_price`, `sku_prices` | 价格 |
| `compare_at_price` | `skus[].compare_at_price`, `compare_at_price` | 原价 |
| `sku` | `skus[].sku_code` | SKU编码 |
| `inventory_quantity` | `skus[].stock`, `total_inventory` | 库存 |
| `weight` | `skus[].weight`, `sku_weights` | 重量 |
| `weight_unit` | `skus[].weight_unit`, `sku_weight_units` | 重量单位 |
| `option1` | `skus[].option1_value`, `option1_values`, `specifications[].value` | 选项1值 |
| `option2` | `skus[].option2_value`, `option2_values`, `specifications[].value` | 选项2值 |
| `option3` | `skus[].option3_value`, `option3_values`, `specifications[].value` | 选项3值 |
| `image_src` | `skus[].image_src` | SKU图片 |
### Option 表主要字段
| 表字段 | ES 字段 | 说明 |
|--------|---------|------|
| `position` | - | 位置(1/2/3) |
| `name` | `option1_name`, `option2_name`, `option3_name`, `specifications[].name` | 选项名称 |