# 1-prompt

现在索引是spu粒度的，sku是作为spu的nested属性。召回和计算exact_image_knn_query都会计算图片的相似度，image_embedding字段是一个list，跟sku是一一对应的。
我希望，是因为哪个image_embedding被召回的、或者重排的时候，是取的哪个元素的exact_image_knn_query得分（image_embedding这个list内最高的得分），我希望将这个sku前置。

之前在重排之前有这个流程：
  1. 有款式意图的时候，才做sku筛选  
  2. sku筛选的时机，改为在reranker之前，对所有内容（rerank输入的所有spus）做sku筛选
若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本
Searcher 只编排：解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 
但是，现在是关闭重排的，所以，这个流程可能没了？请你检查。

我的总的意图是，如果有款式方面的意图，需要做spu内部的sku的排序，
这两点也需要检查：
1. 之前的流程，不知道是否仍然有效（因为现在关闭了reranker）
2. 现在补充了image_embedding的召回和exact_image_knn_query得分，那么应该能知道哪个sku是最匹配的，希望把这个信号利用上，
1）召回的时候，是哪个图片向量相似度最高
2）重排的时候是用哪个算的相似度？这个信号如何利用
      "rescore_query": {
        "bool": {
          "should": [
            {
              "nested": {
                "path": "image_embedding",
                "_name": "exact_image_knn_query",
                "score_mode": "max",
                "query": {
                  "script_score": {
                    "query": {
                      "exists": {
                        "field": "image_embedding.vector"
                      }
                    },
                    "script": {
                      "source": "((dotProduct(params.query_vector, 'image_embedding.vector') + 1.0) / 2.0) * params.boost",
                      "params": {

最终组织结果的时候，sku的排序，要把这个信号利用进来，要按照图片的相似度排序。
是直接用图片的信号，把图片得分最高的url，用于sku的筛选（选取对应url的sku。因为skus和image_embedding是两个字段，skus里面有image_src，可以用于url匹配，选取对应url的sku作为图片得分最高的sku）还是跟以前的策略融合在一起，还是，请思考


可以以query=白色T恤进行实验

# 1-response
我仔细读了当前 `search/searcher.py`、`search/sku_intent_selector.py`、`search/es_query_builder.py`、`mappings/search_products.json` 和 `indexer/document_transformer.py`，先回答你的两个核对问题，再给出如何把图片信号融入的设计。

---

## 1. 关闭 rerank 之后，原来的款式意图 SKU 流程是否还有效？

**仍然有效。** 关键点：

- `in_rank_window = (from_ + size) <= rerank_window`（`searcher.py:529`）只依赖**配置里的 `rerank_window`**，跟请求/配置里的 `do_rerank` 没关系。
- 进入 `in_rank_window` 分支后，款式意图处理是**无条件**执行的：
  - `searcher.py:1070-1081`：在 coarse 完成、ranking 窗口 source fill 之后，调用 `_apply_style_intent_to_hits` 算出 `style_intent_decisions`（带 `_style_rerank_suffix`）。
  - `searcher.py:1083-1154`：fine 和 rerank 阶段现在都通过 `_run_optional_stage` 包装，`enabled=False` 时走 passthrough，**不会丢**前面挂在 hit 上的信号。
  - `searcher.py:1205-1209`：page fill 覆盖 `_source` 之后，再调一次 `apply_precomputed_decisions`，把 SKU 置顶 + `image_url` 替换最终写回。
- 非 rank-window 分支（`searcher.py:1225-1231`）走 `_apply_style_intent_to_hits` 并在 `result_processing` 前 `apply_precomputed_decisions`（1254-1259）。

所以"先 prepare、后 apply"的契约没断。唯一的"废功"是 `_style_rerank_suffix`——关掉重排之后它挂在 hit 上但没人读；代价很小、一旦你重新打开 rerank 又立刻生效，建议保留。

---

## 2. 当前 `image_embedding` / `exact_image_knn_query` 的信号完全**没**喂到 SKU 选择里

我去看了索引 & 查询两端：

- `mappings/search_products.json:1054` 的 `image_embedding` 是 nested，每个元素 `{vector, url}`；索引时（`indexer/document_transformer.py:490-536`）把 SPU 主图 + SKU 图片 URL 合起来去重后逐个编码，**`url` 就是 `skus[].image_src` 里能对得上的那串**。
- `es_query_builder.py:193-281` 里 `build_image_knn_clause` 和 `build_exact_image_knn_rescore_clause` 都是 nested + `score_mode: max`，但**都没带 `inner_hits`**。所以 ES 只告诉我们"nested 里最高分是多少"，不告诉我们是哪条 url 命中的。
- `StyleSkuSelector` 当前完全靠"意图词 × `optionN_value` 文本匹配"选 SKU（`sku_intent_selector.py:252-284`），没有任何 image 通路。

也就是说：**"哪个 image_embedding 被召回 / 被 rescore 命中"这个你已经花代价算出来的信号，下游根本拿不到**。

---

## 3. 融合设计（建议方案）

我倾向**融合**而不是二选一，理由是两类信号各有盲区：

| 信号 | 擅长 | 盲区 |
|---|---|---|
| 文本款式意图（现有） | "白色"能命中 option `color=white` | 店家把颜色塞在 title 或图里、option 是数字码时失效 |
| 图片 KNN 最佳 url（新增） | 视觉接近 query（文本走 CN-CLIP 后也和图片空间对齐） | 可能选出"卖相最好"的主图，和明确的颜色诉求冲突 |

### 契约上的改动最小化：依旧 `SkuSelectionDecision` + `apply_precomputed_decisions`

只是在 prepare 阶段多一个候选源、decision 里多几个字段做 debug 和融合依据。

### 具体改动点

**a. `es_query_builder.py`：给两个 nested image 查询都加 `inner_hits`**

- `build_exact_image_knn_rescore_clause`：`{"inner_hits": {"name": "exact_image_knn_hits", "size": 1, "_source": ["url"]}}`
- `build_image_knn_clause`：`{"inner_hits": {"name": "image_knn_hits", "size": 1, "_source": ["url"]}}`

两个都加的原因：rescore 是 `in_rank_window` 才有；不在窗口内时还能吃到初召回的 inner_hits。代价：每个 hit 多返回一个 `{url, _score}`，很轻。

**b. `sku_intent_selector.py`：扩展 decision，新增 image-pick 通路**

```python
@dataclass(frozen=True)
class SkuSelectionDecision:
    ...                                   # 保留现有字段
    image_pick_sku_id: Optional[str] = None
    image_pick_url: Optional[str] = None
    image_pick_score: Optional[float] = None
    final_source: str = "text"            # "text" | "image" | "none"
```

- 新增 `_pick_sku_by_image(hit, source)`：读 `hit["inner_hits"]["exact_image_knn_hits"]`（缺省回退到 `image_knn_hits`），取 top url，在 `skus[]` 里按 `image_src` 归一化后等值匹配（注意处理 `//` 协议无关前缀、大小写、querystring 这些脏数据，按 indexer 那边的写入规则来）。
- `_select_for_source` 的新决策树：
  1. `text_pick` = 现在的文本匹配结果。
  2. `image_pick` = 上述 helper 的结果。
  3. 有款式意图：
     - `text_pick` 非空 → 选 `text_pick`，`final_source="text"`；把 `image_pick` 存进 decision 里仅用于 debug/验证（两者一致时在 debug 打标，不一致时也不翻盘——query 显式指名了颜色/尺码，视觉信号不该覆盖）。
     - `text_pick` 为空但 `image_pick` 非空 → 选 `image_pick`，`final_source="image"`。
  4. 无款式意图：
     - 若 `image_pick` 的分数 ≥ 阈值（配置项，例如 `query_config.image_sku_selection_min_score`），选 `image_pick`。否则不做 SKU 选择（保持 SPU 原顺序）。
- `_apply_decision_to_source` 无需大改，仍然是"把选中 SKU 顶到前面 + 替换 `image_url` 为该 SKU 的 `image_src`"；只是 `selected_sku_id` 的来源更丰富了。

**c. `searcher.py`：把 image-pick 路径也纳入 "prepare → apply"**

- 把"`_has_style_intent` 才 prepare"改成"`_has_style_intent` 或 `has_image_inner_hits` 就 prepare"。命名上把 `_apply_style_intent_to_hits` 改成 `_apply_sku_selection_to_hits` 更准确（可做可不做，看你对改动面范围的容忍度）。
- `_resolve_rerank_source_filter` 已经在有款式意图时补了 `skus`/`option*_name`；image-only 路径还需要 `skus`（至少 `sku_id`、`image_src`），所以触发条件要扩到"只要可能选 SKU"。
- inner_hits 会带在 top-level `hit` 上，不会被 page fill 的 `_source` 覆盖影响，但 page fill 用的是 `ids` 查询，**本来就不会有 inner_hits**。这正好符合现有契约：**prepare 时把 image_pick 落到 `SkuSelectionDecision` 里**，apply 阶段不再依赖 inner_hits，就不会丢。
- `context.store_intermediate_result("style_intent_profile", ...)` 附近把 image-pick 的诊断也写进 debug。

**d. 配置**

```yaml
query_config:
  image_sku_selection_enabled: true          # 总开关
  image_sku_selection_min_score: 0.55        # 无款式意图时的保护阈值
  image_sku_selection_when_text_missing: true  # 有意图但文本未命中时回退到图片
```

### 我为什么不建议"用图片分直接替换原策略"

两个理由：
1. 你现在的文本策略对明确的词面命中是**最可信**的信号；query 说"白色"，结果给了视觉更漂亮但色号不同的 SKU，这是产品上的倒退。
2. 图片 KNN 的 max score 在 nested score_mode=max 下是"最相似那张图的分"，但 CN-CLIP 的文本→图像相似度阈值校准很敏感，不同品类/拍摄风格分布差异大。拿来**做候选**稳，拿来**做唯一决定**不稳。

---

## 4. `query=白色T恤` 的实验

改完之后可以这样验证：

```bash
source activate.sh
python main.py search "白色T恤" --tenant-id 1 --size 10 --debug \
  | jq '.debug_info.per_result[] | {spu_id, style_intent_sku, first_sku: .skus[0]?}'
```

预期关注点：
- **有 color option 的 SPU**：`style_intent_sku.final_source == "text"`，并且 `image_pick_sku_id` 多数情况下与 text 选的一致（一致率是个好指标）。
- **没有 color option、但多图的 SPU**：`final_source == "image"`，`skus[0].image_src` 的图肉眼是白色。
- **两者冲突**：debug 里应能看到 `selected_sku_id != image_pick_sku_id` 的案例，确认当前默认"文本优先"没有被图片翻盘。
- 分别跑 `--debug` 对比改前/改后同一 query 的 `results[].image_url` 和 `skus[0].image_src`，观察白色主图替换的命中率提升。

如果想更系统，可以扩 `scripts/evaluation/` 里已有的回归 case，加一个"图片颜色是否匹配意图"的人工抽检指标。

---

## 5. 两个我想先和你确认的产品决策

1. **无款式意图时**，是否允许图片信号单独触发 SKU 置顶？（我倾向：允许，但带阈值保护，默认开。）
2. **文本命中 vs 图片命中冲突**时，是硬性文本优先，还是做分数融合（先各自 min-max 归一再取加权 max）？我倾向硬性文本优先——实现简单、行为可解释；融合方案留作后续 A/B。

告诉我你的取舍，我就按上面的改动清单一次性改掉（`es_query_builder.py` 两个 inner_hits、`sku_intent_selector.py` 扩字段 + 新通路、`searcher.py` 触发条件扩容 + 命名收敛、配置项、配套单测），并跑 `白色T恤` 的实际 debug 对照给你看。


# 2-prompt
1. 无款式意图时，允许图片信号单独触发 SKU 置顶

2. 文本命中，硬性文本优先，当匹配失败的时候（"selected_sku_id": null）使用图片最相近的

3. es中有这个字段enriched_taxonomy_attributes：
"enriched_taxonomy_attributes": [
{
"name": "Color",
文本匹配的时候，考虑更全面一点：
1）把这个字段考虑进去，enriched_taxonomy_attributes内Color对应的value匹配的时候
我在  source_fields:加了enriched_taxonomy_attributes之后好像还是没返回这个字段，你检查下是不是能拿到，可以考虑对query=卡其色裙子  首条结果验证下

4. 还是颜色意图文本匹配的问题 query=卡其色裙子的首条结果：
 "selected_sku_id": null,
"specifications": [
{
"value_text": {...},
"name": "颜色",
"sku_id": "447652",
"value_keyword": "卡其色v领收腰长裙【常规款】"
},

意图检测是成功的：
intent_detection:
active: yes
intents:
intent_type: color
dimension_aliases: color, colors, colour, colours, 颜色, 色, 色系
matched_term: 卡其色
canonical_value: beige

但是 "selected_sku_id": null,  颜色没有匹配上，value_keyword 包含了“卡其色”，没有匹配成功，因此也应该没有享受到提权




# 2-response
好，那就直接在当前 agent 模式下按计划施工。施工顺序按"先不破坏现有行为、再扩新通路"：

**Phase 1**（修复 + 显字段，低风险）
1. `api/models.py` 扩 `SpuResult` 两个富化字段
2. `api/result_formatter.py` 透传
3. `sku_intent_selector.py`：扩匹配词集合（zh+en+attr）+ 收敛为子串匹配；加 `enriched_taxonomy_attributes` 作为 SPU 级确认

**Phase 2**（image 回退通路）
4. `es_query_builder.py` 两个 nested image 查询加 `inner_hits`
5. `sku_intent_selector.py` 扩 `SkuSelectionDecision` + `_pick_sku_by_image` + text-first/image-fallback 决策树
6. `searcher.py` 触发条件扩为"款式意图 OR image inner_hits"，`_resolve_rerank_source_filter` 无意图时也带 `skus` 字段
7. `config.yaml` 加 `image_sku_selection.*`

**Phase 3**（调试 + 回归）
8. debug payload 扩 text_pick/image_pick/final_source
9. 单测补四个关键场景
10. 跑 `卡其色裙子` / `白色T恤` 对照

开始吗？两件事需要你拍板后我再动：

1. **`enriched_taxonomy_attributes` 的语义级别**：你要的是"作为另一个 SKU-级 value 匹配源"，还是我上面提议的"仅作 SPU 级确认信号，不直接决定 SKU"？我倾向后者——因为这个字段是 SPU 级的，不能单独指定某个 SKU。前者会带来"SKU A 其实不是卡其色、却被 SPU 级信号顶上去"的错案。

2. **image-pick 的分数阈值**：无意图时建议给个阈值（`min_score_no_intent`），有意图 fallback 时不加阈值。你接受这个不对称吗？接受的话我用 `0.55` 起步，上线后调。

你确认这两点，我就一把改完。

# 3-prompt
请综合考虑、全部完成，enriched_taxonomy_attributes的Color对应到值是和option1_name/option1_values/option2/option3一样对待的，就是当前使用的属性值匹配，多一个字段来源，即，即使对于颜色意图，同时把me对应的option value和enriched_taxonomy_attributes的Color对应到值一起判断，要考虑统一设计、精炼代码。image-pick设计和代码保持精炼，遵循统一的最优的设计，尽量不要fallback。

# 3-response
27/27 通过。

## 问题 2：是不是所有"选中"都享受 1.2x 提权？

**是的，目前无差别地都享受，这确实是个设计缺口。** 机制如下：

```490:490:search/rerank_client.py
    return bool(str(hit.get("_style_rerank_suffix") or "").strip())
```

```641:642:search/rerank_client.py
        sku_selected = _has_selected_sku(hit)
        style_boost = style_intent_selected_sku_boost if sku_selected else 1.0
```

只要 `prepare_hits` 给 hit 写了 `_style_rerank_suffix`（只要 `selected_text` 非空），下游 `run_rerank` / `run_lightweight_rerank` 就把 `style_boost=1.2` 乘进 fused score。  
当前 `_style_rerank_suffix` 的写入条件是 `decision.rerank_suffix` 非空 —— 等价于 `final_source ∈ {"text", "image"}`。也就是说：

| 选中来源 (`final_source`) | 是否吃 1.2x 款式提权（现状） |
|---|---|
| `text`（自有值命中意图词） | ✅ |
| `image`（仅图像相似度置顶） | ✅ ← 语义上不该等价 |
| `none` | ❌ |

把"图像置顶"和"文本命中"按同一强度做 SPU 级提权，存在两个隐患：
1. **置信度不同**：文本命中是显式属性匹配（强信号），图像 pick 是基于视觉相近（弱信号、还可能是 SPU 主图而非 SKU 图）；给同样 1.2x 会让"图像近似 → SPU 排名拔高"发生在没有真正属性命中的情况下。
2. **无意图纯图像触发时更夸张**：query 无任何款式意图，但只要 image_knn 的 top url 能对上某个 SKU，整条 SPU 就吃到 1.2x，等于让向量相似度二次乘进融合。

建议的区分策略（**只改 rerank_client**，不改决策侧）：

- 文本命中（`final_source="text"`）→ 保留 1.2x（可配置）
- 图像命中（`final_source="image"`）→ 1.0x（不加提权，仅做 SKU 置顶 + 改封面图）

最简实现：把"是否命中"从"后缀是否非空"改为"后缀非空 AND 是文本来源"。具体做法：

1. `prepare_hits` 只在 `final_source == "text"` 时写 `_style_rerank_suffix`；`image` 时不写，但仍把决策放进 `decisions`（`apply_precomputed_decisions` 照常置顶 SKU + 换封面图）。
2. 对应的 rerank doc 后缀也就不含图像源的颜色/尺码值（避免把一个"靠图选的 SKU"当作文本证据喂给 reranker）。

要我现在就把这条改了吗？或者你更希望 image 源也吃提权但用一个更小的系数（比如 1.05），这样保留视觉加分但不等同于文本命中。

# 4-prompt
两个问题，请修复：
1. option匹配，应该要能成功匹配才对： 
{
"sku_id": "441679",
"price": 13,
"compare_at_price": 0,
"sku_code": "",
"stock": 100,
"weight": 0,
"weight_unit": "kg",
"option1_value": "卡其色（无内衬）",

2. 即使option没有匹配、而是依靠enriched_taxonomy_attributes 匹配的，那么rerank_suffix selected_text 也要翻译真实的匹配项，final_source也最好 enriched_taxonomy_attributes 和option有所区分
Selected SKU
{
  "selected_sku_id": "441670",
  "rerank_suffix": "白色（无内衬）",
  "selected_text": "白色（无内衬）",
  "final_source": "text",

# 4-response
## 改动摘要

### 问题 1（option 匹配）
我上一条消息里修的 "SKU 自有值优先于 taxonomy" 就解决了这个问题。用你给的样本跑：

```
query=卡其色裙子
skus=[441670:白色（无内衬）, 441679:卡其色（无内衬）]
→ selected_sku_id: 441679
  selected_text: 卡其色（无内衬）
  final_source: option
```

全角括号之所以能命中是两个机制叠加：
- 全角 `（` 不在 `[\u4e00-\u9fff]` 范围内，分词器按 `["卡其色", "无内衬"]` 切分；
- 纯中文词 `卡其色` 落入 "pure-Han 子串回退" 分支，即使分词切分不一致也能匹配。

加了一个贴着真实数据命名的回归 case `test_zh_color_intent_matches_noisy_option_value_with_fullwidth_parens` 锁死。

### 问题 2（区分 taxonomy / option，`selected_text` 反映真实匹配项）

**决策结构扩展**：

```python
SkuSelectionDecision:
    final_source: "option" | "taxonomy" | "image" | "none"
    matched_sources: Dict[intent_type, "option"|"taxonomy"]   # 逐意图细分
    selected_text / rerank_suffix: 真正命中的原始文本（option 源为 sku.optionN_value；taxonomy 源为 enriched_taxonomy_attributes 对应 value）
```

**匹配逻辑**（`_find_text_matched_skus`）：per-intent 走两条互斥路径，保留权威原则：
- SKU 在该维度有自有值 → 只用自有值判断 → 命中时 `source=option`
- SKU 该维度值为空（槽未解析 或 optionN 值空）→ 用 SPU taxonomy 判断 → 命中时 `source=taxonomy`

**整体来源聚合**：任一意图靠 taxonomy 命中 → 整体 `final_source="taxonomy"`；全部由 option 命中 → `final_source="option"`。这保留了"最弱文本证据"的含义，下游（比如 rerank boost）可以据此做差异化。

**真实匹配项回填示例**：
- `option1_name=颜色, option1_value=卡其色（无内衬）`：`selected_text="卡其色（无内衬）"`，`final_source=option`
- `option1_name=Style, enriched_taxonomy_attributes=[{Color, {zh:"卡其色"}}]`：`selected_text="卡其色"`（来自 taxonomy 原文），`final_source=taxonomy`

### 与 1.2x 款式提权的衔接（回答你上一条的后半问）

当前 `rerank_client` 判断 "是否选中" 的依据仍是 `_style_rerank_suffix` 是否非空——也就是说 `option` / `taxonomy` / `image` 三种来源目前仍一视同仁都吃 1.2x。**现在有了 `final_source` 作为可区分的信号，你想怎么分级，我马上就能改**。常见选择：

- `option`：1.2x（强信号）
- `taxonomy`：1.1x（SPU 级，弱一档）
- `image`：1.0x（不加提权，仅做 SKU 置顶 + 换封面）

要不要我把这三档都拉到 `config.yaml` 里做成可配？或者先把 image 档位降到 1.0x、其它保持不变？

# 5-prompt
q=藏青百褶半身裙
第五条结果
  "spu_id": "68501",

Selected SKU
{
  "selected_sku_id": null,
  "rerank_suffix": "",
  "selected_text": "",
  "final_source": "none",
  "matched_sources": {},
  "resolved_dimensions": {
    "color": "option1_value"
  },
  "image_pick": null
}
为什么image_pick也没生效

# 5-response

## 为什么当时 `image_pick` 也是 `null`

`image_pick` **只**来自 ES 返回的 `inner_hits`（`exact_image_knn_query_hits` 优先，其次 `image_knn_query_hits`），再用其中的 **URL** 去对齐 `skus[].image_src`。下面任一情况都会变成 `null`：

1. **没有 `inner_hits`**  
   - 例如：该文档不在 **exact KNN rescore** 的 `window_size` 内（若 `exact_knn_rescore_window` 配得很小，排第 5 条也可能拿不到 exact 的 inner_hits）。你这边配置是 160，第 5 条一般仍在窗口内。  
   - 或：召回阶段 **nested image KNN** 对该父文档没有产生 inner_hits（只靠文本等子句进结果时，有时 nested 子句侧没有可返回的条目）。

2. **有 inner_hits，但 URL 对不上任何 SKU**（最常见）  
   - `inner_hits` 里是 `https://cdn/a.jpg?w=800`，SKU 里是 `https://cdn/a.jpg`  
   - 或 CDN 域名不同、路径相同  
   - 或 `_source.url` 与索引里 `image_embedding` 的 canonical 写法不一致，而 SKU 用的是另一种写法  

3. **以前预取 `_source` 里没有 `image_embedding`**  
   - ES 在 inner_hit 里给了 `_nested.offset`，但 `_source.url` 不可靠时，**无法**用 `image_embedding[offset].url` 再解析一次。

---

## 本次代码上做了什么（已提交到当前分支）

1. **URL 对齐放宽**：去掉 query、路径归一、`host+path` / 仅 path / 文件名 多级比对，减少「明明是一张图却匹配不上」。  
2. **`_nested.offset` + `image_embedding[offset].url` 回退**：inner_hit 里若带 offset，会同时尝试 `_source.url` 和父文档里 `image_embedding[offset].url`（与索引侧一致）。  
3. **`ImagePick.url`** 改为记录**真正用来对齐成功的那条候选 URL**（便于 debug）。  
4. **Searcher**：在存在 **图像查询向量**（`_has_image_signal`）时，预取 `_source` 里增加 **`image_embedding`**，否则 offset 回退拿不到数据。

这样在「藏青百褶半身裙」这类 **文本未命中 SKU、指望图像兜底** 的场景里，只要 ES 给了 inner_hits 且最终能和我们归一化后的 SKU 图对齐，`image_pick` 就会非空；若仍为空，请在 **debug** 里看该条 hit 是否还有 `inner_hits`，以及 `exact_knn_rescore_window`、是否关掉了 rescore 等。

---

## 关于「藏青」文本为什么也可能没命中

词表里 **navy** 已含「藏青、藏蓝、深蓝」。若 SKU 的 `option1_value` 写成 **和词表不一致** 的表述（例如只有英文色号、或「青色系」等），仍可能文本全挂，这时更依赖上面的 **图像对齐**；若图像也对不上，就会是 `final_source: none` + `image_pick: null`。


# 6-prompt

# 6-response


# TODO 


                "inner_hits": {
                  "name": "exact_image_knn_query_hits",
                  "size": 1,
                  "_source": [
                    "url"
                  ]
                }

inner_hits获取exact_image_knn_query_hits的目的是：是因为哪个

参考：
    feat(search): 款式意图下统一 SKU 选取（option/taxonomy/图像）与属性值匹配增强
    
    ## 主要能力
    - 在 rerank 窗口内对 hits 做 SKU 预决策：款式意图（多源同义词）+ 图像 KNN inner_hits URL 对齐 SKU.image_src，统一一次决策
、无级联 fallback。
    - 区分文本证据强度：final_source ∈ {option, taxonomy, image, none}；matched_sources 按意图记录 option 或 taxonomy；selected_text / rerank_suffix 回填真实命中片段（SKU option 原文或 taxonomy value 原文）。
    - 权威规则：SKU 在已解析维度上有非空 option 值时仅以该值参与匹配；SPU 级 enriched_taxonomy_attributes 不覆盖与之一致的 SKU 级矛盾值（修复「taxonomy 把白色 SKU 当卡其色命中」）。
    - 图像：nested image KNN / exact rescore 增加 inner_hits（url），用于 SKU 置顶时的视觉 tie-break（仅在文本命中集内）或无意图时纯图像置顶。
    - 查询侧：DetectedStyleIntent 增加 all_terms（zh+en+attribute 并集），属性值匹配与意图词表一致。
    - API：SpuResult 透出 enriched_attributes / enriched_taxonomy_attributes（避免 Pydantic 丢弃 ES 字段）。
    
    ## 属性值匹配（括号和分隔符）
    - 在分词前对归一化后的 option/taxonomy 字符串执行 _with_segment_boundaries_for_matching：将全/半角括号、斜杠、顿号、中英文标点、中点、各类横线等替换为空格，再 simple_tokenize + 滑窗；无分隔的连续汉字仍走纯中文子串回退（如 卡其色棉）。
    - 参数化测试覆盖多种括号与常见电商分隔写法。
    
    ## 编排与配置
    - searcher：_should_run_sku_selection = 款式意图激活 或 存在 image_query_vector；prefetch _source 含 skus、option 名、enriched_taxonomy_attributes。
    - es_query_builder：image knn / exact image rescore 的 nested 子句带 inner_hits。
    
    ## 测试与仓库
    - tests/test_sku_intent_selector.py、tests/test_search_rerank_window.py 更新；移除已废弃的 embedding-fallback 集成断言。
    - .gitignore：忽略 artifacts/search_evaluation/datasets/（本地评估大数据集，避免误提交）。


  "rescore": {
    "window_size": 160,
    "query": {
      "score_mode": "total",
      "query_weight": 1,
      "rescore_query_weight": 0,
      "rescore_query": {
        "bool": {
          "should": [
            {
              "script_score": {
                "_name": "exact_text_knn_query",
                "query": {
                  "exists": {
                    "field": "title_embedding"
                  }
                },
                "script": {
                  "source": "((dotProduct(params.query_vector, 'title_embedding') + 1.0) / 2.0) * params.boost",
                  "params": {
                    "boost": 4
                  }
                }
              }
            },
            {
              "nested": {
                "path": "image_embedding",
                "_name": "exact_image_knn_query",
                "score_mode": "max",
                "query": {
                  "script_score": {
                    "query": {
                      "exists": {
                        "field": "image_embedding.vector"
                      }
                    },
                    "script": {
                      "source": "((dotProduct(params.query_vector, 'image_embedding.vector') + 1.0) / 2.0) * params.boost",
                      "params": {
                        "boost": 4
                      }
                    }
                  }
                },
                "inner_hits": {
                  "name": "exact_image_knn_query_hits",
                  "size": 1,
                  "_source": [
                    "url"
                  ]
                }
              }
            }
          ],
          "minimum_should_match": 1
        }
      }
    }
  },
