# 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 } } } },