<!-- 42918612-89ae-45a6-9ccb-e2b0df0a7aec 6bb705f4-0105-4c72-8a0b-a067d2e83dc0 -->
# 搜索引擎 API 接口重构实施计划

## 第一部分：现状分析

### 1. 当前实现存在的问题

#### 问题 1：硬编码的价格范围过滤

**位置**：`search/es_query_builder.py` 第 205-233 行

**问题描述**：

```python
if field == 'price_ranges':
    # 硬编码特定字符串值
    if price_range == '0-50':
        price_ranges.append({"lt": 50})
    elif price_range == '50-100':
        price_ranges.append({"gte": 50, "lt": 100})
    # ...
```

**影响**：

- 只支持 `price` 字段，无法扩展到其他数值字段
- 范围值硬编码，无法根据业务需求调整
- 不符合 SaaS 系统的通用性要求

#### 问题 2：聚合参数直接暴露 ES DSL

**位置**：

- `api/models.py` 第 17 行：`aggregations: Optional[Dict[str, Any]]`
- `search/es_query_builder.py` 第 298-319 行：`add_dynamic_aggregations`
- `frontend/static/js/app.js` 第 57-87 行：前端硬编码 ES DSL

**问题描述**：

前端需要了解 Elasticsearch 的聚合语法：

```javascript
const aggregations = {
    "category_stats": {
        "terms": {
            "field": "categoryName_keyword",
            "size": 15
        }
    },
    "price_ranges": {
        "range": {
            "field": "price",
            "ranges": [
                {"key": "0-50", "to": 50},
                // ...
            ]
        }
    }
};
```

**影响**：

- 前端需要了解 ES 语法，增加集成难度
- 不符合 SaaS 产品易用性原则
- 难以进行参数验证和文档生成

#### 问题 3：分面搜索结果格式不统一

**位置**：`frontend/static/js/app.js` 第 208-258 行

**问题描述**：

- 直接返回 ES 原始格式（`buckets` 结构）
- 前端需要知道不同聚合类型的响应结构
- 没有统一的分面结果模型

**影响**：

- 前端解析逻辑复杂
- 不同类型的聚合处理方式不一致
- 难以扩展新的聚合类型

#### 问题 4：缺少搜索建议功能

**当前状态**：完全没有实现

**需求**：

- 自动补全（Autocomplete）
- 搜索建议（Suggestions）
- 搜索即时反馈（Instant Search）

### 2. 依赖关系分析

**影响范围**：

1. **后端模型层**：`api/models.py`
2. **查询构建层**：`search/es_query_builder.py`
3. **搜索执行层**：`search/searcher.py`
4. **API 路由层**：`api/routes/search.py`
5. **前端代码**：`frontend/static/js/app.js`

## 第二部分：优化方案设计

### 方案概述

采用**结构化过滤参数方案（方案 A 的简化版）**：

- 分离 `filters`（精确匹配）和 `range_filters`（范围过滤）
- **不支持单字段多个不连续范围**，简化设计
- 标准化聚合参数，使用简化的接口
- 统一分面搜索响应格式
- **完全重构，不保留旧接口和兼容代码**

### 1. 新的请求模型设计

#### 1.1 核心模型定义

**文件**：`api/models.py`

```python
from pydantic import BaseModel, Field, field_validator
from typing import List, Dict, Any, Optional, Union, Literal


class RangeFilter(BaseModel):
    """数值范围过滤器"""
    gte: Optional[float] = Field(None, description="大于等于 (>=)")
    gt: Optional[float] = Field(None, description="大于 (>)")
    lte: Optional[float] = Field(None, description="小于等于 (<=)")
    lt: Optional[float] = Field(None, description="小于 (<)")
    
    @field_validator('*')
    def check_at_least_one(cls, v, info):
        """确保至少指定一个边界"""
        values = info.data
        if not any([values.get('gte'), values.get('gt'), 
                   values.get('lte'), values.get('lt')]):
            raise ValueError('至少需要指定一个范围边界')
        return v
    
    class Config:
        json_schema_extra = {
            "examples": [
                {"gte": 50, "lte": 200},
                {"gt": 100},
                {"lt": 50}
            ]
        }


class FacetConfig(BaseModel):
    """分面配置（简化版）"""
    field: str = Field(..., description="分面字段名")
    size: int = Field(10, ge=1, le=100, description="返回的分面值数量")
    type: Literal["terms", "range"] = Field("terms", description="分面类型")
    ranges: Optional[List[Dict[str, Any]]] = Field(
        None,
        description="范围分面的范围定义（仅当 type='range' 时需要）"
    )
    
    class Config:
        json_schema_extra = {
            "examples": [
                {
                    "field": "categoryName_keyword",
                    "size": 15,
                    "type": "terms"
                },
                {
                    "field": "price",
                    "size": 4,
                    "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}
                    ]
                }
            ]
        }


class SearchRequest(BaseModel):
    """搜索请求模型（重构版）"""
    
    # 基础搜索参数
    query: str = Field(..., description="搜索查询字符串，支持布尔表达式（AND, OR, RANK, ANDNOT）")
    size: int = Field(10, ge=1, le=100, description="返回结果数量")
    from_: int = Field(0, ge=0, alias="from", description="分页偏移量")
    
    # 过滤器 - 精确匹配和多值匹配
    filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]]]]] = Field(
        None,
        description="精确匹配过滤器。单值表示精确匹配，数组表示 OR 匹配（匹配任意一个值）",
        json_schema_extra={
            "examples": [
                {
                    "categoryName_keyword": ["玩具", "益智玩具"],
                    "brandName_keyword": "乐高",
                    "in_stock": True
                }
            ]
        }
    )
    
    # 范围过滤器 - 数值范围
    range_filters: Optional[Dict[str, RangeFilter]] = Field(
        None,
        description="数值范围过滤器。支持 gte, gt, lte, lt 操作符",
        json_schema_extra={
            "examples": [
                {
                    "price": {"gte": 50, "lte": 200},
                    "days_since_last_update": {"lte": 30}
                }
            ]
        }
    )
    
    # 排序
    sort_by: Optional[str] = Field(None, description="排序字段名（如 'price', 'create_time'）")
    sort_order: Optional[str] = Field("desc", description="排序方向: 'asc'（升序）或 'desc'（降序）")
    
    # 分面搜索 - 简化接口
    facets: Optional[List[Union[str, FacetConfig]]] = Field(
        None,
        description="分面配置。可以是字段名列表（使用默认配置）或详细的分面配置对象",
        json_schema_extra={
            "examples": [
                # 简单模式：只指定字段名，使用默认配置
                ["categoryName_keyword", "brandName_keyword"],
                # 高级模式：详细配置
                [
                    {"field": "categoryName_keyword", "size": 15},
                    {
                        "field": "price",
                        "type": "range",
                        "ranges": [
                            {"key": "0-50", "to": 50},
                            {"key": "50-100", "from": 50, "to": 100}
                        ]
                    }
                ]
            ]
        }
    )
    
    # 高级选项
    min_score: Optional[float] = Field(None, ge=0, description="最小相关性分数阈值")
    highlight: bool = Field(False, description="是否高亮搜索关键词（暂不实现）")
    debug: bool = Field(False, description="是否返回调试信息")
    
    # 个性化参数（预留）
    user_id: Optional[str] = Field(None, description="用户ID，用于个性化搜索和推荐")
    session_id: Optional[str] = Field(None, description="会话ID，用于搜索分析")


class ImageSearchRequest(BaseModel):
    """图片搜索请求模型"""
    image_url: str = Field(..., description="查询图片的 URL")
    size: int = Field(10, ge=1, le=100, description="返回结果数量")
    filters: Optional[Dict[str, Union[str, int, bool, List[Union[str, int]]]]] = None
    range_filters: Optional[Dict[str, RangeFilter]] = None


class SearchSuggestRequest(BaseModel):
    """搜索建议请求模型（框架，暂不实现）"""
    query: str = Field(..., min_length=1, description="搜索查询字符串")
    size: int = Field(5, ge=1, le=20, description="返回建议数量")
    types: List[Literal["query", "product", "category", "brand"]] = Field(
        ["query"],
        description="建议类型：query（查询建议）, product（商品建议）, category（类目建议）, brand（品牌建议）"
    )
```

#### 1.2 响应模型定义

```python
class FacetValue(BaseModel):
    """分面值"""
    value: Union[str, int, float] = Field(..., description="分面值")
    label: Optional[str] = Field(None, description="显示标签（如果与 value 不同）")
    count: int = Field(..., description="匹配的文档数量")
    selected: bool = Field(False, description="是否已选中（当前过滤器中）")


class FacetResult(BaseModel):
    """分面结果（标准化格式）"""
    field: str = Field(..., description="字段名")
    label: str = Field(..., description="分面显示名称")
    type: Literal["terms", "range"] = Field(..., description="分面类型")
    values: List[FacetValue] = Field(..., description="分面值列表")
    total_count: Optional[int] = Field(None, description="该字段的总文档数")


class SearchResponse(BaseModel):
    """搜索响应模型（重构版）"""
    
    # 核心结果
    hits: List[Dict[str, Any]] = Field(..., description="搜索结果列表")
    total: int = Field(..., description="匹配的总文档数")
    max_score: float = Field(..., description="最高相关性分数")
    
    # 分面搜索结果（标准化格式）
    facets: Optional[List[FacetResult]] = Field(
        None,
        description="分面统计结果（标准化格式）"
    )
    
    # 查询信息
    query_info: Dict[str, Any] = Field(
        default_factory=dict,
        description="查询处理信息（原始查询、改写、语言检测、翻译等）"
    )
    
    # 推荐与建议（预留）
    related_queries: Optional[List[str]] = Field(None, description="相关搜索查询")
    
    # 性能指标
    took_ms: int = Field(..., description="搜索总耗时（毫秒）")
    performance_info: Optional[Dict[str, Any]] = Field(None, description="详细性能信息")
    
    # 调试信息
    debug_info: Optional[Dict[str, Any]] = Field(None, description="调试信息（仅当 debug=True）")


class SearchSuggestResponse(BaseModel):
    """搜索建议响应模型（框架，暂不实现）"""
    query: str = Field(..., description="原始查询")
    suggestions: List[Dict[str, Any]] = Field(..., description="建议列表")
    took_ms: int = Field(..., description="耗时（毫秒）")
```

### 2. 查询构建器重构

#### 2.1 移除硬编码的 price_ranges 逻辑

**文件**：`search/es_query_builder.py`

**需要修改的方法**：`_build_filters(self, filters, range_filters)`

**改进点**：

1. **完全移除** `if field == 'price_ranges'` 的特殊处理代码
2. 分离 filters 和 range_filters 的处理逻辑
3. 添加字段类型验证（利用配置系统）

**新的实现逻辑**：

```python
def _build_filters(
    self, 
    filters: Optional[Dict[str, Any]] = None,
    range_filters: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
    """
    构建过滤子句（重构版）。
    
    Args:
        filters: 精确匹配过滤器字典
        range_filters: 范围过滤器字典
    
    Returns:
        ES filter子句列表
    """
    filter_clauses = []
    
    # 1. 处理精确匹配过滤
    if filters:
        for field, value in filters.items():
            if isinstance(value, list):
                # 多值匹配（OR）
                filter_clauses.append({
                    "terms": {field: value}
                })
            else:
                # 单值精确匹配
                filter_clauses.append({
                    "term": {field: value}
                })
    
    # 2. 处理范围过滤
    if range_filters:
        for field, range_spec in range_filters.items():
            # 构建范围查询
            range_conditions = {}
            if isinstance(range_spec, dict):
                for op in ['gte', 'gt', 'lte', 'lt']:
                    if op in range_spec and range_spec[op] is not None:
                        range_conditions[op] = range_spec[op]
            
            if range_conditions:
                filter_clauses.append({
                    "range": {field: range_conditions}
                })
    
    return filter_clauses
```

#### 2.2 优化聚合参数接口

**需要完全移除的方法**：

- `add_dynamic_aggregations` - 直接暴露 ES DSL，完全删除

**需要重构的方法**：

- `add_aggregations` → 重构为 `build_facets`

**新增方法**：`build_facets(self, facet_configs)`

**新的实现逻辑**：

```python
def build_facets(
    self,
    facet_configs: Optional[List[Union[str, Dict[str, Any]]]] = None
) -> Dict[str, Any]:
    """
    构建分面聚合（重构版）。
    
    Args:
        facet_configs: 分面配置列表。可以是：
      - 字符串列表：字段名，使用默认配置
      - 配置对象列表：详细的分面配置
    
    Returns:
        ES aggregations字典
    """
    if not facet_configs:
        return {}
    
    aggs = {}
    
    for config in facet_configs:
        # 1. 简单模式：只有字段名
        if isinstance(config, str):
            field = config
            agg_name = f"{field}_facet"
            aggs[agg_name] = {
                "terms": {
                    "field": field,
                    "size": 10,  # 默认大小
                    "order": {"_count": "desc"}
                }
            }
        
        # 2. 高级模式：详细配置对象
        elif isinstance(config, dict):
            field = config['field']
            facet_type = config.get('type', 'terms')
            size = config.get('size', 10)
            agg_name = f"{field}_facet"
            
            if facet_type == 'terms':
                # Terms 聚合（分组统计）
                aggs[agg_name] = {
                    "terms": {
                        "field": field,
                        "size": size,
                        "order": {"_count": "desc"}
                    }
                }
            
            elif facet_type == 'range':
                # Range 聚合（范围统计）
                ranges = config.get('ranges', [])
                if ranges:
                    aggs[agg_name] = {
                        "range": {
                            "field": field,
                            "ranges": ranges
                        }
                    }
    
    return aggs
```

#### 2.3 更新主查询构建方法

**修改方法签名**：`build_query()`

```python
def build_query(
    self,
    query_text: str,
    query_vector: Optional[np.ndarray] = None,
    query_node: Optional[QueryNode] = None,
    filters: Optional[Dict[str, Any]] = None,
    range_filters: Optional[Dict[str, Any]] = None,  # 新增
    size: int = 10,
    from_: int = 0,
    enable_knn: bool = True,
    knn_k: int = 50,
    knn_num_candidates: int = 200,
    min_score: Optional[float] = None
) -> Dict[str, Any]:
    """构建完整的 ES 查询（重构版）"""
    # ... 实现
    
    # 添加过滤器
    if filters or range_filters:
        filter_clauses = self._build_filters(filters, range_filters)
        if filter_clauses:
            es_query["query"] = {
                "bool": {
                    "must": [query_clause],
                    "filter": filter_clauses
                }
            }
```

### 3. 搜索执行层重构

**文件**：`search/searcher.py`

**需要修改的方法**：`search()`

**改进点**：

1. 更新方法签名，接受 `range_filters` 和 `facets` 参数
2. **完全移除** `aggregations` 参数支持
3. 使用新的 `build_facets` 方法替代旧的聚合逻辑
4. 标准化分面搜索结果

**关键代码片段**：

```python
def search(
    self,
    query: str,
    size: int = 10,
    from_: int = 0,
    filters: Optional[Dict[str, Any]] = None,
    range_filters: Optional[Dict[str, Any]] = None,  # 新增
    facets: Optional[List[Union[str, Dict]]] = None,  # 替代 aggregations
    min_score: Optional[float] = None,
    sort_by: Optional[str] = None,
    sort_order: Optional[str] = "desc",
    debug: bool = False,
    context: Optional[RequestContext] = None
) -> SearchResult:
    """执行搜索（重构版）"""
    
    # ... 查询解析 ...
    
    # 构建 ES 查询
    es_query = self.query_builder.build_multilang_query(
        parsed_query=parsed_query,
        query_vector=parsed_query.query_vector,
        query_node=query_node,
        filters=filters,
        range_filters=range_filters,  # 新增
        size=size,
        from_=from_,
        enable_knn=enable_embedding,
        min_score=min_score
    )
    
    # 添加分面聚合（完全替代旧的 aggregations 逻辑）
    if facets:
        facet_aggs = self.query_builder.build_facets(facets)
        if facet_aggs:
            if "aggs" not in es_query:
                es_query["aggs"] = {}
            es_query["aggs"].update(facet_aggs)
    
    # ... 执行搜索 ...
    
    # 标准化分面结果
    standardized_facets = self._standardize_facets(
        es_response.get('aggregations', {}),
        facets,
        filters
    )
    
    return SearchResult(
        hits=hits,
        total=total_value,
        max_score=max_score,
        took_ms=int(total_duration),
        facets=standardized_facets,  # 标准化格式
        query_info=parsed_query.to_dict(),
        debug_info=debug_info
    )
```

**新增辅助方法**：

```python
def _standardize_facets(
    self,
    es_aggregations: Dict[str, Any],
    facet_configs: Optional[List[Union[str, Dict]]],
    current_filters: Optional[Dict[str, Any]]
) -> Optional[List[Dict[str, Any]]]:
    """
    将 ES 聚合结果转换为标准化的分面格式。
    
    Args:
        es_aggregations: ES 原始聚合结果
        facet_configs: 分面配置列表
        current_filters: 当前应用的过滤器
    
    Returns:
        标准化的分面结果列表
    """
    if not es_aggregations or not facet_configs:
        return None
    
    standardized_facets = []
    
    for config in facet_configs:
        # 解析配置
        if isinstance(config, str):
            field = config
            facet_type = "terms"
        else:
            field = config['field']
            facet_type = config.get('type', 'terms')
        
        agg_name = f"{field}_facet"
        
        if agg_name not in es_aggregations:
            continue
        
        agg_result = es_aggregations[agg_name]
        
        # 构建标准化分面结果
        facet = {
            "field": field,
            "label": field,
            "type": facet_type,
            "values": []
        }
        
        # 获取当前字段的选中值
        selected_values = set()
        if current_filters and field in current_filters:
            filter_value = current_filters[field]
            if isinstance(filter_value, list):
                selected_values = set(filter_value)
            else:
                selected_values = {filter_value}
        
        # 转换 buckets
        if 'buckets' in agg_result:
            for bucket in agg_result['buckets']:
                value = bucket.get('key')
                count = bucket.get('doc_count', 0)
                
                facet['values'].append({
                    "value": value,
                    "label": str(value),
                    "count": count,
                    "selected": value in selected_values
                })
        
        standardized_facets.append(facet)
    
    return standardized_facets


def _get_field_label(self, field: str) -> str:
    """获取字段的显示标签"""
    # 从配置中获取字段标签
    for field_config in self.config.fields:
        if field_config.name == field:
            return getattr(field_config, 'label', field)
    return field
```

### 4. API 路由层更新

**文件**：`api/routes/search.py`

**改进点**：

1. 接受新的请求模型参数
2. 添加搜索建议端点（框架）
3. **完全移除**旧的 `aggregations` 参数支持

**主搜索端点更新**：

```python
@router.post("/", response_model=SearchResponse)
async def search(request: SearchRequest, http_request: Request):
    """
    执行文本搜索。
    
    支持：
  - 布尔表达式（AND, OR, RANK, ANDNOT）
  - 精确匹配过滤器
  - 范围过滤器
  - 分面搜索
  - 自定义排序
    """
    # ... 实现使用新的参数
    result = searcher.search(
        query=request.query,
        size=request.size,
        from_=request.from_,
        filters=request.filters,
        range_filters=request.range_filters,  # 新增
        facets=request.facets,  # 替代 aggregations
        min_score=request.min_score,
        sort_by=request.sort_by,
        sort_order=request.sort_order,
        debug=request.debug,
        context=context
    )
```

**新增端点**：

```python
@router.get("/suggestions", response_model=SearchSuggestResponse)
async def search_suggestions(
    q: str = Query(..., min_length=1, description="搜索查询"),
    size: int = Query(5, ge=1, le=20, description="建议数量"),
    types: str = Query("query", description="建议类型（逗号分隔）")
):
    """
    获取搜索建议（自动补全）。
    
    功能说明：
  - 查询建议（query）：基于历史搜索和热门搜索
  - 商品建议（product）：匹配的商品
  - 类目建议（category）：匹配的类目
  - 品牌建议（brand）：匹配的品牌
    
    注意：此功能暂未实现，仅返回框架响应。
    """
    import time
    start_time = time.time()
    
    # TODO: 实现搜索建议逻辑
    suggestions = []
    took_ms = int((time.time() - start_time) * 1000)
    
    return SearchSuggestResponse(
        query=q,
        suggestions=suggestions,
        took_ms=took_ms
    )


@router.get("/instant", response_model=SearchResponse)
async def instant_search(
    q: str = Query(..., min_length=2, description="搜索查询"),
    size: int = Query(5, ge=1, le=20, description="结果数量")
):
    """
    即时搜索（Instant Search）。
    
    功能说明：
  - 边输入边搜索，无需点击搜索按钮
  - 返回简化的搜索结果
    
    注意：此功能暂未实现，调用标准搜索接口。
    """
    from api.app import get_searcher
    searcher = get_searcher()
    
    result = searcher.search(
        query=q,
        size=size,
        from_=0
    )
    
    return SearchResponse(
        hits=result.hits,
        total=result.total,
        max_score=result.max_score,
        took_ms=result.took_ms,
        query_info=result.query_info
    )
```

### 5. 前端适配

**文件**：`frontend/static/js/app.js`

**需要完全重写的地方**：

1. **聚合参数改用简化配置**（第 57-87 行）：
```javascript
// 旧的方式（ES DSL）- 完全删除
const aggregations = {
    "category_stats": {...}  // 删除
};

// 新的方式（简化配置）
const facets = [
    {
        "field": "categoryName_keyword",
        "size": 15,
        "type": "terms"
    },
    {
        "field": "brandName_keyword",
        "size": 15,
        "type": "terms"
    },
    {
        "field": "supplierName_keyword",
        "size": 10,
        "type": "terms"
    },
    {
        "field": "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}
        ]
    }
];
```

2. **过滤器使用新格式**（第 103 行）：
```javascript
// 旧的方式 - 完全删除
filters: {
    "price_ranges": ["0-50", "50-100"]  // 删除
}

// 新的方式
filters: Object.keys(state.filters).length > 0 ? state.filters : null,
range_filters: state.rangeFilters ? state.rangeFilters : null
```

3. **解析标准化的分面结果**（第 208-258 行）：
```javascript
// 旧的方式（ES 结构）- 完全删除并重写
function displayAggregations(aggregations) {
    if (!aggregations) return;
    // 旧的代码全部删除
}

// 新的方式（标准化格式）
function displayFacets(facets) {
    if (!facets) return;
    
    facets.forEach(facet => {
        if (facet.field === 'categoryName_keyword') {
            const container = document.getElementById('categoryTags');
            let html = '';
            
            facet.values.forEach(facetValue => {
                const value = facetValue.value;
                const count = facetValue.count;
                const selected = facetValue.selected;
                
                html += `
                    <span class="filter-tag ${selected ? 'active' : ''}" 
                          onclick="toggleFilter('${escapeAttr(facet.field)}', '${escapeAttr(value)}')">
                        ${escapeHtml(value)} (${count})
                    </span>
                `;
            });
            
            container.innerHTML = html;
        }
        // 处理其他分面...
    });
}
```


## 第三部分：实施步骤

### 阶段 1：后端模型层重构

**任务清单**：

- [ ] 更新 `api/models.py`
  - [ ] 定义 `RangeFilter` 模型
  - [ ] 定义 `FacetConfig` 模型
  - [ ] 更新 `SearchRequest`，添加 `range_filters` 和 `facets`
  - [ ] **完全移除** `aggregations` 参数
  - [ ] 定义 `FacetValue` 和 `FacetResult` 模型
  - [ ] 更新 `SearchResponse`，使用标准化分面格式
  - [ ] 更新 `ImageSearchRequest`，添加 `range_filters`
  - [ ] 添加 `SearchSuggestRequest` 和 `SearchSuggestResponse`（框架）

**验证方式**：

- 运行 Pydantic 模型验证
- 检查 API 文档（`/docs`）是否正确生成
- 确认旧的 `aggregations` 参数已完全移除

### 阶段 2：查询构建器重构

**任务清单**：

- [ ] 重构 `search/es_query_builder.py`
  - [ ] **完全删除** `price_ranges` 硬编码逻辑（第 205-233 行）
  - [ ] 重构 `_build_filters` 方法，支持 `range_filters`
  - [ ] **完全删除** `add_dynamic_aggregations` 方法
  - [ ] **完全删除或重命名** `add_aggregations` 方法
  - [ ] 新增 `build_facets` 方法
  - [ ] 更新 `build_query` 方法签名，添加 `range_filters`
- [ ] 更新 `search/multilang_query_builder.py`
  - [ ] 更新 `build_multilang_query` 方法签名
  - [ ] 确保正确传递 `range_filters` 参数

**验证方式**：

- 打印生成的 ES DSL，检查正确性
- 确认旧的硬编码逻辑和方法已完全移除
- 手动测试不同的过滤器组合

### 阶段 3：搜索执行层重构

**任务清单**：

- [ ] 更新 `search/searcher.py`
  - [ ] 更新 `search()` 方法签名，添加 `range_filters` 和 `facets`
  - [ ] **完全移除** `aggregations` 参数支持
  - [ ] 使用新的 `build_facets` 方法
  - [ ] 实现 `_standardize_facets()` 辅助方法
  - [ ] 实现 `_get_field_label()` 辅助方法
  - [ ] 更新 `SearchResult` 类，使用标准化分面格式
  - [ ] 更新 `search_by_image()` 方法，支持 `range_filters`

**验证方式**：

- 手动测试各种搜索场景
- 检查分面结果格式是否标准化
- 确认旧的聚合逻辑已完全移除

### 阶段 4：API 路由层更新

**任务清单**：

- [ ] 更新 `api/routes/search.py`
  - [ ] 更新 `/search/` 端点，使用新的请求参数
  - [ ] **确认完全移除**对旧 `aggregations` 的支持
  - [ ] 添加 `/search/suggestions` 端点（框架，返回空结果）
  - [ ] 添加 `/search/instant` 端点（框架，调用标准搜索）
  - [ ] 更新 `/search/image` 端点，支持 `range_filters`
  - [ ] 添加完整的端点文档和示例

**验证方式**：

- 使用 Swagger UI（`/docs`）测试所有端点
- 检查请求和响应格式
- 确认旧参数不再被接受

### 阶段 5：前端适配

**任务清单**：

- [ ] 完全重构 `frontend/static/js/app.js`
  - [ ] **删除**所有 ES DSL 聚合代码（第 57-87 行）
  - [ ] 使用新的 `facets` 简化配置
  - [ ] 修改过滤器参数，分离 `filters` 和 `range_filters`
  - [ ] **完全重写** `displayAggregations()` 为 `displayFacets()`
  - [ ] 解析标准化的分面结果格式
  - [ ] **删除**所有 `price_ranges` 硬编码
  - [ ] 添加范围过滤器 UI 组件
  - [ ] 更新状态管理，支持 `rangeFilters`

**验证方式**：

- 浏览器测试所有前端功能
- 检查网络请求格式
- 检查分面结果显示
- 确认旧的代码已完全删除

### 阶段 6：文档更新与示例

**任务清单**：

- [ ] 撰写完整的 API 接口文档
  - [ ] 搜索接口文档
  - [ ] 过滤器使用说明
  - [ ] 分面搜索使用说明
  - [ ] 搜索建议接口文档（标注为框架）
- [ ] 更新项目文档
  - [ ] 更新 `README.md`
  - [ ] 更新 `USER_GUIDE.md`
  - [ ] 更新 `CHANGES.md` 或 `CHANGELOG.md`
- [ ] 添加完整的使用示例
  - [ ] 简单搜索示例
  - [ ] 过滤器搜索示例
  - [ ] 分面搜索示例
  - [ ] cURL 命令示例
  - [ ] Python 代码示例
  - [ ] JavaScript 代码示例
- [ ] 更新 API 文档注释
  - [ ] 所有模型的文档字符串
  - [ ] 所有端点的文档字符串
  - [ ] 参数说明和示例

**验证方式**：

- 文档审查
- 示例代码验证
- 确保文档与实现一致

## 第四部分：API 使用示例

### 示例 1：简单搜索

```bash
POST /search/
Content-Type: application/json

{
  "query": "芭比娃娃",
  "size": 20
}
```

**响应**：

```json
{
  "hits": [
    {
      "_id": "12345",
      "_score": 8.5,
      "_source": {
        "name": "芭比时尚娃娃",
        "price": 89.99,
        "categoryName": "玩具"
      }
    }
  ],
  "total": 118,
  "max_score": 8.5,
  "took_ms": 45,
  "query_info": {
    "original_query": "芭比娃娃",
    "detected_language": "zh"
  }
}
```

### 示例 2：带过滤器的搜索

```bash
POST /search/
Content-Type: application/json

{
  "query": "玩具",
  "size": 20,
  "filters": {
    "categoryName_keyword": ["玩具", "益智玩具"],
    "in_stock": true
  },
  "range_filters": {
    "price": {
      "gte": 50,
      "lte": 200
    }
  }
}
```

**响应**：

```json
{
  "hits": [...],
  "total": 45,
  "max_score": 7.2,
  "took_ms": 38
}
```

### 示例 3：带分面搜索的请求（简单模式）

```bash
POST /search/
Content-Type: application/json

{
  "query": "玩具",
  "size": 20,
  "facets": [
    "categoryName_keyword",
    "brandName_keyword"
  ]
}
```

**响应**：

```json
{
  "hits": [...],
  "total": 118,
  "max_score": 8.5,
  "took_ms": 45,
  "facets": [
    {
      "field": "categoryName_keyword",
      "label": "商品类目",
      "type": "terms",
      "values": [
        {
          "value": "玩具",
          "label": "玩具",
          "count": 85,
          "selected": false
        },
        {
          "value": "益智玩具",
          "label": "益智玩具",
          "count": 33,
          "selected": false
        }
      ]
    },
    {
      "field": "brandName_keyword",
      "label": "品牌",
      "type": "terms",
      "values": [
        {
          "value": "乐高",
          "label": "乐高",
          "count": 42,
          "selected": false
        }
      ]
    }
  ]
}
```

### 示例 4：带分面搜索的请求（高级模式）

```bash
POST /search/
Content-Type: application/json

{
  "query": "玩具",
  "size": 20,
  "facets": [
    {
      "field": "categoryName_keyword",
      "size": 15,
      "type": "terms"
    },
    {
      "field": "brandName_keyword",
      "size": 15,
      "type": "terms"
    },
    {
      "field": "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}
      ]
    }
  ]
}
```

**响应**：

```json
{
  "hits": [...],
  "total": 118,
  "max_score": 8.5,
  "took_ms": 45,
  "facets": [
    {
      "field": "categoryName_keyword",
      "label": "商品类目",
      "type": "terms",
      "values": [...]
    },
    {
      "field": "price",
      "label": "价格区间",
      "type": "range",
      "values": [
        {
          "value": "0-50",
          "label": "0-50",
          "count": 23,
          "selected": false
        },
        {
          "value": "50-100",
          "label": "50-100",
          "count": 45,
          "selected": false
        },
        {
          "value": "100-200",
          "label": "100-200",
          "count": 38,
          "selected": false
        },
        {
          "value": "200+",
          "label": "200+",
          "count": 12,
          "selected": false
        }
      ]
    }
  ]
}
```

### 示例 5：复杂搜索（过滤+分面+排序）

```bash
POST /search/
Content-Type: application/json

{
  "query": "玩具 AND (乐高 OR 芭比)",
  "size": 20,
  "from": 0,
  "filters": {
    "categoryName_keyword": "玩具"
  },
  "range_filters": {
    "price": {
      "gte": 50,
      "lte": 200
    },
    "days_since_last_update": {
      "lte": 30
    }
  },
  "facets": [
    {"field": "brandName_keyword", "size": 15},
    {"field": "supplierName_keyword", "size": 10}
  ],
  "sort_by": "price",
  "sort_order": "asc",
  "debug": false
}
```

### 示例 6：搜索建议（框架）

```bash
GET /search/suggestions?q=芭&size=5

{
  "query": "芭",
  "suggestions": [],
  "took_ms": 2
}
```

### 示例 7：即时搜索（框架）

```bash
GET /search/instant?q=玩具&size=5

{
  "hits": [...],
  "total": 118,
  "max_score": 8.5,
  "took_ms": 25,
  "query_info": {...}
}
```

### Python 示例代码

```python
import requests

API_URL = "http://localhost:6002/search/"

# 简单搜索
response = requests.post(API_URL, json={
    "query": "芭比娃娃",
    "size": 20
})
print(response.json())

# 带过滤器和分面的搜索
response = requests.post(API_URL, json={
    "query": "玩具",
    "size": 20,
    "filters": {
        "categoryName_keyword": ["玩具", "益智玩具"]
    },
    "range_filters": {
        "price": {"gte": 50, "lte": 200}
    },
    "facets": [
        {"field": "brandName_keyword", "size": 15},
        {"field": "categoryName_keyword", "size": 15}
    ],
    "sort_by": "price",
    "sort_order": "asc"
})
result = response.json()

# 处理分面结果
for facet in result.get('facets', []):
    print(f"\n{facet['label']}:")
    for value in facet['values']:
        print(f"  - {value['label']}: {value['count']}")
```

### JavaScript 示例代码

```javascript
// 搜索函数
async function searchProducts(query, filters, rangeFilters, facets) {
    const response = await fetch('http://localhost:6002/search/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            query: query,
            size: 20,
            filters: filters,
            range_filters: rangeFilters,
            facets: facets
        })
    });
    
    const data = await response.json();
    return data;
}

// 使用示例
const result = await searchProducts(
    "玩具",
    { categoryName_keyword: ["玩具"] },
    { price: { gte: 50, lte: 200 } },
    [
        { field: "brandName_keyword", size: 15 },
        { field: "categoryName_keyword", size: 15 }
    ]
);

// 显示分面结果
result.facets.forEach(facet => {
    console.log(`${facet.label}:`);
    facet.values.forEach(value => {
        console.log(`  - ${value.label}: ${value.count}`);
    });
});
```

## 第五部分：验收标准

### 功能验收

- [ ] 所有硬编码逻辑已完全移除
- [ ] 新的过滤器参数正常工作
- [ ] 分面搜索返回标准化格式
- [ ] API 文档完整准确
- [ ] 前端功能正常工作
- [ ] 搜索建议端点已添加（框架）

### 代码质量验收

- [ ] 代码中不再有 `price_ranges` 字符串
- [ ] 代码中不再有 `add_dynamic_aggregations` 方法
- [ ] 代码中不再有 `aggregations` 参数（除了文档说明）
- [ ] 所有方法签名已更新
- [ ] 所有文档字符串已更新

### 性能验收

- [ ] 搜索响应时间无明显增加
- [ ] 分面查询性能正常
- [ ] 过滤器性能正常

## 总结

本计划通过系统性的重构，将搜索 API 从硬编码、暴露 ES 细节的实现，转变为灵活、通用、易用的 SaaS 产品接口。关键改进包括：

1. ✅ **完全移除**硬编码的 price_ranges 逻辑
2. ✅ 实现结构化的过滤参数（filters + range_filters）
3. ✅ **完全移除** aggregations 参数，使用简化的 facets 接口
4. ✅ 标准化分面搜索响应格式
5. ✅ 添加搜索建议功能框架（暂不实现）
6. ✅ **不保留向后兼容代码**，完全重构

通过这些改进，系统将具备更好的通用性、可维护性和可扩展性，代码更加精简和统一。