# 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` | 逗号分隔的字符串 → 数组<br/>示例：`"标签1,标签2"` → `["标签1", "标签2"]` |

### 2.4 图片字段

| ES 字段 | 数据来源表 | 表中字段 | 转换说明 |
|---------|-----------|----------|----------|
| `image_url` | SPU 表 | `image_src` |  |

### 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 值，去重<br/>示例：`["红色", "蓝色", "绿色"]` |
| `option2_values` | SKU 表 | `option2` | 收集所有 SKU 的 option2 值，去重<br/>示例：`["S", "M", "L"]` |
| `option3_values` | SKU 表 | `option3` | 收集所有 SKU 的 option3 值，去重<br/>示例：`["棉", "涤纶"]` |

**转换逻辑：**

```
遍历所有 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` 数组<br/>示例：`[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` 数组<br/>示例：`[500, 500, 600]` |
| `sku_weight_units` | SKU 表 | 所有 SKU 的 `weight_unit` 去重后的列表<br/>示例：`["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 维向量<br/>优先使用 `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
GET /search_products/_search
{
    "size": 1,
    "_source": "*",
    "query": {
      "bool": {
        "filter": [
          { "term": {"_id" : 74174} },
          { "term": { "tenant_id": "162" } }
        ]
      }
    }
  }
```

结果

```json
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0,
    "hits": [
      {
        "_index": "search_products",
        "_id": "74174",
        "_score": 0,
        "_source": {
          "tenant_id": "162",
          "spu_id": "74174",
          "title.zh": "实色二阶碳纤维魔方 方形 2阶 塑料【英文包装】",
          "title.en": "Solid Color 2nd Order Carbon Fiber Rubik's Cube Square 2 Steps Plastic [English Packaging",
          "brief.zh": "实色二阶碳纤维魔方 方形 2阶 塑料【英文包装】",
          "brief.en": "Solid Color 2nd Order Carbon Fiber Rubik's Cube Square 2 Steps Plastic [English Packaging",
          "description.zh": "<p>实色二阶碳纤维魔方 方形 2阶 塑料【英文包装】</p>",
          "description.en": "<p>Solid Color 2nd Order Carbon Fiber Rubik's Cube Square 2 Steps Plastic [English Packaging]</p",
          "vendor.zh": "顺达",
          "vendor.en": "Shunda",
          "title_embedding": [
            -0.005125104915350676,
...
            0.018752742558717728
          ],
          "tags": [
            "魔方",
            "ShunDa"
          ],
          "category_path.zh": "玩具/汽车",
          "category_path.en": null,
          "category1_name": "玩具",
          "category2_name": "汽车",
          "category_name_text.zh": "汽车",
          "category_name_text.en": null,
          "category_name": "汽车",
          "category_id": "593389466647880832",
          "category_level": 2,
          "option1_name": "color",
          "option2_name": "size",
          "option3_name": "material",
          "image_url": "////img.staticdj.com/6eb4a98e2cb23af523da5456e424fc24.jpeg",
          "sales": 0,
          "skus": [
            {
              "sku_id": "3577878",
              "price": 346.13,
              "compare_at_price": 464.3,
              "sku_code": "2210777-Orange-30-塑料英文包装",
              "stock": 87,
              "weight": 1.99,
              "weight_unit": "kg",
              "option1_value": "Orange",
              "option2_value": "30",
              "option3_value": "塑料英文包装"
            },
            {
              "sku_id": "3577879",
              "price": 316.62,
              "compare_at_price": 466.27,
              "sku_code": "2210777-Orange-8-塑料英文包装",
              "stock": 41,
              "weight": 1.95,
              "weight_unit": "kg",
              "option1_value": "Orange",
              "option2_value": "8",
              "option3_value": "塑料英文包装"
            },
...
            {
              "sku_id": "3577941",
              "price": 380.03,
              "compare_at_price": 553.67,
              "sku_code": "2210777-Olive-2-塑料英文包装",
              "stock": 56,
              "weight": 2.78,
              "weight_unit": "kg",
              "option1_value": "Olive",
              "option2_value": "2",
              "option3_value": "塑料英文包装"
            }
          ],
          "specifications": [
            {
              "sku_id": "3577878",
              "name": "color",
              "value": "Orange"
            },
...
            {
              "sku_id": "3577941",
              "name": "material",
              "value": "塑料英文包装"
            }
          ],
          "option1_values": [
            "White",
            "Gold",
            "Violet",
            "Olive",
            "Turquoise",
            "Khaki",
            "Teal",
            "Orange"
          ],
          "option2_values": [
            "2",
            "30",
            "8",
            "1",
            "18",
            "27",
            "25",
            "29"
          ],
          "option3_values": [
            "塑料英文包装"
          ],
          "min_price": 262.92,
          "max_price": 389.18,
          "compare_at_price": 567.6,
          "sku_prices": [
            346.13,
...
            309.64,
            380.03
          ],
          "sku_weights": [
            1,
            1,
...
            2,
            2
          ],
          "sku_weight_units": [
            "kg"
          ],
          "total_inventory": 3269,
          "create_time": "2025-11-29T00:02:00",
          "update_time": "2025-12-20T00:01:41"
        }
      }
    ]
  }
}
```

---

## 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` | 选项名称 |

