

增加款式意图识别模块。意图类型： 颜色，尺码（目前只需要支持这两种）

一、 意图判断
- 意图召回层：
每种意图，有一个召回词集合
对query（包括原始query、各种翻译query 都做匹配）
- 以颜色意图为例：
有一个词表，每一行 都逗号分割，互为同义词，行内第一个为标准化词
query匹配了其中任何一个词，都认为，具有颜色意图
匹配规则： 用细粒度、粗粒度分词，看是否有在词表中的。原始query分词、和每种翻译的分词，都要用。

二、 意图使用：
  当前 SKU 置顶逻辑在「分页 + 详情回填」之后
流程是：run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter
  要改为：  
  1. 有款式意图的时候，才做sku筛选  
  2. sku筛选的时机，改为在reranker之前，对所有内容（rerank输入的所有spus）做sku筛选
  3. 从仅 option1 扩展到多个维度，识别的意图，包含意图的维度名（color）和维度名的泛化词list（color、颜色、colour、colors...），遍历spu的option1_name,option2_name,option3_name字段，看哪个能匹配上意图的维度名list，哪个匹配上了，则在这个维度筛选。
    1. 比如匹配到option2_name，那么取每一个sku的option2_values。如果没匹配到任何一个，那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用：
      1. 用来跟query匹配，看哪个更query相关性更高，以此进行最优sku筛选，把选出来的sku置顶，并替换spu的image_url
      2. 用来做rerank doc的title补充，从而参与rerank
  4. Rerank doc （有款式意图的时候）要带上属性后缀，拼接到title后面。在调用 run_rerank 前，对每条 hit 生成「用于重排的 doc 文本」（标题 + 可选后缀）  

- sku筛选的规则也要优化： 
现在的逻辑是，先做包含的判断，找到第一个 option_value被query包含的，则直接认为匹配。没有匹配的再用embedding相似度。
改为： 
  1. 第一轮：遍历完，如果有且仅有一个被query包含，那么认为匹配。  
  2. 第二轮：如果有多个符合（被query包含），跳到3。如果没有，对每个词都走泛化词表进行匹配。  
  3. 第三轮：如果有多个，那么对这多个，走embedding相关性取最高的。如果一个也没有，则对所有的走embedding相关性取最高的
  这个sku筛选也需要提取为一个独立的模块。

细节备注：
intent 考虑由 QueryParser 编排、具体实现拆成独立模块，主义好，现有的分词等基础设施的复用，缺失的英文分词可以补充。
在重排窗口内，第一次 ES 查询会把 _source 裁成「重排模板需要的字段」，默认只有 title 等，不包含 skus / option*_name。因此，有意图的时候，需要给这一次的_source加上 skus / option*_name  

先仔细理解需求，查看代码，深度思考应该如何设计，和当前的系统较好的融合，给出统一的设计，可以根据需要适当改造当前的实现，降低整个系统的复杂度，提高模块化程度，而不是打补丁。修改后的最终状态应该是要足够简单、清晰、无冗余和分叉，模块间低耦合。多步思考确认最佳施工方案之后才进行代码修改。

5. TODO： 搜索接口里，results[].skus 不是全量子 SKU：由 sku_filter_dimension 控制在应用层按维度分组折叠，每个「维度取值组合」只保留一条 SKU（组内第一条）。请求未传该字段时，Pydantic 默认是 ["option1"]，等价于只按 option1_value 去重；服务端不会读取店铺主题的「主展示维」，需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3（或对应 option*_name）时，若仍用默认只按 option1（常见为颜色）折叠，同一颜色下多种款式只会出现一条代表 SKU，无法从返回的 skus 里拿到该颜色下的全部款式行。（若业务需要全量子款，需传包含对应维度的 sku_filter_dimension，或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。）





GPT5.4 主要思路：
我准备按这条线改，尽量把逻辑收束成两个新模块而不是继续堆在 Searcher 里：

QueryParser 负责产出统一的“款式意图分析结果”，包含：命中的意图类型、标准化值、命中的 query 变体、分词结果、维度候选词。
独立的 SKU 选择模块负责：维度解析、候选值抽取、三阶段匹配、选中 SKU 后的主图替换，以及生成 rerank 用的属性后缀。
Searcher 只编排：解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 -> page fill -> ResultFormatter。这样旧的页内 SKU 置顶逻辑可以下沉或删除，避免前后两个阶段重复做同一件事。我再补看一下你最近写的意图草稿文档，确认有没有现成词表示例或边界条件，避免我这边把词表格式和你预期做偏。





意图匹配的性能优化：
上面一版实现，性能上完全无法接受。因此进行了一轮策略简化

prompt
style_sku_prepare_hits阶段耗时太长。请根据需求，思考优化的方法，给出性能优化的方案。
1. _select_by_embedding，有缓存吗，option_value的值是有限的，之前已经算过的，就不用再算了。不仅仅是embedding相似的结果，整个option_value的匹配结果，是有包含、还是没包含，相似度多少，都不用重新计算。比如之前已经有一个sku的某个属性值叫做“卡其色”，已经算出来是否文本匹配了，那么不需要再去做文本匹配。如果已经算出来向量的相似度，那么不需要再去取向量以及计算相似度。
2. 匹配可以适当的优化：
匹配流程简化：
1）找到第一个文本匹配的，如果有直接匹配成功。不需要考虑匹配多个的情况。
2）如果全部都没有匹配，那么进行embedding筛选。

匹配规则：
option_name的匹配，直接看规范化后的option_name是不是意图维度的泛化词之一（比如颜色、color、colour），如果没有匹配的，现在应该是把所有维度都算上，这样匹配成本和比较成本太高了，去掉这些逻辑，这种情况不需要加后缀、不需要选择sku。
ption_value的匹配。意图检测的时候，有匹配的query中的命中的词，这个词被包含在属性值中，那么就算匹配。属性值被包含在query（包括翻译文本）中，也算匹配。提高匹配的覆盖率。

3. 这一阶段得到sku选择的结果即可（选中的sku的id，也可以为空值表示没找到匹配成功的，这种情况不需要拼接title后缀给重排输入），但是不用着急做image_url的替换和sku的置顶。等最后填充的时候判断有选中sku的时候直接做替换和置顶即可。
请你思考如何进行设计，提高性能的时候不带来复杂度的提升，可以适当的重构以降低修改后的代码行数。
@search/sku_intent_selector.py @query/style_intent.py 


修改结果：
核心变化在 `search/sku_intent_selector.py` 和 `search/searcher.py`。

现在的策略是：
- `option_name` 只做规范化后 alias 精确匹配；任何意图维度没 resolve 到字段，就直接不选 SKU、不加 suffix。
- `option_value` 文本匹配改成顺序扫描 SKU，找到第一个命中的就直接返回，不再收集多个候选再比较。
- 文本匹配规则支持两种：
  - query 命中的意图词被属性值包含
  - 属性值被 query/translation 包含
- 只有当所有 SKU 都没有文本命中时，才做 embedding 选择。
- `prepare_hits()` 现在只产出决策和 `_style_rerank_suffix`，不再提前改 `_source`；真正的 `image_url` 替换和 SKU 置顶统一后移到 `apply_precomputed_decisions()`。
- 加了请求级缓存，避免同一个 `option_value`/`selection_text` 在一次请求里重复做文本判断、取向量和算相似度。
- 顺手删掉了旧的 generalized match / fallback_text 路径，代码比之前更短也更直。

另外补了回归测试，覆盖了：
- 翻译文本命中后正确选 SKU
- 多个文本命中时取第一个
- `option_name` 不命中 alias 时不做 SKU 选择
- 无文本命中时走 embedding fallback

验证过：
- `pytest tests/test_search_rerank_window.py -q` 通过
- 变更文件 lint 无报错


------------------------------



---

## 1. 现状（与需求的差距）

**流水线**（`search/searcher.py`）大致是：

1. `QueryParser.parse` → `ParsedQuery`（含 `translations`、`query_tokens` 等）  
2. 组 ES 查询；若在重排窗口内，第一次查询把 `_source` 裁成「重排模板所需字段」（`_resolve_rerank_source_filter`）  
3. ES 搜索 → `run_rerank`（`rerank_client.build_docs_from_hits` 用 `{title}` 等拼 doc）  
4. 按 `from/size` 切片 → **page fill** 用 `ids` 查询把当前页 `_source` 补全  
5. `_apply_sku_sorting_for_page_hits`（仅 **option1**，先子串包含命中第一个，否则全量 option1 embedding）  
6. `ResultFormatter`（`sku_filter_dimension` 只做**展示层**按维度折叠 SKU，与置顶逻辑独立）


**与需求冲突但必须一起解决的一点**：page fill 会用 ES 拉回来的 `_source`**整份覆盖**当前 hit（约 841–842 行）。若在 rerank **之前**只改内存里的 `skus` 顺序/`image_url`，**不**在 fill 后再处理一次，最终响应会被覆盖掉。因此「rerank 前对所有 window 内 hit 做 SKU 决策」和「用户看到的最终列表」之间，必须有一条**明确的数据契约**（见下文 §4）。

---

## 2. 模块划分（建议：`intent` + `sku_intent` 两层）

避免继续在 `Searcher` 里堆方法，建议新建小包，职责清晰、由 `Searcher` 编排。

| 模块 | 职责 |
|------|------|
| **`query/intent/`**（或 `search/intent/`，二选一以「离谁更近」为准；更推荐 **`query/intent`**，因为输入完全是 query 侧事实） | 加载词表、**意图召回**、多 query 变体 + 粗细分词、输出结构化 **`IntentProfile`** |
| **`search/sku_intent/`**（或 `intent/sku_selection.py`） | 根据 `IntentProfile` 解析 **option1/2/3** 哪一维、生成每 SKU 的**匹配文本**、三轮匹配规则、embedding 批处理、对 `_source` 做 **promote + image_url** |
| **`search/rerank_client.py`（薄扩展）** | 支持「每条 hit 的 doc 文本」：模板扩展或 **显式传入 per-hit 字符串列表**，避免把业务塞进 format 字符串 |

**`IntentProfile`（概念模型）建议包含**：

- `active_intents: Set[Literal["color","size"]]`（可扩展）
- 每种意图：`canonical_terms`（命中行的标准词）、`matched_surface_forms`（可选，用于 debug）
- **维度别名**：如 color → `{"color","颜色","colour",...}`（配置或独立小词表）
- 原始用于匹配的 token 集合：每个 query 变体 ×（细粒度 | 粗粒度），便于日志与单测

**词表**：

- **意图召回表**：每行逗号分隔同义词，首词标准化；颜色、尺码各一份（路径放 `config/` 或 `resources/intent/` + `config.yaml` 指路径）。
- **SKU 第二轮「泛化」表**（对 **option 取值** 做同义扩展）：与意图召回表分开，避免语义混在一起。

---

## 3. 意图判断（与 `QueryParser` 的衔接）

需求：对 **原始 query + 各类翻译** 都做匹配；**细粒度 + 粗粒度** 分词。

现状：

- `ParsedQuery` 里 **`query_tokens` 只对 rewritten 后的 `query_text` 跑了一次 HanLP**（`query_parser.py` 269–274 行附近），**没有**对 `original_query`、各 `translations` 的 token 缓存。
- 已有 **`simple_tokenize_query`**（粗粒度）在 `query_parser.py`。

**建议**：

- 在 **`IntentDetector.detect(parsed_query, tokenizer_fn)`** 内统一生成「query 变体列表」：至少包含 `original_query`、`query_normalized`、`rewritten_query`、`translations` 的值（与当前 `_build_sku_query_texts` 思路一致，但升级为**结构化**）。
- 细粒度：复用 `QueryParser._get_query_tokens`（需把该方法暴露为公开 API 或注入同一 HanLP callable），对每个变体字符串调用。
- 粗粒度：对每个变体调用 `simple_tokenize_query`。
- 匹配逻辑：**任意变体 × 任意粒度** 的 token 落在「标准化 → 同义词闭包」上即视为命中该意图（与你描述的行内同义一致）。

**可选优化**：在 `parse()` 里顺带产出 `intent_profile`，减少一次遍历；但为控制 `QueryParser` 体积，更稳妥的是 **parse 之后**单独调 `IntentDetector`，依赖清晰。

---

## 4. 流水线改造（与 page fill 的契约）

目标顺序变为：

`ES(window)`（有意图时 `_source` 含 `skus` + `option*_name`）  
→ **对每个 hit：SKU 决策 + 生成 rerank 用后缀/全文**  
→ `run_rerank`（doc = 标题 + 后缀）  
→ 切片  
→ page fill  
→ **最终响应前再应用一次 SKU 决策（或与 prefetch 结果合并）**  
→ `ResultFormatter`

**为何最后还要一次？** 因为 page fill 会覆盖 `_source`，rerank 前内存里的 `skus` 顺序不能当作最终真相。

**推荐契约（降低复杂度）**：

1. **Rerank 前**：对 window 内每个 hit 计算 `SkuIntentDecision`（至少包含：`option_slot` 1/2/3、`candidate_sku_index` 或 `sku_id`、`rerank_suffix` 字符串）。可挂在 hit 的**非 ES 字段**上，例如 `hit["_intent_sku"] = {...}`（或只存 `rerank_doc_text` 全文）。
2. **`run_rerank`**：`build_docs_from_hits` 若发现 hit 上已有 `rerank_doc_text`（或 `style_suffix` + 模板），则优先使用，否则走原模板。
3. **Page fill 之后**：对**当前页** hit 再调用**同一** `SkuIntentSelector.apply(source, parsed_query, intent_profile)`（或根据 `_id` 合并 prefetch 决策）。这样最终 `image_url` / SKU 顺序与 rerank 一致，且不被 fill 冲掉。

若担心算两次 embedding：**第一次**在 window 全量上算 query 向量 + option 向量；第二次仅对当前页且可带缓存（按 `embed_key` 去重），一般量很小。

**不在重排窗口内**：没有「rerank 前全 window」这一步；可在 **ResultFormatter 前**对当前页 `es_hits` 用同一 `SkuIntentSelector`（仅当有意图时），与「有意图才做 SKU 筛选」一致。

---

## 5. `_resolve_rerank_source_filter` 与 ES 字段

需求：有意图时预取需包含 `skus`、`option1_name`、`option2_name`、`option3_name`。

建议签名扩展为：

`_resolve_rerank_source_filter(doc_template, intent_profile: Optional[IntentProfile])`

- 若 `intent_profile` 非空且含 color/size（或任意「款式意图」），在 `includes` 中**合并**上述字段（并与模板解析出的 `title` 等取并集）。
- 注意与全局 `source_fields` 的 tri-state 语义（`_apply_source_filter`）是否冲突：若租户配置 `_source` 白名单且不含 `skus`，需定义优先级——**建议**：「款式意图所需字段」作为**最低保证**合并进本次请求的 fetch includes，或在文档中写明限制。

---

## 6. 多维度 option 与「未匹配维度名」

需求逻辑可落到纯函数：

1. 对每个意图类型，有 **维度别名集合**（如 color）。  
2. 依次与 `option1_name`、`option2_name`、`option3_name`（字符串，注意多语言：与 indexer 一致，可能是纯英文或中文）做 **casefold / 规范化** 后匹配别名表。  
3. 命中则该 SKU 行的匹配字段为 `option{k}_value`；用于 embedding key 时继续用 `name:value` 形式（沿用现有 `_sku_option1_embedding_key` 思路，泛化为 `option_slot`）。  
4. **若三个 name 都不匹配意图维度**：用 `option1_value`、`option2_value`、`option3_value` **空格拼接**成一条「兜底描述字符串」，供：  
   - 与 query 的包含/泛化/embedding 比较；  
   - 作为 `rerank_suffix` 的一部分（若你希望无明确维度时仍加强 rerank）。

**多意图同时存在**（如同时颜色+尺码）：需要在产品层定规则，例如：

- 只对「主意图」排序（配置优先级 color > size），或  
- 要求两个维度都满足的 SKU 优先，否则退化为单意图。  

实现上可在 `SkuIntentSelector` 输入 `List[IntentType]` 与策略枚举，避免写死 if-else 散落。

---

## 7. 三轮 SKU 匹配规则（独立模块内）

从当前「第一个包含就返回」改为：

1. **第一轮**：统计「option 匹配文本被 **整条 query 文本** **包含**」的 SKU（或对每个 query 变体分别计，再合并——建议与你现有 `_build_sku_query_texts` 对齐）；**若恰好 1 个** → 选中。  
2. **第二轮**：若 0 个，对每个 SKU 的候选词走 **取值泛化表**（同义词行），再跑包含判断；仍统计「多个 / 零个」。  
3. **第三轮**：  
   - 若 **多个** 满足包含（第一轮或第二轮）→ 仅在这多个上算 embedding，取相似度最高；  
   - 若 **仍 0 个** → 对 **全部** SKU 算 embedding，取最高。

实现上保持 **批量 encode**（与当前 `option1_values_to_encode` 去重逻辑类似），只是把「embed_key」从固定 option1 改为按 slot 动态生成。

---

## 8. `sku_filter_dimension`（API）与意图的关系

- **`sku_filter_dimension`**：客户端指定「结果里 SKU 列表如何按维度折叠」，在 `ResultFormatter._filter_skus_by_dimensions` 中实现。  
- **意图 SKU 置顶**：服务端根据 query 推断维度与取值，改顺序与主图。

建议约定：

- **置顶 / 换图**仅在意图开启时执行；  
- **`sku_filter_dimension` 仍只影响返回 SKU 条数结构**；若与意图维度冲突（例如意图命中 color，客户端只按 size 折叠），应用**文档说明优先级**：常见做法是 **先意图置顶，再 filter**（或相反，需在 PRD 写清）。  

避免在 `ResultFormatter` 里再猜意图；意图结论由上游传入或在 Formatter 前已完成 `_source` 调整。

---

## 9. 配置与观测

- `config.yaml`：`intent.enabled`、`intent.lexicon_paths`、`intent.dimension_aliases`（或按类型分块）。  
- `RequestContext` / `debug`：写入 `intent_profile`、`sku_intent_decision`、rerank 用的 doc 摘要，便于与 `docs/TODO-意图判断.md` 对齐。

---

## 10. 小结

- **核心架构**：**`IntentDetector`（query 侧）** + **`SkuIntentSelector`（search 侧）** + **`run_rerank` 的 per-hit doc 覆盖** + **`_resolve_rerank_source_filter` 条件 includes**。  
- **必须处理 page fill 覆盖 `_source`**：rerank 前决策与 **fill 后再 apply 一次**（或等价合并策略），否则会出现「重排用了带后缀的 doc、返回结果却是未置顶 SKU」的不一致。  
- **与现有系统融合点**：`ParsedQuery` 变体列表、HanLP + `simple_tokenize_query`、`TextEmbeddingEncoder`、`ResultFormatter` / `sku_filter_dimension` 的边界清晰，避免把意图逻辑复制到 `api/` 层。

若你后续希望把「多意图优先级」或「rerank 后缀格式」定成唯一产品规则，可以在实现前写进同一份 spec，模块接口会很好稳定下来。