# ES Mapping Configuration

## 概述

所有租户共享同一个 Elasticsearch mapping 结构。

当前目录采用“声明式 Python 规格 + 字段模板 + 最终 JSON 产物”的方式维护 `search_products` 的索引定义：

- `generate_search_products_mapping.py`: 唯一的生成源，包含字段模板、语言列表、分析器配置和递归生成逻辑
- `search_products.json`: 由脚本生成的完整 ES 索引配置，包括 `settings` 和 `mappings`
- `search_suggestions.json`: 搜索建议索引配置

默认应修改生成脚本中的规格定义，而不是手工编辑 `search_products.json`。

## 字段抽象

脚本从业务语义上抽象出 4 类文本模板：

- `all_language_text`: 全语言字段，不带 `keyword`
- `all_language_text_with_keyword`: 全语言字段，所有受支持语言都带 `keyword`
- `core_language_text`: 核心索引语言字段，不带 `keyword`
- `core_language_text_with_keyword`: 核心索引语言字段，核心语言都带 `keyword`

这里的“核心索引语言”不是因为系统只支持两种语言，而是因为所有店铺、所有商品都必须至少产出这两种语言的索引内容。目前核心索引语言固定为：

- `zh`
- `en`

“全语言”表示 mapping 为原始商品语言预留了更多语言槽位。商品实际灌入时，不要求每个字段把所有语言都填满，只要求：

- 核心索引语言字段必须填充 `zh` 和 `en`
- 全语言字段必须填充 `zh` 和 `en`
- 如果商品原始语言属于受支持语言，还应额外填充对应的原始语言字段，例如 `ru`

当前字段大致分为几类：

- 全语言字段：`title`、`keywords`、`brief`、`description`、`vendor`、`category_path`、`category_name_text`
- 核心索引语言字段：`qanchors`、`enriched_tags`、`option1_values`、`option2_values`、`option3_values`、`enriched_attributes.value`
- 复合嵌套字段：`image_embedding`、`specifications`、`enriched_attributes`、`skus`
- 其他标量字段：`tenant_id`、`spu_id`、价格、库存、类目等

生成规则里的几个基础约束：

- 中文字段使用 `index_ik`，并额外设置 `search_analyzer: query_ik`
- 非中文语言使用各自的 Elasticsearch 内置 analyzer
- 带 `with_keyword` 的模板会为对应语言增加 `.keyword`
- `settings.analysis`、`normalizer`、`similarity` 也属于生成结果的一部分，不能只维护 `mappings.properties`

## 索引灌入指引

### 基本原则

1. 所有商品都必须生成核心索引语言版本，也就是 `zh` 和 `en`。
2. 全语言字段除了必须有 `zh` 和 `en`，还应尽量保留商品原始语言版本。
3. 如果商品原始语言本身就是 `zh` 或 `en`，则原文直接写入对应字段，另一种核心语言通过翻译补齐。
4. 如果商品原始语言是 `ru` 这类受支持的非核心语言，则应同时写入原始语言字段和 `zh/en` 翻译结果。
5. 如果某个值为空，不应写入伪造内容；应在上游清洗后决定是否跳过该字段。

### 核心索引语言字段

这类字段的目标是保证所有商品都至少能被中文和英文检索到。无论商品原始语言是什么，都应通过翻译或标准化得到 `zh` 和 `en` 两份结果。

典型字段：

- `qanchors`
- `enriched_tags`
- `option1_values`
- `option2_values`
- `option3_values`
- `enriched_attributes.value`
- `enriched_taxonomy_attributes.value`
- `specifications.value_text`

以 `category_path` 和 `option*_values` 为例，核心语言灌入结果应至少包含：

- `category_path.zh`
- `category_path.en`
- `option1_values.zh`
- `option1_values.en`
- `option2_values.zh`
- `option2_values.en`
- `option3_values.zh`
- `option3_values.en`

示例：原始商品语言为俄语，原始 `option1_values` 为 `красный, синий`

```json
{
  "option1_values": {
    "zh": "红色, 蓝色",
    "en": "red, blue"
  }
}
```

示例：原始商品语言为俄语，类目路径为 `Одежда > Женская одежда > Куртки`

```json
{
  "category_path": {
    "zh": "服饰 > 女装 > 夹克",
    "en": "Apparel > Women's Clothing > Jackets",
    "ru": "Одежда > Женская одежда > Куртки"
  }
}
```

注意：`category_path` 在 mapping 上属于全语言字段，但在灌入规范上依然要求 `zh/en` 必填。

### 全语言字段

这类字段既要保证 `zh/en` 两个核心索引语言可用，也要尽量保留商品原始语言，以便原语种召回和更自然的检索。

典型字段：

- `title`
- `keywords`
- `brief`
- `description`
- `vendor`
- `category_path`
- `category_name_text`

灌入规则：

1. 找到商品原始语言，例如 `ru`
2. 原文写入对应语言字段，例如 `title.ru`
3. 将原文翻译成 `zh` 和 `en`
4. 分别写入 `title.zh` 和 `title.en`

示例：原始商品语言为俄语，标题为 `Женская зимняя куртка`

```json
{
  "title": {
    "zh": "女士冬季夹克",
    "en": "Women's winter jacket",
    "ru": "Женская зимняя куртка"
  }
}
```

示例：原始商品语言为俄语，类目名称为 `Женские куртки`

```json
{
  "category_name_text": {
    "zh": "女式夹克",
    "en": "Women's jackets",
    "ru": "Женские куртки"
  }
}
```

示例：规格值 `specifications.value_text` / `specifications.value_keyword`

```json
{
  "specifications": [
    {
      "sku_id": "sku-red-s",
      "name": "color",
      "value_keyword": "красный",
      "value_text": {
        "zh": "红色",
        "en": "red"
      }
    }
  ]
}
```

其中：

- `specifications.value_keyword` 保存原始规格值，用于精确过滤 / 分面
- `specifications.value_text` 保存 `zh/en` 两个核心索引语言版本，用于检索召回

### 原始语言为中文或英文时

如果原始语言就是核心索引语言之一，不需要额外再写第三份语言字段。

示例：原始语言为中文

```json
{
  "title": {
    "zh": "女士冬季夹克",
    "en": "Women's winter jacket"
  },
  "option1_values": {
    "zh": "红色, 蓝色",
    "en": "red, blue"
  }
}
```

示例：原始语言为英文

```json
{
  "title": {
    "zh": "女士冬季夹克",
    "en": "Women's winter jacket"
  },
  "vendor": {
    "zh": "北境服饰",
    "en": "Northern Apparel"
  }
}
```

### 不同字段的灌入方式

可以按下面的方式理解和实现：

- 标量字段：直接写固定值，例如 `tenant_id`、`spu_id`、`min_price`
- 核心索引语言字段：只生成 `zh/en`
- 全语言字段：生成 `zh/en`，再按原始语言补一个对应语种字段
- 嵌套字段：对每个元素内部重复应用同样规则，例如 `specifications[].value_text`、`enriched_attributes[].value`

### 推荐灌入流程

1. 识别商品原始语言
2. 提取原文标题、描述、类目、规格、属性、选项值等字段
3. 生成 `zh` 和 `en` 两份核心索引语言内容
4. 对全语言字段，如果原始语言受支持，则额外写入原始语言字段
5. 组装最终 ES 文档并写入索引

## 生成 Mapping

在仓库根目录执行：

```bash
source activate.sh
python mappings/generate_search_products_mapping.py > mappings/search_products.json
```

如果只想查看输出而不覆盖文件：

```bash
source activate.sh
python mappings/generate_search_products_mapping.py
```

如果想先生成到临时文件：

```bash
source activate.sh
python mappings/generate_search_products_mapping.py > mappings/search_products.generated.json
```

## 校验 Mapping

确认当前 `search_products.json` 是否与生成规则完全一致：

```bash
source activate.sh
python mappings/generate_search_products_mapping.py --check mappings/search_products.json
```

## 创建索引

```python
from indexer.mapping_generator import load_mapping, create_index_if_not_exists
from utils.es_client import ESClient

es_client = ESClient(hosts=["http://localhost:9200"])
mapping = load_mapping()
create_index_if_not_exists(es_client, "search_products", mapping)
```

## 修改 Mapping

推荐流程：

1. 修改 `mappings/generate_search_products_mapping.py`
2. 重新生成 `mappings/search_products.json`
3. 用 `--check` 或 diff 确认变更符合预期
4. 重新创建索引并导入数据

注意：Elasticsearch 不支持直接修改已有字段的 mapping 类型，只能新增字段。如需修改字段类型，需要：

1. 删除旧索引
2. 使用新 mapping 创建索引
3. 重新导入数据

## 字段说明

参考 `docs/索引字段说明v2-mapping结构.md`

