# 索引字段说明

本文档详细说明商品搜索所用 Elasticsearch 索引的字段结构、类型、数据来源和用途。

## 索引概述

- **索引命名**: 按租户独立索引，名称为 **`search_products_tenant_{tenant_id}`**（例如 `search_products_tenant_162`）。由代码中 `get_tenant_index_name(tenant_id)` 生成。
- **索引维度**: SPU（Standard Product Unit）级别
- **多租户隔离**: **索引层面隔离**——每个租户一个独立索引，查询与写入时直接使用该索引名，无需在查询条件中再按 `tenant_id` 过滤。
- **Mapping 模板**: `mappings/search_products.json`（新建每个租户索引时复用同一份 mapping）

## ES 索引在代码中的使用

索引名由 **`indexer/mapping_generator.py`** 中的 `get_tenant_index_name(tenant_id)` 生成，格式为 `search_products_tenant_{tenant_id}`。

| 场景 | 位置 | 说明 |
|------|------|------|
| 搜索请求 | `search/searcher.py` | 构建 ES 查询时通过 `get_tenant_index_name(tenant_id)` 得到 `index_name`，调用 `es_client.search(index_name=..., body=...)`，不再在 query 中加 `tenant_id` 过滤。 |
| 全量写入 | `indexer/bulk_indexing_service.py` | 按 `tenant_id` 调用 `get_tenant_index_name(tenant_id)` 作为写入目标索引，建索引与 bulk 写入均使用该名。 |
| 增量写入 | `indexer/incremental_service.py` | 同上，索引名由 `get_tenant_index_name(tenant_id)` 得到。 |
| 建索引 / 删索引 | `indexer/mapping_generator.py` | `create_index_if_not_exists`、`delete_index_if_exists` 等均按传入的 `index_name`（即租户索引名）操作。 |

因此：每个租户对应一个独立 ES 索引，查询与写入都直接指定该索引名，实现索引级多租户隔离。

## 字段分类

### 1. 基础标识字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `tenant_id` | keyword | 租户ID（文档级标识，便于排查与统计；隔离已由索引名保证，查询无需再按此过滤） | MySQL: `shoplazza_product_spu.tenant_id` |
| `spu_id` | keyword | SPU唯一标识 | MySQL: `shoplazza_product_spu.id` |
| `create_time` | date | 创建时间 | MySQL: `shoplazza_product_spu.created_at` |
| `update_time` | date | 更新时间 | MySQL: `shoplazza_product_spu.updated_at` |

### 2. 多语言文本字段

所有文本字段都支持中英文双语，后端根据请求的 `language` 参数自动选择对应语言字段返回。

#### 2.1 标题字段

| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 |
|--------|--------|--------|------|----------|
| `title.zh` | text | hanlp_index / hanlp_standard | 中文标题 | MySQL: `shoplazza_product_spu.title` |
| `title.en` | text | english | 英文标题 | 暂为空（待翻译服务填充） |

#### 2.2 描述字段

| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 |
|--------|--------|--------|------|----------|
| `brief.zh` | text | hanlp_index / hanlp_standard | 中文短描述 | MySQL: `shoplazza_product_spu.brief` |
| `brief.en` | text | english | 英文短描述 | 暂为空 |
| `description.zh` | text | hanlp_index / hanlp_standard | 中文详细描述 | MySQL: `shoplazza_product_spu.description` |
| `description.en` | text | english | 英文详细描述 | 暂为空 |

#### 2.3 供应商/品牌字段

| 字段名 | ES类型 | 分析器 | 子字段 | 说明 | 数据来源 |
|--------|--------|--------|--------|------|----------|
| `vendor.zh` | text | hanlp_index / hanlp_standard | `vendor.zh.keyword` (keyword, normalizer: lowercase) | 中文供应商/品牌 | MySQL: `shoplazza_product_spu.vendor` |
| `vendor.en` | text | english | `vendor.en.keyword` (keyword, normalizer: lowercase) | 英文供应商/品牌 | 暂为空 |

**用途**:
- `text` 类型：用于全文搜索（支持模糊匹配）
- `keyword` 子字段：用于精确匹配过滤和分面聚合

### 3. 标签字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `tags` | keyword | 标签列表（数组） | MySQL: `shoplazza_product_spu.tags`（逗号分隔字符串，转换为数组） |

**数据格式**: `["新品", "热卖", "爆款"]`

### 4. 类目字段

#### 4.1 类目路径（用于搜索）

| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 |
|--------|--------|--------|------|----------|
| `category_path.zh` | text | hanlp_index / hanlp_standard | 中文类目路径（如"服装/男装/衬衫"） | MySQL: `shoplazza_product_spu.category_path` |
| `category_path.en` | text | english | 英文类目路径 | 暂为空 |

#### 4.2 类目名称（用于搜索）

| 字段名 | ES类型 | 分析器 | 说明 | 数据来源 |
|--------|--------|--------|------|----------|
| `category_name_text.zh` | text | hanlp_index / hanlp_standard | 中文类目名称 | MySQL: `shoplazza_product_spu.category` |
| `category_name_text.en` | text | english | 英文类目名称 | 暂为空 |

#### 4.3 类目标识（用于过滤和分面）

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `category_id` | keyword | 类目ID | MySQL: `shoplazza_product_spu.category_id` |
| `category_name` | keyword | 类目名称（用于过滤） | MySQL: `shoplazza_product_spu.category` |
| `category_level` | integer | 类目层级（1/2/3） | MySQL: `shoplazza_product_spu.category_level` |
| `category1_name` | keyword | 一级类目名称 | 从 `category_path` 解析 |
| `category2_name` | keyword | 二级类目名称 | 从 `category_path` 解析 |
| `category3_name` | keyword | 三级类目名称 | 从 `category_path` 解析 |

**用途**:
- `category_path.zh/en`, `category_name_text.zh/en`: 用于全文搜索，支持模糊匹配
- `category_id`, `category_name`, `category_level`, `category1/2/3_name`: 用于精确过滤和分面聚合

### 5. 规格字段（Specifications）

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `specifications` | nested | 规格列表（嵌套对象数组） | MySQL: `shoplazza_product_option` + `shoplazza_product_sku.option1/2/3` |

**嵌套结构**:
```json
{
  "specifications": [
    {
      "sku_id": "sku_123",
      "name": "color",
      "value": "white"
    },
    {
      "sku_id": "sku_123",
      "name": "size",
      "value": "256GB"
    }
  ]
}
```

**数据来源**:
- `name`: 从 `shoplazza_product_option` 表获取（选项名称，如"color"、"size"）
- `value`: 从 `shoplazza_product_sku` 表的 `option1`, `option2`, `option3` 字段获取（选项值，如"white"、"256GB"）
- `sku_id`: SKU ID，用于关联

**API 过滤示例**:
```json
{
  "query": "手机",
  "filters": {
  "specifications": {
      "name": "color",
      "value": "white"
    }
  }
}
```

**多个规格过滤（按维度分组）**:
```json
{
  "query": "手机",
  "filters": {
    "specifications": [
      {"name": "color", "value": "white"},
      {"name": "size", "value": "256GB"}
    ]
    }
}
```
说明：不同维度（不同name）是AND关系，相同维度（相同name）的多个值是OR关系。

**示例：相同维度的多个值（OR）**:
```json
{
  "query": "手机",
  "filters": {
    "specifications": [
      {"name": "size", "value": "3"},
      {"name": "size", "value": "4"},
      {"name": "size", "value": "5"},
      {"name": "color", "value": "green"}
    ]
  }
}
```
生成查询：(size=3 OR size=4 OR size=5) AND color=green

**ES 查询结构**（后端自动生成）:
```json
{
  "filter": [
    {
      "nested": {
        "path": "specifications",
        "query": {
          "bool": {
            "should": [
              {"bool": {"must": [{"term": {"specifications.name": "size"}}, {"term": {"specifications.value": "3"}}]}},
              {"bool": {"must": [{"term": {"specifications.name": "size"}}, {"term": {"specifications.value": "4"}}]}},
              {"bool": {"must": [{"term": {"specifications.name": "size"}}, {"term": {"specifications.value": "5"}}]}}
            ],
            "minimum_should_match": 1
          }
        }
      }
    },
    {
      "nested": {
        "path": "specifications",
        "query": {
          "bool": {
            "must": [
              {"term": {"specifications.name": "color"}},
              {"term": {"specifications.value": "green"}}
            ]
          }
        }
      }
    }
  ]
}
```

**API 分面示例**:

所有规格名称的分面：
```json
{
  "query": "手机",
  "facets": ["specifications"]
}
```

指定规格名称的分面：
```json
{
  "query": "手机",
  "facets": ["specifications.color", "specifications.size"]
}
```

**ES 聚合结构**（后端自动生成）:

所有规格名称：
```json
{
  "aggs": {
    "specifications_facet": {
      "nested": { "path": "specifications" },
      "aggs": {
        "by_name": {
          "terms": { "field": "specifications.name", "size": 20 },
          "aggs": {
            "value_counts": {
              "terms": { "field": "specifications.value", "size": 10 }
            }
          }
        }
      }
    }
  }
}
```
  
指定规格名称：
```json
{
  "aggs": {
    "specifications_color_facet": {
      "nested": { "path": "specifications" },
      "aggs": {
        "filter_by_name": {
          "filter": { "term": { "specifications.name": "color" } },
          "aggs": {
            "value_counts": {
              "terms": { "field": "specifications.value", "size": 10 }
            }
          }
        }
      }
    }
  }
}
```

### 6. 选项名称字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `option1_name` | keyword | 选项1名称（如"color"） | MySQL: `shoplazza_product_option` |
| `option2_name` | keyword | 选项2名称（如"size"） | MySQL: `shoplazza_product_option` |
| `option3_name` | keyword | 选项3名称 | MySQL: `shoplazza_product_option` |

### 7. 价格字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `min_price` | float | 最低价格 | 从所有 SKU 价格计算 |
| `max_price` | float | 最高价格 | 从所有 SKU 价格计算 |
| `compare_at_price` | float | 原价/对比价 | MySQL: `shoplazza_product_spu.compare_at_price` |
| `sku_prices` | float | 所有 SKU 价格列表（数组） | 从所有 SKU 价格汇总 |

### 8. 重量字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `sku_weights` | long | 所有 SKU 重量列表（数组） | 从所有 SKU 重量汇总 |
| `sku_weight_units` | keyword | 所有 SKU 重量单位列表（数组） | 从所有 SKU 重量单位汇总 |

### 9. 库存与销量字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `total_inventory` | long | 总库存（所有 SKU 库存之和） | 从所有 SKU 库存汇总 |
| `sales` | long | 销量（展示销量） | MySQL: `shoplazza_product_spu.fake_sales` |

### 10. SKU 嵌套字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `skus` | nested | SKU 详细信息列表（嵌套对象数组） | MySQL: `shoplazza_product_sku` |

**嵌套结构**:
```json
{
  "skus": [
    {
      "sku_id": "sku_123",
      "price": 99.99,
      "compare_at_price": 149.99,
      "sku_code": "SKU001",
      "stock": 100,
      "weight": 0.5,
      "weight_unit": "kg",
      "option1_value": "white",
      "option2_value": "256GB",
      "option3_value": null,
      "image_src": "https://example.com/image.jpg"
    }
  ]
}
```

**字段说明**:
- `sku_id`: SKU 唯一标识
- `price`: SKU 价格
- `compare_at_price`: SKU 原价
- `sku_code`: SKU 编码
- `stock`: 库存数量
- `weight`: 重量
- `weight_unit`: 重量单位
- `option1_value`, `option2_value`, `option3_value`: 选项值（对应 `option1_name`, `option2_name`, `option3_name`）
- `image_src`: SKU 图片地址（`index: false`，仅用于返回）

### 11. 图片字段

| 字段名 | ES类型 | 说明 | 数据来源 |
|--------|--------|------|----------|
| `image_url` | keyword | 主图URL（`index: false`，仅用于返回） | MySQL: `shoplazza_product_spu.image_url` |

### 12. 向量字段（不返回给前端）

| 字段名 | ES类型 | 维度 | 说明 | 数据来源 |
|--------|--------|------|------|----------|
| `title_embedding` | dense_vector | 1024 | 标题向量（用于语义搜索） | 由 Qwen3-Embedding-0.6B 模型生成 |
| `image_embedding` | nested | - | 图片向量（用于图片搜索） | 由 CN-CLIP 模型生成 |

**注意**: 这些字段仅用于搜索，不会返回给前端。

## 字段用途总结

### 搜索字段（参与相关性计算）

- `title.zh`, `title.en` (boost: 3.0)
- `brief.zh`, `brief.en` (boost: 1.5)
- `description.zh`, `description.en` (boost: 1.0)
- `vendor.zh`, `vendor.en` (boost: 1.5)
- `tags` (boost: 1.0)
- `category_path.zh`, `category_path.en` (boost: 1.5)
- `category_name_text.zh`, `category_name_text.en` (boost: 1.5)
- `title_embedding` (向量召回，boost: 0.2)

### 过滤字段（精确匹配）

- `tenant_id`（文档中保留，便于排查与统计；**多租户隔离已由索引名保证，查询时无需再按 tenant_id 过滤**）
- `category_id`, `category_name`, `category1_name`, `category2_name`, `category3_name`
- `vendor.zh.keyword`, `vendor.en.keyword`
- `specifications` (嵌套查询)
- `min_price`, `max_price` (范围过滤)
- `sales` (范围过滤)
- `total_inventory` (范围过滤)

### 排序字段

- `price`: 价格（前端传入，后端自动映射：asc→min_price，desc→max_price）
- `sales`: 销量
- `create_time`: 创建时间
- `update_time`: 更新时间
- `relevance_score`: 相关性分数（默认）

### 分面字段（聚合统计）

- `category1_name`, `category2_name`, `category3_name`
- `specifications` (所有规格名称的分面，嵌套聚合，按 name 分组，然后按 value 聚合)
- `specifications.{name}` (指定规格名称的分面，如 `specifications.color`，只返回该 name 的 value 列表)

### 返回字段（前端展示）

除 `title_embedding` 和 `image_embedding` 外，所有字段都会根据 `language` 参数自动选择对应的中英文字段返回。

## 数据映射规则

### 多语言字段映射

后端根据请求的 `language` 参数（`zh` 或 `en`）自动选择：

- `language="zh"`: 优先返回 `*_zh` 字段，如果为空则回退到 `*_en` 字段
- `language="en"`: 优先返回 `*_en` 字段，如果为空则回退到 `*_zh` 字段

映射到前端字段：
- `title.zh/en` → `title`
- `brief.zh/en` → `brief`
- `description.zh/en` → `description`
- `vendor.zh/en` → `vendor`
- `category_path.zh/en` → `category_path`
- `category_name_text.zh/en` → `category_name`

### 规格数据构建

1. 从 `shoplazza_product_option` 表获取选项名称（`option1_name`, `option2_name`, `option3_name`）
2. 从 `shoplazza_product_sku` 表获取选项值（`option1`, `option2`, `option3`）
3. 将每个 SKU 的选项组合构建为 `specifications` 数组：
   ```python
   for sku in skus:
       if sku.option1 and option1_name:
           specifications.append({
               "sku_id": sku.id,
               "name": option1_name,  # 如"color"
               "value": sku.option1   # 如"white"
           })
       # 同样处理 option2, option3
   ```

## 查询架构

### 查询结构

```
filters AND (text_recall OR embedding_recall)
```

- **filters**: 前端传递的过滤条件（永远起作用）
- **text_recall**: 文本相关性召回（同时搜索中英文字段）
- **embedding_recall**: 向量召回（KNN）
- **function_score**: 包装召回部分，支持提权字段（新鲜度、销量等）

### 文本召回字段

根据查询词的语言选择对应的索引字段：
- `title.zh^3.0`, `title.en^3.0`
- `brief.zh^1.5`, `brief.en^1.5`
- `description.zh^1.0`, `description.en^1.0`
- `vendor.zh^1.5`, `vendor.en^1.5`
- `category_path.zh^1.5`, `category_path.en^1.5`
- `category_name_text.zh^1.5`, `category_name_text.en^1.5`
- `tags^1.0`

## 注意事项

1. **索引维度**: 所有数据以 SPU 为单位索引，SKU 信息作为嵌套字段存储
2. **多租户隔离**: 按租户使用独立索引（`search_products_tenant_{tenant_id}`），查询时通过 `get_tenant_index_name(tenant_id)` 指定索引名即可，无需在 query 中加 `tenant_id` 过滤
3. **多语言支持**: 文本字段支持中英文，后端根据 `language` 参数自动选择
4. **规格分面**: `specifications` 使用嵌套聚合，按 `name` 分组，然后按 `value` 聚合
5. **向量字段**: `title_embedding` 和 `image_embedding` 仅用于搜索，不返回给前端
  
