# 搜索API接口对接指南

本文档为搜索服务的使用方提供完整的API对接指南，包括接口说明、请求参数、响应格式和使用示例。

## 目录

1. [快速开始](#快速开始)
   - 1.1 [基础信息](#11-基础信息)
   - 1.2 [最简单的搜索请求](#12-最简单的搜索请求)
   - 1.3 [带过滤与分页的搜索](#13-带过滤与分页的搜索)
   - 1.4 [开启分面的搜索](#14-开启分面的搜索)

2. [接口概览](#接口概览)

3. [搜索接口](#搜索接口)
   - 3.1 [接口信息](#31-接口信息)
   - 3.2 [请求参数](#32-请求参数)
   - 3.3 [过滤器详解](#33-过滤器详解)
   - 3.4 [分面配置](#34-分面配置)
   - 3.5 [SKU筛选维度](#35-sku筛选维度)
   - 3.6 [布尔表达式语法](#36-布尔表达式语法)
   - 3.7 [搜索建议接口](#37-搜索建议接口)
   - 3.8 [即时搜索接口](#38-即时搜索接口)
   - 3.9 [获取单个文档](#39-获取单个文档)

4. [响应格式说明](#响应格式说明)
   - 4.1 [标准响应结构](#41-标准响应结构)
   - 4.2 [响应字段说明](#42-响应字段说明)
   - 4.3 [SpuResult字段说明](#43-spuresult字段说明)
   - 4.4 [SkuResult字段说明](#44-skuresult字段说明)
   - 4.5 [多语言字段说明](#45-多语言字段说明)

5. [索引接口](#索引接口)
   - 5.1 [全量重建索引接口](#51-全量重建索引接口)
   - 5.2 [增量索引接口](#52-增量索引接口)
   - 5.3 [查询文档接口](#53-查询文档接口)
   - 5.4 [索引健康检查接口](#54-索引健康检查接口)

6. [管理接口](#管理接口)
   - 6.1 [健康检查](#61-健康检查)
   - 6.2 [获取配置](#62-获取配置)
   - 6.3 [索引统计](#63-索引统计)

7. [常见场景示例](#常见场景示例)
   - 7.1 [基础搜索与排序](#71-基础搜索与排序)
   - 7.2 [过滤搜索](#72-过滤搜索)
   - 7.3 [分面搜索](#73-分面搜索)
   - 7.4 [规格过滤与分面](#74-规格过滤与分面)
   - 7.5 [SKU筛选](#75-sku筛选)
   - 7.6 [布尔表达式搜索](#76-布尔表达式搜索)
   - 7.7 [分页查询](#77-分页查询)

8. [数据模型](#数据模型)
   - 8.1 [商品字段定义](#81-商品字段定义)
   - 8.2 [字段类型速查](#82-字段类型速查)
   - 8.3 [常用字段列表](#83-常用字段列表)
   - 8.4 [支持的分析器](#84-支持的分析器)

---

## 快速开始

### 1.1 基础信息

- **Base URL**: `http://your-domain:6002` 或 `http://120.76.41.98:6002`
- **协议**: HTTP/HTTPS
- **数据格式**: JSON
- **字符编码**: UTF-8
- **请求方法**: POST（搜索接口）

**重要提示**: `tenant_id` 通过 HTTP Header `X-Tenant-ID` 传递，不在请求体中。

### 1.2 最简单的搜索请求

```bash
curl -X POST "http://120.76.41.98:6002/search/" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: 162" \
  -d '{"query": "芭比娃娃"}'
```

### 1.3 带过滤与分页的搜索

```bash
curl -X POST "http://120.76.41.98:6002/search/" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: 162" \
  -d '{
    "query": "芭比娃娃",
    "size": 5,
    "from": 10,
    "range_filters": {
      "min_price": {
        "gte": 50,
        "lte": 200
      },
      "create_time": {
        "gte": "2020-01-01T00:00:00Z" 
      }
    },
    "sort_by": "price",
    "sort_order": "asc"
  }'
```

### 1.4 开启分面的搜索

```bash
curl -X POST "http://120.76.41.98:6002/search/" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: 162" \
  -d '{
    "query": "芭比娃娃",
    "facets": [
      {"field": "category1_name", "size": 10, "type": "terms"},
      {"field": "specifications.color", "size": 10, "type": "terms"},
      {"field": "specifications.size", "size": 10, "type": "terms"}
    ],
    "min_score": 0.2
  }'
```

---

## 接口概览

| 接口 | HTTP Method | Endpoint | 说明 |
|------|------|------|------|
| 搜索 | POST | `/search/` | 执行搜索查询 |
| 搜索建议 | GET | `/search/suggestions` | 搜索建议（框架，暂未实现） ⚠️ TODO |
| 即时搜索 | GET | `/search/instant` | 边输入边搜索（框架） ⚠️ TODO |
| 获取文档 | GET | `/search/{doc_id}` | 获取单个文档 |
| 全量重建索引 | POST | `/indexer/reindex` | 全量重建索引接口 |
| 增量索引 | POST | `/indexer/index` | 增量索引接口（指定SPU ID列表进行索引，支持自动检测删除和显式删除） |
| 查询文档 | POST | `/indexer/documents` | 查询SPU文档数据（不写入ES） |
| 索引健康检查 | GET | `/indexer/health` | 检查索引服务状态 |
| 健康检查 | GET | `/admin/health` | 服务健康检查 |
| 获取配置 | GET | `/admin/config` | 获取租户配置 |
| 索引统计 | GET | `/admin/stats` | 获取索引统计信息 |

---

## 搜索接口

### 3.1 接口信息

- **端点**: `POST /search/`
- **描述**: 执行文本搜索查询，支持多语言、布尔表达式、过滤器和分面搜索

### 3.2 请求参数

#### 完整请求体结构

```json
{
  "query": "string (required)",
  "size": 10,
  "from": 0,
  "language": "zh",
  "filters": {},
  "range_filters": {},
  "facets": [],
  "sort_by": "string",
  "sort_order": "desc",
  "min_score": 0.0,
  "sku_filter_dimension": ["string"],
  "debug": false,
  "user_id": "string",
  "session_id": "string"
}
```

#### 参数详细说明

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `query` | string | Y | - | 搜索查询字符串，支持布尔表达式（AND, OR, RANK, ANDNOT） |
| `size` | integer | N | 10 | 返回结果数量（1-100） |
| `from` | integer | N | 0 | 分页偏移量（用于分页） |
| `language` | string | N | "zh" | 返回语言：`zh`（中文）或 `en`（英文）。后端会根据此参数选择对应的中英文字段返回 |
| `filters` | object | N | null | 精确匹配过滤器（见[过滤器详解](#33-过滤器详解)） |
| `range_filters` | object | N | null | 数值范围过滤器（见[过滤器详解](#33-过滤器详解)） |
| `facets` | array | N | null | 分面配置（见[分面配置](#34-分面配置)） |
| `sort_by` | string | N | null | 排序字段名。支持：`price`（价格）、`sales`（销量）、`create_time`（创建时间）、`update_time`（更新时间）。默认按相关性排序 |
| `sort_order` | string | N | "desc" | 排序方向：`asc`（升序）或 `desc`（降序）。注意：`price`+`asc`=价格从低到高，`price`+`desc`=价格从高到低（后端自动映射为min_price或max_price） |
| `min_score` | float | N | null | 最小相关性分数阈值 |
| `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表（见[SKU筛选维度](#35-sku筛选维度)） |
| `debug` | boolean | N | false | 是否返回调试信息 |
| `user_id` | string | N | null | 用户ID（用于个性化，预留） |
| `session_id` | string | N | null | 会话ID（用于分析，预留） |

### 3.3 过滤器详解

#### 3.3.1 精确匹配过滤器 (filters)

用于精确匹配或多值匹配。对于普通字段，数组表示 OR 逻辑（匹配任意一个值）；对于 specifications 字段，按维度分组处理。

**格式**:
```json
{
  "filters": {
    "category_name": "手机",                      // 可以为单值 或者 数组 匹配数组中任意一个
    "category1_name": "服装",                    // 可以为单值 或者 数组 匹配数组中任意一个
    "category2_name": "男装",                    // 可以为单值 或者 数组 匹配数组中任意一个
    "category3_name": "衬衫",                    // 可以为单值 或者 数组 匹配数组中任意一个
    "vendor_zh.keyword": ["奇乐", "品牌A"],      // 可以为单值 或者 数组 匹配数组中任意一个
    "tags": "手机",                              // 可以为单值 或者 数组 匹配数组中任意一个
    // specifications 嵌套过滤（特殊格式）
    "specifications": {
      "name": "color",
      "value": "white"
    }
  }
}
```

**支持的值类型**:
- 字符串：精确匹配
- 整数：精确匹配
- 布尔值：精确匹配
- 数组：匹配任意值（OR 逻辑）
- 对象：specifications 嵌套过滤（见下文）

**Specifications 嵌套过滤**:

`specifications` 是嵌套字段，支持按规格名称和值进行过滤。

**单个规格过滤**:
```json
{
  "filters": {
    "specifications": {
      "name": "color",
      "value": "white"
    }
  }
}
```
查询规格名称为"color"且值为"white"的商品。

**多个规格过滤（按维度分组）**:
```json
{
  "filters": {
    "specifications": [
      {"name": "color", "value": "white"},
      {"name": "size", "value": "256GB"}
    ]
  }
}
```
查询同时满足所有规格的商品（color=white **且** size=256GB）。

**相同维度的多个值（OR 逻辑）**:
```json
{
  "filters": {
    "specifications": [
      {"name": "size", "value": "3"},
      {"name": "size", "value": "4"},
      {"name": "size", "value": "5"},
      {"name": "color", "value": "green"}
    ]
  }
}
```
查询满足 (size=3 **或** size=4 **或** size=5) **且** color=green 的商品。

**过滤逻辑说明**:
- **不同维度**（不同的 `name`）之间是 **AND** 关系（求交集）
- **相同维度**（相同的 `name`）的多个值之间是 **OR** 关系（求并集）

**常用过滤字段**（详见[常用字段列表](#83-常用字段列表)）:
- `category_name`: 类目名称
- `category1_name`, `category2_name`, `category3_name`: 多级类目
- `category_id`: 类目ID
- `vendor_zh.keyword`, `vendor_en.keyword`: 供应商/品牌（使用keyword子字段）
- `tags`: 标签（keyword类型，支持数组）
- `option1_name`, `option2_name`, `option3_name`: 选项名称
- `specifications`: 规格过滤（嵌套字段，格式见上文）

#### 3.3.2 范围过滤器 (range_filters)

用于数值字段的范围过滤。

**格式**:
```json
{
  "range_filters": {
    "min_price": {
      "gte": 50,    // 大于等于
      "lte": 200    // 小于等于
    },
    "max_price": {
      "gt": 100     // 大于
    },
    "create_time": {
      "gte": "2024-01-01T00:00:00Z"  // 日期时间字符串
    }
  }
}
```

**支持的操作符**:
- `gte`: 大于等于 (>=)
- `gt`: 大于 (>)
- `lte`: 小于等于 (<=)
- `lt`: 小于 (<)

**注意**: 至少需要指定一个操作符。

**常用范围字段**（详见[常用字段列表](#83-常用字段列表)）:
- `min_price`: 最低价格
- `max_price`: 最高价格
- `compare_at_price`: 原价
- `create_time`: 创建时间
- `update_time`: 更新时间

### 3.4 分面配置

用于生成分面统计（分组聚合），常用于构建筛选器UI。

#### 3.4.1 配置格式

```json
{
  "facets": [
    {
      "field": "category1_name",
      "size": 15,
      "type": "terms",
      "disjunctive": false
    },
    {
      "field": "brand_name",
      "size": 10,
      "type": "terms",
      "disjunctive": true
    },
    {
      "field": "specifications.color",
      "size": 20,
      "type": "terms",
      "disjunctive": true
    },
    {
      "field": "min_price",
      "type": "range",
      "ranges": [
        {"key": "0-50", "to": 50},
        {"key": "50-100", "from": 50, "to": 100},
        {"key": "100-200", "from": 100, "to": 200},
        {"key": "200+", "from": 200}
      ]
    }
  ]
}
```

#### 3.4.2 Facet 字段说明

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `field` | string | 是 | - | 分面字段名 |
| `size` | int | 否 | 10 | 返回的分面值数量（1-100） |
| `type` | string | 否 | "terms" | 分面类型：`terms`（词条聚合）或 `range`（范围聚合） |
| `disjunctive` | bool | 否 | false | **是否支持多选模式**（见下文详细说明） |
| `ranges` | array | 否 | null | 范围配置（仅 `type="range"` 时需要） |

#### 3.4.3 Multi-Select Faceting（多选分面）

**重要特性**: `disjunctive` 字段控制分面的行为模式。

**标准模式 (disjunctive: false)**:
- **行为**: 选中某个分面值后，该分面只显示选中的值
- **适用场景**: 层级类目、互斥选择
- **示例**: 类目下钻（玩具 > 娃娃 > 芭比）

**Multi-Select 模式 (disjunctive: true)** ⭐:
- **行为**: 选中某个分面值后，该分面仍显示所有可选项
- **适用场景**: 颜色、品牌、尺码等可切换属性
- **示例**: 选择了"红色"后，仍能看到"蓝色"、"绿色"等选项

**推荐配置**:

| 分面类型 | disjunctive | 原因 |
|---------|-------------|------|
| 颜色 | `true` | 用户需要切换颜色 |
| 品牌 | `true` | 用户需要比较品牌 |
| 尺码 | `true` | 用户需要查看其他尺码 |
| 类目 | `false` | 层级下钻 |
| 价格区间 | `false` | 互斥选择 |

#### 3.4.4 规格分面说明

`specifications` 是嵌套字段，支持两种分面模式：

**模式1：所有规格名称的分面**:
```json
{
  "facets": [
    {
      "field": "specifications",
      "size": 10,
      "type": "terms"
    }
  ]
}
```
返回所有规格名称（name）及其对应的值（value）列表。每个 name 会生成一个独立的分面结果。

**模式2：指定规格名称的分面**:
```json
{
  "facets": [
    {
      "field": "specifications.color",
      "size": 20,
      "type": "terms",
      "disjunctive": true
    },
    {
      "field": "specifications.size",
      "size": 15,
      "type": "terms",
      "disjunctive": true
    }
  ]
}
```
只返回指定规格名称的值列表。格式：`specifications.{name}`，其中 `{name}` 是规格名称（如"color"、"size"、"material"）。

**返回格式示例**:
```json
{
  "facets": [
    {
      "field": "specifications.color",
      "label": "color",
      "type": "terms",
      "values": [
        {"value": "white", "count": 50, "selected": true},  // ✓ selected 字段由后端标记
        {"value": "black", "count": 30, "selected": false},
        {"value": "red", "count": 20, "selected": false}
      ]
    },
    {
      "field": "specifications.size",
      "label": "size",
      "type": "terms",
      "values": [
        {"value": "256GB", "count": 40, "selected": false},
        {"value": "512GB", "count": 20, "selected": false}
      ]
    }
  ]
}
```

### 3.5 SKU筛选维度

**功能说明**:
`sku_filter_dimension` 用于控制搜索列表页中 **每个 SPU 下方可切换的子款式（子 SKU）维度**，为字符串列表。  
在店铺的 **主题装修配置** 中，商家可以为店铺设置一个或多个子款式筛选维度（例如 `color`、`size`），前端列表页会在每个 SPU 下展示这些维度对应的子 SKU 列表，用户可以通过点击不同维度值（如不同颜色）来切换展示的子款式。  
当指定 `sku_filter_dimension` 后，后端会根据店铺的这项配置，从所有 SKU 中筛选出这些维度组合对应的子 SKU 数据：系统会按指定维度**组合**对 SKU 进行分组，每个维度组合只返回第一个 SKU（从简实现，选择该组合下的第一款），其余不在这些维度组合中的子 SKU 将不返回。

**支持的维度值**:
1. **直接选项字段**: `option1`、`option2`、`option3`
   - 直接使用对应的 `option1_value`、`option2_value`、`option3_value` 字段进行分组
   
2. **规格/选项名称**: 通过 `option1_name`、`option2_name`、`option3_name` 匹配
   - 例如：如果 `option1_name` 为 `"color"`，则可以使用 `sku_filter_dimension: ["color"]` 来按颜色分组

**示例**:

**按颜色筛选（假设 option1_name = "color"）**:
```json
{
  "query": "芭比娃娃",
  "sku_filter_dimension": ["color"]
}
```

**按选项1筛选**:
```json
{
  "query": "芭比娃娃",
  "sku_filter_dimension": ["option1"]
}
```

**按颜色 + 尺寸组合筛选（假设 option1_name = "color", option2_name = "size"）**:
```json
{
  "query": "芭比娃娃",
  "sku_filter_dimension": ["color", "size"]
}
```

### 3.6 布尔表达式语法

搜索查询支持布尔表达式，提供更灵活的搜索能力。

**支持的操作符**:

| 操作符 | 描述 | 示例 |
|--------|------|------|
| `AND` | 所有词必须匹配 | `玩具 AND 乐高` |
| `OR` | 任意词匹配 | `芭比 OR 娃娃` |
| `ANDNOT` | 排除特定词 | `玩具 ANDNOT 电动` |
| `RANK` | 排序加权（不强制匹配） | `玩具 RANK 乐高` |
| `()` | 分组 | `玩具 AND (乐高 OR 芭比)` |

**操作符优先级**（从高到低）:
1. `()` - 括号
2. `ANDNOT` - 排除
3. `AND` - 与
4. `OR` - 或
5. `RANK` - 排序

**示例**:
```
"芭比娃娃"                    // 简单查询
"玩具 AND 乐高"               // AND 查询
"芭比 OR 娃娃"                // OR 查询
"玩具 ANDNOT 电动"            // 排除查询
"玩具 AND (乐高 OR 芭比)"      // 复杂查询
```

### 3.7 搜索建议接口

> ⚠️ **TODO**: 此接口当前为框架实现，功能暂未实现，仅返回空结果。接口和响应格式已经固定，可平滑扩展。

- **端点**: `GET /search/suggestions`
- **描述**: 返回搜索建议（自动补全/热词）。当前为框架实现，接口和响应格式已经固定，可平滑扩展。

#### 查询参数

| 参数 | 类型 | 必填 | 默认值 | 描述 |
|------|------|------|--------|------|
| `q` | string | Y | - | 查询字符串（至少 1 个字符） |
| `size` | integer | N | 5 | 返回建议数量（1-20） |
| `types` | string | N | `query` | 建议类型（逗号分隔）：`query`, `product`, `category`, `brand` |

#### 响应示例

```json
{
  "query": "芭",
  "suggestions": [
    {
      "text": "芭比娃娃",
      "type": "query",
      "highlight": "<em>芭</em>比娃娃",
      "popularity": 850
    }
  ],
  "took_ms": 5
}
```

#### 请求示例

```bash
curl "http://localhost:6002/search/suggestions?q=芭&size=5&types=query,product"
```

### 3.8 即时搜索接口

> ⚠️ **TODO**: 此接口当前为框架实现，功能暂未实现，调用标准搜索接口。后续需要优化即时搜索性能（添加防抖/节流、实现结果缓存、简化返回字段）。

- **端点**: `GET /search/instant`
- **描述**: 边输入边搜索，采用轻量参数响应当前输入。底层复用标准搜索能力。

#### 查询参数

| 参数 | 类型 | 必填 | 默认值 | 描述 |
|------|------|------|--------|------|
| `q` | string | Y | - | 搜索查询（至少 2 个字符） |
| `size` | integer | N | 5 | 返回结果数量（1-20） |

#### 请求示例

```bash
curl "http://localhost:6002/search/instant?q=玩具&size=5"
```

### 3.9 获取单个文档

- **端点**: `GET /search/{doc_id}`
- **描述**: 根据文档 ID 获取单个商品详情，用于点击结果后的详情页或排查问题。

#### 路径参数

| 参数 | 类型 | 描述 |
|------|------|------|
| `doc_id` | string | 商品或文档 ID |

#### 响应示例

```json
{
  "id": "12345",
  "source": {
    "title_zh": "芭比时尚娃娃",
    "min_price": 89.99,
    "category1_name": "玩具"
  }
}
```

#### 请求示例

```bash
curl "http://localhost:6002/search/12345"
```

---

## 响应格式说明

### 4.1 标准响应结构

```json
{
  "results": [
    {
      "spu_id": "12345",
      "title": "芭比时尚娃娃",
      "brief": "高品质芭比娃娃",
      "description": "详细描述...",
      "vendor": "美泰",
      "category": "玩具",
      "category_path": "玩具/娃娃/时尚",
      "category_name": "时尚",
      "category_id": "cat_001",
      "category_level": 3,
      "category1_name": "玩具",
      "category2_name": "娃娃",
      "category3_name": "时尚",
      "tags": ["娃娃", "玩具", "女孩"],
      "price": 89.99,
      "compare_at_price": 129.99,
      "currency": "USD",
      "image_url": "https://example.com/image.jpg",
      "in_stock": true,
      "sku_prices": [89.99, 99.99, 109.99],
      "sku_weights": [100, 150, 200],
      "sku_weight_units": ["g", "g", "g"],
      "total_inventory": 500,
      "option1_name": "color",
      "option2_name": "size",
      "option3_name": null,
      "specifications": [
        {"sku_id": "sku_001", "name": "color", "value": "pink"},
        {"sku_id": "sku_001", "name": "size", "value": "standard"}
      ],
      "skus": [
        {
          "sku_id": "67890",
          "price": 89.99,
          "compare_at_price": 129.99,
          "sku": "BARBIE-001",
          "stock": 100,
          "weight": 0.1,
          "weight_unit": "kg",
          "option1_value": "pink",
          "option2_value": "standard",
          "option3_value": null,
          "image_src": "https://example.com/sku1.jpg"
        }
      ],
      "relevance_score": 8.5
    }
  ],
  "total": 118,
  "max_score": 8.5,
  "facets": [
    {
      "field": "category1_name",
      "label": "category1_name",
      "type": "terms",
      "values": [
        {
          "value": "玩具",
          "label": "玩具",
          "count": 85,
          "selected": false
        }
      ]
    },
    {
      "field": "specifications.color",
      "label": "color",
      "type": "terms",
      "values": [
        {
          "value": "pink",
          "label": "pink",
          "count": 30,
          "selected": false
        }
      ]
    }
  ],
  "query_info": {
    "original_query": "芭比娃娃",
    "detected_language": "zh",
    "translations": {
      "en": "barbie doll"
    }
  },
  "suggestions": [],
  "related_searches": [],
  "took_ms": 45,
  "performance_info": null,
  "debug_info": null
}
```

### 4.2 响应字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `results` | array | 搜索结果列表（SpuResult对象数组） |
| `results[].spu_id` | string | SPU ID |
| `results[].title` | string | 商品标题 |
| `results[].price` | float | 价格（min_price） |
| `results[].skus` | array | SKU列表（如果指定了`sku_filter_dimension`，则按维度过滤后的SKU） |
| `results[].relevance_score` | float | 相关性分数 |
| `total` | integer | 匹配的总文档数 |
| `max_score` | float | 最高相关性分数 |
| `facets` | array | 分面统计结果 |
| `query_info` | object | 查询处理信息 |
| `took_ms` | integer | 搜索耗时（毫秒） |

### 4.3 SpuResult字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `spu_id` | string | SPU ID |
| `title` | string | 商品标题（根据language参数自动选择title_zh或title_en） |
| `brief` | string | 商品短描述（根据language参数自动选择） |
| `description` | string | 商品详细描述（根据language参数自动选择） |
| `vendor` | string | 供应商/品牌（根据language参数自动选择） |
| `category` | string | 类目（兼容字段，等同于category_name） |
| `category_path` | string | 类目路径（多级，用于面包屑，根据language参数自动选择） |
| `category_name` | string | 类目名称（展示用，根据language参数自动选择） |
| `category_id` | string | 类目ID |
| `category_level` | integer | 类目层级（1/2/3） |
| `category1_name` | string | 一级类目名称 |
| `category2_name` | string | 二级类目名称 |
| `category3_name` | string | 三级类目名称 |
| `tags` | array[string] | 标签列表 |
| `price` | float | 价格（min_price） |
| `compare_at_price` | float | 原价 |
| `currency` | string | 货币单位（默认USD） |
| `image_url` | string | 主图URL |
| `in_stock` | boolean | 是否有库存（任意SKU有库存即为true） |
| `sku_prices` | array[float] | 所有SKU价格列表 |
| `sku_weights` | array[integer] | 所有SKU重量列表 |
| `sku_weight_units` | array[string] | 所有SKU重量单位列表 |
| `total_inventory` | integer | 总库存 |
| `sales` | integer | 销量（展示销量） |
| `option1_name` | string | 选项1名称（如"color"） |
| `option2_name` | string | 选项2名称（如"size"） |
| `option3_name` | string | 选项3名称 |
| `specifications` | array[object] | 规格列表（与ES specifications字段对应） |
| `skus` | array | SKU 列表 |
| `relevance_score` | float | 相关性分数 |

### 4.4 SkuResult字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `sku_id` | string | SKU ID |
| `price` | float | 价格 |
| `compare_at_price` | float | 原价 |
| `sku` | string | SKU编码（sku_code） |
| `stock` | integer | 库存数量 |
| `weight` | float | 重量 |
| `weight_unit` | string | 重量单位 |
| `option1_value` | string | 选项1取值（如color值） |
| `option2_value` | string | 选项2取值（如size值） |
| `option3_value` | string | 选项3取值 |
| `image_src` | string | SKU图片地址 |

### 4.5 多语言字段说明

- `title`, `brief`, `description`, `vendor`, `category_path`, `category_name` 会根据请求的 `language` 参数自动选择对应的中英文字段
- `language="zh"`: 优先返回 `*_zh` 字段，如果为空则回退到 `*_en` 字段
- `language="en"`: 优先返回 `*_en` 字段，如果为空则回退到 `*_zh` 字段

---

## 索引接口

### 5.1 全量重建索引接口

- **端点**: `POST /indexer/reindex`
- **描述**: 全量重建索引，将指定租户的所有SPU数据导入到ES索引

#### 请求参数

```json
{
  "tenant_id": "162",
  "recreate_index": false,
  "batch_size": 500
}
```

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `tenant_id` | string | Y | - | 租户ID |
| `recreate_index` | boolean | N | false | 是否重建索引（删除旧索引后创建新索引） |
| `batch_size` | integer | N | 500 | 批量导入大小 |

#### 响应格式

**成功响应（200 OK）**:
```json
{
  "success": true,
  "total": 1000,
  "indexed": 1000,
  "failed": 0,
  "elapsed_time": 12.34,
  "index_name": "search_products",
  "tenant_id": "162"
}
```

**错误响应**:
- `400 Bad Request`: 参数错误
- `503 Service Unavailable`: 服务未初始化

#### 请求示例

**首次索引（重建索引）**:
```bash
curl -X POST "http://localhost:6002/indexer/reindex" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "recreate_index": true,
    "batch_size": 500
  }'
```

**查看日志**:
```bash
# 查看API日志（包含索引操作日志）
tail -f logs/api.log

# 或者查看所有日志文件
tail -f logs/*.log
```

**增量更新（不重建索引）**:
```bash
curl -X POST "http://localhost:6002/indexer/reindex" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "recreate_index": false,
    "batch_size": 500
  }'
```

**查看索引日志**:

索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中（JSON 格式），包括：
- 请求开始和结束时间
- 租户ID、SPU ID、操作类型
- 每个SPU的处理状态
- ES批量写入结果
- 成功/失败统计和详细错误信息

```bash
# 实时查看索引日志（包含全量和增量索引的所有操作）
tail -f logs/indexer.log

# 使用 grep 查询（简单方式）
# 查看全量索引日志
grep "\"index_type\":\"bulk\"" logs/indexer.log | tail -100

# 查看增量索引日志
grep "\"index_type\":\"incremental\"" logs/indexer.log | tail -100

# 查看特定租户的索引日志
grep "\"tenant_id\":\"162\"" logs/indexer.log | tail -100

# 使用 jq 查询（推荐，更精确的 JSON 查询）
# 安装 jq: sudo apt-get install jq 或 brew install jq

# 查看全量索引日志
cat logs/indexer.log | jq 'select(.index_type == "bulk")' | tail -100

# 查看增量索引日志
cat logs/indexer.log | jq 'select(.index_type == "incremental")' | tail -100

# 查看特定租户的索引日志
cat logs/indexer.log | jq 'select(.tenant_id == "162")' | tail -100

# 查看失败的索引操作
cat logs/indexer.log | jq 'select(.operation == "request_complete" and .failed_count > 0)'

# 查看特定SPU的处理日志
cat logs/indexer.log | jq 'select(.spu_id == "123")'

# 查看最近的索引请求统计
cat logs/indexer.log | jq 'select(.operation == "request_complete") | {timestamp, index_type, tenant_id, total_count, success_count, failed_count, elapsed_time}'
```

### 5.2 增量索引接口

- **端点**: `POST /indexer/index`
- **描述**: 增量索引接口，根据指定的SPU ID列表进行索引，直接将数据写入ES。用于增量更新指定商品。

**删除说明**：
- `spu_ids`中的SPU：如果数据库`deleted=1`，自动从ES删除，响应状态为`deleted`
- `delete_spu_ids`中的SPU：直接删除，响应状态为`deleted`、`not_found`或`failed`

#### 请求参数

```json
{
  "tenant_id": "162",
  "spu_ids": ["123", "456", "789"],
  "delete_spu_ids": ["100", "101"]
}
```

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `tenant_id` | string | Y | 租户ID |
| `spu_ids` | array[string] | N | SPU ID列表（1-100个），要索引的SPU。如果为空，则只执行删除操作 |
| `delete_spu_ids` | array[string] | N | 显式指定要删除的SPU ID列表（1-100个），可选。无论数据库状态如何，都会从ES中删除这些SPU |

**注意**：
- `spu_ids` 和 `delete_spu_ids` 不能同时为空
- 每个列表最多支持100个SPU ID
- 如果SPU在`spu_ids`中且数据库`deleted=1`，会自动从ES删除（自动检测删除）

#### 响应格式

```json
{
  "spu_ids": [
    {
      "spu_id": "123",
      "status": "indexed"
    },
    {
      "spu_id": "456",
      "status": "deleted"
    },
    {
      "spu_id": "789",
      "status": "failed",
      "msg": "SPU not found (unexpected)"
    }
  ],
  "delete_spu_ids": [
    {
      "spu_id": "100",
      "status": "deleted"
    },
    {
      "spu_id": "101",
      "status": "not_found"
    },
    {
      "spu_id": "102",
      "status": "failed",
      "msg": "Failed to delete from ES: Connection timeout"
    }
  ],
  "total": 6,
  "success_count": 4,
  "failed_count": 2,
  "elapsed_time": 1.23,
  "index_name": "search_products",
  "tenant_id": "162"
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `spu_ids` | array | spu_ids对应的响应列表，每个元素包含 `spu_id` 和 `status` |
| `spu_ids[].status` | string | 状态：`indexed`（已索引）、`deleted`（已删除，自动检测）、`failed`（失败） |
| `spu_ids[].msg` | string | 当status为`failed`时，包含失败原因（可选） |
| `delete_spu_ids` | array | delete_spu_ids对应的响应列表，每个元素包含 `spu_id` 和 `status` |
| `delete_spu_ids[].status` | string | 状态：`deleted`（已删除）、`not_found`（ES中不存在）、`failed`（失败） |
| `delete_spu_ids[].msg` | string | 当status为`failed`时，包含失败原因（可选） |
| `total` | integer | 总处理数量（spu_ids数量 + delete_spu_ids数量） |
| `success_count` | integer | 成功数量（indexed + deleted + not_found） |
| `failed_count` | integer | 失败数量 |
| `elapsed_time` | float | 耗时（秒） |
| `index_name` | string | 索引名称 |
| `tenant_id` | string | 租户ID |

**状态说明**：
- `spu_ids` 的状态：
  - `indexed`: SPU已成功索引到ES
  - `deleted`: SPU在数据库中被标记为deleted=1，已从ES删除（自动检测）
  - `failed`: 处理失败，会包含`msg`字段说明失败原因
- `delete_spu_ids` 的状态：
  - `deleted`: SPU已从ES成功删除
  - `not_found`: SPU在ES中不存在（也算成功，可能已经被删除过）
  - `failed`: 删除失败，会包含`msg`字段说明失败原因

#### 请求示例

**示例1：普通增量索引（自动检测删除）**:
```bash
curl -X POST "http://localhost:6002/indexer/index" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "spu_ids": ["123", "456", "789"]
  }'
```
说明：如果SPU 456在数据库中`deleted=1`，会自动从ES删除，在响应中`spu_ids`列表里456的状态为`deleted`。

**示例2：显式删除（批量删除）**:
```bash
curl -X POST "http://localhost:6002/indexer/index" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "spu_ids": ["123", "456"],
    "delete_spu_ids": ["100", "101", "102"]
  }'
```
说明：SPU 100、101、102会被显式删除，无论数据库状态如何。

**示例3：仅删除（不索引）**:
```bash
curl -X POST "http://localhost:6002/indexer/index" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "spu_ids": [],
    "delete_spu_ids": ["100", "101"]
  }'
```
说明：只执行删除操作，不进行索引。

**示例4：混合操作（索引+删除）**:
```bash
curl -X POST "http://localhost:6002/indexer/index" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "spu_ids": ["123", "456", "789"],
    "delete_spu_ids": ["100", "101"]
  }'
```
说明：同时执行索引和删除操作。

#### 日志说明

增量索引操作的所有关键信息都会记录到 `logs/indexer.log` 文件中（JSON格式），包括：
- 请求开始和结束时间
- 每个SPU的处理状态（获取、转换、索引、删除）
- ES批量写入结果
- 成功/失败统计
- 详细的错误信息

日志查询方式请参考[5.1节查看索引日志](#51-全量重建索引接口)部分。

### 5.3 查询文档接口

- **端点**: `POST /indexer/documents`
- **描述**: 查询文档接口，根据SPU ID列表获取ES文档数据（**不写入ES**）。用于查看、调试或验证SPU数据。

#### 请求参数

```json
{
  "tenant_id": "162",
  "spu_ids": ["123", "456", "789"]
}
```

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `tenant_id` | string | Y | 租户ID |
| `spu_ids` | array[string] | Y | SPU ID列表（1-100个） |

#### 响应格式

```json
{
  "success": [
    {
      "spu_id": "123",
      "document": {
        "tenant_id": "162",
        "spu_id": "123",
        "title_zh": "商品标题",
        ...
      }
    },
    {
      "spu_id": "456",
      "document": {...}
    }
  ],
  "failed": [
    {
      "spu_id": "789",
      "error": "SPU not found or deleted"
    }
  ],
  "total": 3,
  "success_count": 2,
  "failed_count": 1
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `success` | array | 成功获取的SPU列表，每个元素包含 `spu_id` 和 `document`（完整的ES文档数据） |
| `failed` | array | 失败的SPU列表，每个元素包含 `spu_id` 和 `error`（失败原因） |
| `total` | integer | 总SPU数量 |
| `success_count` | integer | 成功数量 |
| `failed_count` | integer | 失败数量 |

#### 请求示例

**单个SPU查询**:
```bash
curl -X POST "http://localhost:6002/indexer/documents" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "spu_ids": ["123"]
  }'
```

**批量SPU查询**:
```bash
curl -X POST "http://localhost:6002/indexer/documents" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "162",
    "spu_ids": ["123", "456", "789"]
  }'
```

#### 与 `/indexer/index` 的区别

| 接口 | 功能 | 是否写入ES | 返回内容 |
|------|------|-----------|----------|
| `/indexer/documents` | 查询SPU文档数据 | 否 | 返回完整的ES文档数据 |
| `/indexer/index` | 增量索引 | 是 | 返回成功/失败列表和统计信息 |

**使用场景**：
- `/indexer/documents`：用于查看、调试或验证SPU数据，不修改ES索引
- `/indexer/index`：用于实际的增量索引操作，将更新的SPU数据同步到ES

### 5.4 索引健康检查接口

- **端点**: `GET /indexer/health`
- **描述**: 检查索引服务的健康状态

#### 响应格式

```json
{
  "status": "available",
  "database": "connected",
  "preloaded_data": {
    "category_mappings": 150
  }
}
```

#### 请求示例

```bash
curl -X GET "http://localhost:6002/indexer/health"
```

---

## 管理接口

### 6.1 健康检查

- **端点**: `GET /admin/health`
- **描述**: 检查服务与依赖（如 Elasticsearch）状态。

```json
{
  "status": "healthy",
  "elasticsearch": "connected",
  "tenant_id": "tenant1"
}
```

### 6.2 获取配置

- **端点**: `GET /admin/config`
- **描述**: 返回当前租户的脱敏配置，便于核对索引及排序表达式。

```json
{
  "tenant_id": "tenant1",
  "tenant_name": "Tenant1 Test Instance",
  "es_index_name": "search_tenant1",
  "num_fields": 20,
  "num_indexes": 4,
  "supported_languages": ["zh", "en", "ru"],
  "ranking_expression": "bm25() + 0.2*text_embedding_relevance()",
  "spu_enabled": false
}
```

### 6.3 索引统计

- **端点**: `GET /admin/stats`
- **描述**: 获取索引文档数量与磁盘大小，方便监控。

```json
{
  "index_name": "search_tenant1",
  "document_count": 10000,
  "size_mb": 523.45
}
```

---

## 常见场景示例

### 7.1 基础搜索与排序

**按价格从低到高排序**:
```json
{
  "query": "玩具",
  "size": 20,
  "from": 0,
  "sort_by": "price",
  "sort_order": "asc"
}
```

**按价格从高到低排序**:
```json
{
  "query": "玩具",
  "size": 20,
  "from": 0,
  "sort_by": "price",
  "sort_order": "desc"
}
```

**按销量从高到低排序**:
```json
{
  "query": "玩具",
  "size": 20,
  "from": 0,
  "sort_by": "sales",
  "sort_order": "desc"
}
```

**按默认（相关性）排序**:
```json
{
  "query": "玩具",
  "size": 20,
  "from": 0
}
```

### 7.2 过滤搜索

**需求**: 搜索"玩具"，筛选类目为"益智玩具"，价格在50-200之间

```json
{
  "query": "玩具",
  "size": 20,
  "language": "zh",
  "filters": {
    "category_name": "益智玩具"
  },
  "range_filters": {
    "min_price": {
      "gte": 50,
      "lte": 200
    }
  }
}
```

**需求**: 搜索"手机"，筛选多个品牌，价格范围

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "filters": {
    "vendor_zh.keyword": ["品牌A", "品牌B"]
  },
  "range_filters": {
    "min_price": {
      "gte": 50,
      "lte": 200
    }
  }
}
```

### 7.3 分面搜索

**需求**: 搜索"玩具"，获取类目和规格的分面统计，用于构建筛选器

```json
{
  "query": "玩具",
  "size": 20,
  "language": "zh",
  "facets": [
    {"field": "category1_name", "size": 15, "type": "terms"},
    {"field": "category2_name", "size": 10, "type": "terms"},
    {"field": "specifications", "size": 10, "type": "terms"}
  ]
}
```

**需求**: 搜索"手机"，获取价格区间和规格的分面统计

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "facets": [
    {
      "field": "min_price",
      "type": "range",
      "ranges": [
        {"key": "0-50", "to": 50},
        {"key": "50-100", "from": 50, "to": 100},
        {"key": "100-200", "from": 100, "to": 200},
        {"key": "200+", "from": 200}
      ]
    },
    {
      "field": "specifications",
      "size": 10,
      "type": "terms"
    }
  ]
}
```

### 7.4 规格过滤与分面

**需求**: 搜索"手机"，筛选color为"white"的商品

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "filters": {
    "specifications": {
      "name": "color",
      "value": "white"
    }
  }
}
```

**需求**: 搜索"手机"，筛选color为"white"且size为"256GB"的商品

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "filters": {
    "specifications": [
      {"name": "color", "value": "white"},
      {"name": "size", "value": "256GB"}
    ]
  }
}
```

**需求**: 搜索"手机"，筛选size为"3"、"4"或"5"，且color为"green"的商品

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "filters": {
    "specifications": [
      {"name": "size", "value": "3"},
      {"name": "size", "value": "4"},
      {"name": "size", "value": "5"},
      {"name": "color", "value": "green"}
    ]
  }
}
```

**需求**: 搜索"手机"，获取所有规格的分面统计

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "facets": [
    {"field": "specifications", "size": 10, "type": "terms"}
  ]
}
```

**需求**: 只获取"color"和"size"规格的分面统计

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "facets": [
    {"field": "specifications.color", "size": 20, "type": "terms"},
    {"field": "specifications.size", "size": 15, "type": "terms"}
  ]
}
```

**需求**: 搜索"手机"，筛选类目和规格，并获取对应的分面统计

```json
{
  "query": "手机",
  "size": 20,
  "language": "zh",
  "filters": {
    "category_name": "手机",
    "specifications": {
      "name": "color",
      "value": "white"
    }
  },
  "facets": [
    {"field": "category1_name", "size": 15, "type": "terms"},
    {"field": "category2_name", "size": 10, "type": "terms"},
    {"field": "specifications.color", "size": 20, "type": "terms"},
    {"field": "specifications.size", "size": 15, "type": "terms"}
  ]
}
```

### 7.5 SKU筛选

**需求**: 搜索"芭比娃娃"，每个SPU下按颜色筛选，每种颜色只显示一个SKU

```json
{
  "query": "芭比娃娃",
  "size": 20,
  "sku_filter_dimension": ["color"]
}
```

**说明**:
- 如果 `option1_name` 为 `"color"`，则使用 `sku_filter_dimension: ["color"]` 可以按颜色分组
- 每个SPU下，每种颜色只会返回第一个SKU
- 如果维度不匹配，返回所有SKU（不进行过滤）

### 7.6 布尔表达式搜索

**需求**: 搜索包含"手机"和"智能"的商品，排除"二手"

```json
{
  "query": "手机 AND 智能 ANDNOT 二手",
  "size": 20
}
```

### 7.7 分页查询

**需求**: 获取第2页结果（每页20条）

```json
{
  "query": "手机",
  "size": 20,
  "from": 20
}
```

---

## 数据模型

### 8.1 商品字段定义

| 字段名 | 类型 | 描述 |
|--------|------|------|
| `tenant_id` | keyword | 租户ID（多租户隔离） |
| `spu_id` | keyword | SPU ID |
| `title_zh`, `title_en` | text | 商品标题（中英文） |
| `brief_zh`, `brief_en` | text | 商品短描述（中英文） |
| `description_zh`, `description_en` | text | 商品详细描述（中英文） |
| `vendor_zh`, `vendor_en` | text | 供应商/品牌（中英文，含keyword子字段） |
| `category_path_zh`, `category_path_en` | text | 类目路径（中英文，用于搜索） |
| `category_name_zh`, `category_name_en` | text | 类目名称（中英文，用于搜索） |
| `category_id` | keyword | 类目ID |
| `category_name` | keyword | 类目名称（用于过滤） |
| `category_level` | integer | 类目层级 |
| `category1_name`, `category2_name`, `category3_name` | keyword | 多级类目名称（用于过滤和分面） |
| `tags` | keyword | 标签（数组） |
| `specifications` | nested | 规格（嵌套对象数组） |
| `option1_name`, `option2_name`, `option3_name` | keyword | 选项名称 |
| `min_price`, `max_price` | float | 最低/最高价格 |
| `compare_at_price` | float | 原价 |
| `sku_prices` | float | SKU价格列表（数组） |
| `sku_weights` | long | SKU重量列表（数组） |
| `sku_weight_units` | keyword | SKU重量单位列表（数组） |
| `total_inventory` | long | 总库存 |
| `sales` | long | 销量（展示销量） |
| `skus` | nested | SKU详细信息（嵌套对象数组） |
| `create_time`, `update_time` | date | 创建/更新时间 |
| `title_embedding` | dense_vector | 标题向量（1024维，仅用于搜索） |
| `image_embedding` | nested | 图片向量（嵌套，仅用于搜索） |

> 所有租户共享统一的索引结构。文本字段支持中英文双语，后端根据 `language` 参数自动选择对应字段返回。

### 8.2 字段类型速查

| 类型 | ES Mapping | 用途 |
|------|------------|------|
| `text` | `text` | 全文检索（支持中英文分析器） |
| `keyword` | `keyword` | 精确匹配、聚合、排序 |
| `integer` | `integer` | 整数 |
| `long` | `long` | 长整数 |
| `float` | `float` | 浮点数 |
| `date` | `date` | 日期时间 |
| `nested` | `nested` | 嵌套对象（specifications, skus, image_embedding） |
| `dense_vector` | `dense_vector` | 向量字段（title_embedding，仅用于搜索） |

### 8.3 常用字段列表

#### 过滤字段

- `category_name`: 类目名称
- `category1_name`, `category2_name`, `category3_name`: 多级类目
- `category_id`: 类目ID
- `vendor_zh.keyword`, `vendor_en.keyword`: 供应商/品牌（使用keyword子字段）
- `tags`: 标签（keyword类型）
- `option1_name`, `option2_name`, `option3_name`: 选项名称
- `specifications`: 规格过滤（嵌套字段，格式见[过滤器详解](#33-过滤器详解)）

#### 范围字段

- `min_price`: 最低价格
- `max_price`: 最高价格
- `compare_at_price`: 原价
- `create_time`: 创建时间
- `update_time`: 更新时间

#### 排序字段

- `price`: 价格（后端自动根据sort_order映射：asc→min_price，desc→max_price）
- `sales`: 销量
- `create_time`: 创建时间
- `update_time`: 更新时间
- `relevance_score`: 相关性分数（默认，不指定sort_by时使用）

**注意**: 前端只需传 `price`，后端会自动处理：
- `sort_by: "price"` + `sort_order: "asc"` → 按 `min_price` 升序（价格从低到高）
- `sort_by: "price"` + `sort_order: "desc"` → 按 `max_price` 降序（价格从高到低）

### 8.4 支持的分析器

| 分析器 | 语言 | 描述 |
|--------|------|------|
| `index_ansj` | 中文 | 中文索引分析器（用于中文字段） |
| `query_ansj` | 中文 | 中文查询分析器（用于中文字段） |
| `hanlp_index` ⚠️ TODO（暂不支持） | 中文 | 中文索引分析器（用于中文字段） |
| `hanlp_standard` ⚠️ TODO（暂不支持） | 中文 | 中文查询分析器（用于中文字段） |
| `english` | 英文 | 标准英文分析器（用于英文字段） |
| `lowercase` | - | 小写标准化器（用于keyword子字段） |
