增加款式意图识别模块。意图类型: 颜色,尺码(目前只需要支持这两种)
一、 意图判断
- 意图召回层: 每种意图,有一个召回词集合 对query(包括原始query、各种翻译query 都做匹配)
- 以颜色意图为例: 有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 query匹配了其中任何一个词,都认为,具有颜色意图 匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。
二、 意图使用: 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter 要改为:
- 有款式意图的时候,才做sku筛选
- sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选
- 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。
- 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用:
- 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url
- 用来做rerank doc的title补充,从而参与rerank
- 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用:
- Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀)
- sku筛选的规则也要优化:
现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。
改为:
- 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。
- 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。
- 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 这个sku筛选也需要提取为一个独立的模块。
- 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。
细节备注: intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 在重排窗口内,第一次 ES 查询会把 source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*name。因此,有意图的时候,需要给这一次的source加上 skus / option*name
先仔细理解需求,查看代码,深度思考应该如何设计,和当前的系统较好的融合,给出统一的设计,可以根据需要适当改造当前的实现,降低整个系统的复杂度,提高模块化程度,而不是打补丁。修改后的最终状态应该是要足够简单、清晰、无冗余和分叉,模块间低耦合。多步思考确认最佳施工方案之后才进行代码修改。
- TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 skufilter_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阶段耗时太长。请根据需求,思考优化的方法,给出性能优化的方案。
- _select_by_embedding,有缓存吗,option_value的值是有限的,之前已经算过的,就不用再算了。不仅仅是embedding相似的结果,整个option_value的匹配结果,是有包含、还是没包含,相似度多少,都不用重新计算。比如之前已经有一个sku的某个属性值叫做“卡其色”,已经算出来是否文本匹配了,那么不需要再去做文本匹配。如果已经算出来向量的相似度,那么不需要再去取向量以及计算相似度。
- 匹配可以适当的优化: 匹配流程简化: 1)找到第一个文本匹配的,如果有直接匹配成功。不需要考虑匹配多个的情况。 2)如果全部都没有匹配,那么进行embedding筛选。
匹配规则: option_name的匹配,直接看规范化后的option_name是不是意图维度的泛化词之一(比如颜色、color、colour),如果没有匹配的,现在应该是把所有维度都算上,这样匹配成本和比较成本太高了,去掉这些逻辑,这种情况不需要加后缀、不需要选择sku。 ption_value的匹配。意图检测的时候,有匹配的query中的命中的词,这个词被包含在属性值中,那么就算匹配。属性值被包含在query(包括翻译文本)中,也算匹配。提高匹配的覆盖率。
- 这一阶段得到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)大致是:
QueryParser.parse→ParsedQuery(含translations、query_tokens等)- 组 ES 查询;若在重排窗口内,第一次查询把
_source裁成「重排模板所需字段」(_resolve_rerank_source_filter) - ES 搜索 →
run_rerank(rerank_client.build_docs_from_hits用{title}等拼 doc) - 按
from/size切片 → page fill 用ids查询把当前页_source补全 _apply_sku_sorting_for_page_hits(仅 option1,先子串包含命中第一个,否则全量 option1 embedding)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.py269–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 顺序不能当作最终真相。
推荐契约(降低复杂度):
- Rerank 前:对 window 内每个 hit 计算
SkuIntentDecision(至少包含:option_slot1/2/3、candidate_sku_index或sku_id、rerank_suffix字符串)。可挂在 hit 的非 ES 字段上,例如hit["_intent_sku"] = {...}(或只存rerank_doc_text全文)。 run_rerank:build_docs_from_hits若发现 hit 上已有rerank_doc_text(或style_suffix+ 模板),则优先使用,否则走原模板。- 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 与「未匹配维度名」
需求逻辑可落到纯函数:
- 对每个意图类型,有 维度别名集合(如 color)。
- 依次与
option1_name、option2_name、option3_name(字符串,注意多语言:与 indexer 一致,可能是纯英文或中文)做 casefold / 规范化 后匹配别名表。 - 命中则该 SKU 行的匹配字段为
option{k}_value;用于 embedding key 时继续用name:value形式(沿用现有_sku_option1_embedding_key思路,泛化为option_slot)。 - 若三个 name 都不匹配意图维度:用
option1_value、option2_value、option3_value空格拼接成一条「兜底描述字符串」,供:
- 与 query 的包含/泛化/embedding 比较;
- 作为
rerank_suffix的一部分(若你希望无明确维度时仍加强 rerank)。
- 与 query 的包含/泛化/embedding 比较;
多意图同时存在(如同时颜色+尺码):需要在产品层定规则,例如:
- 只对「主意图」排序(配置优先级 color > size),或
- 要求两个维度都满足的 SKU 优先,否则退化为单意图。
实现上可在 SkuIntentSelector 输入 List[IntentType] 与策略枚举,避免写死 if-else 散落。
7. 三轮 SKU 匹配规则(独立模块内)
从当前「第一个包含就返回」改为:
- 第一轮:统计「option 匹配文本被 整条 query 文本 包含」的 SKU(或对每个 query 变体分别计,再合并——建议与你现有
_build_sku_query_texts对齐);若恰好 1 个 → 选中。 - 第二轮:若 0 个,对每个 SKU 的候选词走 取值泛化表(同义词行),再跑包含判断;仍统计「多个 / 零个」。
- 第三轮:
- 若 多个 满足包含(第一轮或第二轮)→ 仅在这多个上算 embedding,取相似度最高;
- 若 仍 0 个 → 对 全部 SKU 算 embedding,取最高。
- 若 多个 满足包含(第一轮或第二轮)→ 仅在这多个上算 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,模块接口会很好稳定下来。