diff --git a/docs/issues/issue-2026-04-14-粗排环节增加启发式调序-0420.md b/docs/issues/issue-2026-04-14-粗排环节增加启发式调序-0420.md new file mode 100644 index 0000000..336fe02 --- /dev/null +++ b/docs/issues/issue-2026-04-14-粗排环节增加启发式调序-0420.md @@ -0,0 +1,25 @@ +需求: +目前160条结果(rerank_window: 160)会进入重排,重排中 文本和图片向量的相关性,都会作为融合公式的因子之一(粗排和reranker都有): +knn_score +text_knn +image_knn +text_factor +knn_factor +但是文本向量召回和图片向量召回,是使用 KNN 索引召回的方式,并不是所有结果都有这两个得分,这两项得分都有为0的。 +为了解决这个问题,有一个方法是对最终能进入重排的 160 条,看其中还有哪些分别缺失文本和图片向量召回的得分,再通过某种方式让 ES 去算,或者从 ES 把向量拉回来,自己算,或者在召回的时候请求 ES 的时候,就通过某种设定,确保前面的若干条都带有这两个分数,不知道有哪些方法,我感觉这些方法都不太好,请你思考一下 + +考虑的一个方案: +想在“第一次 ES 搜索”里,只对 topN 补向量精算,考虑 rescore 或 retriever.rescorer的方案(官方明确支持多段 rescore/支持 score_mode: multiply,甚至示例里就有 function_score/script_score 放进 rescore 的写法。) +这意味着你完全可以: +初检仍然用现在的 lexical + text knn + image knn 召回候选 +对 window_size=160 做 rescore +用 exact script_score 给 top160 补 text/image vector 分 +顺手把你现在本地 coarse 融合迁回 ES + +export ES_AUTH="saas:4hOaLaf41y2VuI8y" +export ES="http://127.0.0.1:9200" +"index":"search_products_tenant_163" + +有个细节暴露出来了:dotProduct() 这类向量函数在 script_score 评分上下文能用,但在 script_fields 取字段上下文里不认。所以如果我们要把 exact 分顺手回传给 rerank,用 script_fields 的话得自己写数组循环,不能直接调向量内建函数。 + +重排打分公式需要的base_query base_query_trans_zh knn_query image_knn_query还能不能拿到?请你考虑,尽量想想如何得到这些打分,如果实在拿不到去想替代的办法比如简化打分公式。 diff --git a/docs/issues/issue-2026-04-16-bayes寻参-TODO.md b/docs/issues/issue-2026-04-16-bayes寻参-TODO.md deleted file mode 100644 index 0630e64..0000000 --- a/docs/issues/issue-2026-04-16-bayes寻参-TODO.md +++ /dev/null @@ -1,508 +0,0 @@ - - -一、扩展评估标注集 -二、在大标注集上寻参 -三、(暂时不做,克制,业务发展初期不要做!做通用性强维护成本低的!)coarse rank使用LTR(各个因子做多个非线性映射+FM拟合 pairwise,参考ranknet) - - - -0、得到all_keywords.txt.top1w.shuf.top1k.clothing_filtered(done) - -方法1(目前这么做的): -用awk,读取not_clothing.txt作为set,对all_keywords.txt.top1w.shuf.top1k每一行,如果该行在set中,则过滤,得到过滤后的文件,生成文件:all_keywords.txt.top1w.shuf.top1k.clothing_filtered - -方法2: -scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k -这个文件还混杂了一些非“服饰鞋帽”类搜索词,请先做一遍清理。 -用llm做剔出,每次输入50条,提示词是: -Please filter out the queries from the following list that do not belong to the clothing, shoes, and accessories category. Output the original list of queries, one query per line, without any additional content. - -然后将返回的,从原始query剔出。 -生成文件:all_keywords.txt.top1w.shuf.top1k.clothing_filtered - - - -一、扩展评估标注集 - -参考当前的评估框架 -@scripts/evaluation/README.md @scripts/evaluation/eval_framework/framework.py -@start_eval.sh.sh -当前,是基于54个评测样本(queries.txt),建立了自动化评估的系统,便于发现策略在这个评估集上的效果。 - -我需要扩大评估样本,使用all_keywords.txt.top1w.shuf.top1k.clothing_filtered(771条)为query集合,走标注流程,从而新建一个标注集。 -那么以后eval-web服务,现在的Batch Evaluation按钮,应该支持多个评估集合,左侧的History,也有对应多个评估集合的评估结果,请你考虑如何支持、如何设计。请进行统一的设计,不要补丁式的支持。 - -统一设计方案(2026-04-17) - -先校正一下现状口径: - -- `scripts/evaluation/queries/queries.txt` 当前仓库里是 53 条非空 query,不是 54 条。 -- `scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k.clothing_filtered` 当前是 771 条。 - -当前实现的问题,不只是 UI 没有下拉框,而是“评估集”这个概念在系统里还不是一等公民: - -- 配置层只有一个全局 `search_evaluation.queries_file` -- Web UI 左侧 Queries/History 默认只服务这一份 query 文件 -- `batch_runs` / `build_runs` 历史记录没有 `dataset_id` -- 产物目录是全局平铺的 `batch_reports/`、`query_builds/` -- `start_eval.sh` / `start_eval_web.sh` / `tune_fusion.py` 都是通过 `queries_file` 隐式指定评估集 -- `--reset-artifacts` 现在会清空整套 SQLite + query_builds,多评估集后这个语义会变得危险 - -所以这里要做的,不是“给 batch API 多传一个文件路径”,而是把“评估集”抽成贯穿配置、存储、API、UI、产物、调参脚本的一层统一模型。 - -设计目标 - -1. 一个 eval-web 服务同时支持多个评估集。 -2. Batch Evaluation、History、调参任务都必须明确绑定某个评估集。 -3. 历史结果必须可追溯到“当时到底用了哪一批 query”,不能因为 query 文件后续变更而失真。 -4. 相同 `(tenant_id, query, spu_id)` 的标签尽量复用,不因为 query 同时出现在两个评估集里就重复标注。 -5. 扩展到第三个、第四个评估集时,不需要再改表结构思路或前端交互模型。 - -核心抽象:区分“评估集”与“标签缓存” - -- 评估集(Evaluation Dataset):一组有稳定 `dataset_id` 的 query 集合,用来驱动 build、batch、history、调参。 -- 标签缓存(Label Cache):对 `(tenant_id, query_text, spu_id)` 的相关性判断结果。 - -这两者不要混为一谈。 - -建议保留现有 `relevance_labels` / `rerank_scores` 的“按 query 共享缓存”设计,不按 dataset 拆表,原因: - -- 同一个 query 如果同时属于 `core_queries` 和 `clothing_top771`,其 `(query, spu_id)` 标签语义本质相同,应该复用。 -- 这样新增大评估集时,只需要补齐新 query 的标签,不会对已有 query 重复做 LLM 标注。 -- 真正需要 dataset 维度的是:运行历史、构建历史、覆盖率统计、产物归档、UI 选择上下文。 - -配置设计 - -把当前单一 `queries_file` 升级为“评估集注册表”。建议在 `config.yaml` 中变成: - -```yaml -search_evaluation: - artifact_root: artifacts/search_evaluation - default_dataset_id: core_queries - datasets: - - dataset_id: core_queries - display_name: Core Queries - description: Legacy baseline query set from queries.txt - query_file: scripts/evaluation/queries/queries.txt - tenant_id: "163" - language: en - enabled: true - - dataset_id: clothing_top771 - display_name: Clothing Filtered 771 - description: 771 filtered clothing/shoes/accessories queries - query_file: scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k.clothing_filtered - tenant_id: "163" - language: en - enabled: true - - # 保留这些作为全局默认值;dataset 没显式覆盖时继承 - batch_top_k: 100 - audit_top_k: 100 - build_search_depth: 1000 - build_rerank_depth: 10000 -``` - -建议点: - -- `dataset_id` 是稳定主键,前后端、SQLite、历史记录、调参脚本都只认它,不认文件路径。 -- `query_file` 只是这个 dataset 当前版本的来源,不是外部协议的一部分。 -- 继续保留全局默认参数;以后如果某个 dataset 需要特殊 top_k / language,再支持局部覆盖。 -- 为兼容老脚本,可暂时保留 `queries_file`,但只作为 fallback,在 loader 里自动转换成一个隐式 dataset;新代码不再直接依赖它。 - -产物目录设计 - -当前所有 batch 报告都平铺在 `artifacts/search_evaluation/batch_reports/` 下,后面 dataset 一多会很乱。建议改成“共享缓存 + dataset 独立产物目录”: - -```text -artifacts/search_evaluation/ - search_eval.sqlite3 # 共享标签缓存/共享 rerank 缓存/运行索引 - datasets/ - core_queries/ - batch_reports/ - / - report.json - report.md - config_snapshot.json - dataset_snapshot.json - queries.txt - query_builds/ - .json - audits/ - ... - clothing_top771/ - batch_reports/ - / - ... - query_builds/ - .json - audits/ - ... -``` - -重点是每次 batch/build 都要固化 dataset snapshot: - -- `dataset_id` -- `display_name` -- `query_file` -- `query_count` -- `query_sha1` -- 当次实际 queries 副本 `queries.txt` - -这样即使以后 `all_keywords...clothing_filtered` 文件被重新清洗、条数变化,历史 batch 仍然可复现“当时到底评了哪些 query”。 - -SQLite / 存储层设计 - -共享缓存表可以继续保留: - -- `relevance_labels(tenant_id, query_text, spu_id, ...)` -- `rerank_scores(tenant_id, query_text, spu_id, ...)` -- `query_profiles(tenant_id, query_text, prompt_version, ...)` - -需要升级的是运行历史表: - -1. `build_runs` 增加 - - `dataset_id` - - `dataset_display_name` - - `dataset_query_file` - - `dataset_query_count` - - `dataset_query_sha1` - -2. `batch_runs` 增加 - - `dataset_id` - - `dataset_display_name` - - `dataset_query_file` - - `dataset_query_count` - - `dataset_query_sha1` - -3. `list_batch_runs()` / `get_batch_run()` / `insert_batch_run()` 全部变成 dataset-aware - -4. 覆盖率统计接口按 dataset 聚合,而不是简单按全库 query 聚合 - - - 当前 `list_query_label_stats(tenant_id)` 是“全量 query_text 分组” - - 以后应该是“给定 dataset_id 后,只统计该 dataset queries 的覆盖情况” - -这里建议不要额外把 query 全量写进 SQLite 做注册表主数据,query 主数据仍从 config + query_file 解析即可;SQLite 只负责记录 run 时的 snapshot 元数据。 - -API 设计 - -建议把 Web API 升级成以 dataset 为主轴,而不是默认只服务一个 `query_file`: - -1. `GET /api/datasets` - -返回所有可用评估集: - -- `dataset_id` -- `display_name` -- `description` -- `query_count` -- `query_file` -- `tenant_id` -- `language` -- `coverage_summary` - -2. `GET /api/datasets/{dataset_id}/queries` - -返回该 dataset 的 query 列表,以及 dataset 元信息。 - -3. `POST /api/search-eval` - -请求体增加可选 `dataset_id`。 - -- 单 query 评估本身仍然可以支持任意 query 文本 -- 但当页面处于某个 dataset 上下文时,返回里也带上该 dataset 信息,便于 UI 一致展示 - -4. `POST /api/batch-eval` - -请求体优先使用 `dataset_id`,不再默认依赖服务启动时绑定的唯一 `query_file`。 - -建议请求模型变成: - -```json -{ - "dataset_id": "clothing_top771", - "top_k": 100, - "auto_annotate": false, - "language": "en", - "force_refresh_labels": false -} -``` - -`queries` 字段可保留为高级/调试能力,但 UI 主路径和调参脚本主路径都应该走 `dataset_id`。 - -5. `GET /api/history?dataset_id=clothing_top771&limit=20` - -History 默认按当前 dataset 过滤;如有需要再支持 `all=true` 看全量。 - -6. `GET /api/history/{batch_id}/report` - -返回报告时补充 dataset 元信息,前端 report modal 里能看到这是哪个 dataset 的报告。 - -前端 / eval-web 交互设计 - -现在左侧栏写死了: - -- Queries 来自 `queries.txt` -- History 没有 dataset 维度 - -建议改成三层结构: - -1. 左上增加 Dataset Selector - -- 下拉框或 tabs,显示 `Core Queries (53)`、`Clothing Filtered 771 (771)` -- 当前选中的 dataset 决定左侧 query 列表和默认 history 过滤 - -2. Queries 区域绑定当前 dataset - -- 标题显示 dataset 名称 + query 数 -- 副标题显示 query 文件路径 -- 点击 query 触发单 query 评估 - -3. History 区域绑定当前 dataset - -- 默认只显示当前 dataset 的 batch history -- 每个 item 显示 `dataset badge + batch_id + created_at + query_count + primary metrics` -- 可选再加一个 “All Datasets” 开关,但默认视角一定要是“当前 dataset” - -4. 主区 Batch Evaluation 按钮绑定当前 dataset - -- 点击时执行当前 dataset 的 batch,而不是对服务启动时唯一 query_file 执行 -- 按钮文案建议带上 dataset 名,例如:`Batch Evaluate: Clothing Filtered 771` - -5. 页面顶端增加当前 dataset 概览卡片 - -- `dataset_id` -- query 数 -- 已有标签 query 数 / 覆盖率 -- 最近一次 batch 时间 - -这样进入页面时,用户始终知道自己正在看哪个评估集,不会把 53 条基线集和 771 条大集合的结果混在一起。 - -CLI / 启动脚本设计 - -需要把 `--dataset-id` 提升为第一入口参数: - -- `build_annotation_set.py build --dataset-id clothing_top771` -- `build_annotation_set.py batch --dataset-id clothing_top771` -- `build_annotation_set.py audit --dataset-id clothing_top771` -- `serve_eval_web.py serve --dataset-id core_queries` - -说明: - -- `serve` 的 `--dataset-id` 只决定页面初始选中哪个 dataset,不应该再把整个服务绑定死到一个 query 文件。 -- `--queries-file` 可以保留一段时间做兼容,但内部先解析 registry;如果能映射到某个 dataset,就统一转成 `dataset_id` 处理。 - -`start_eval.sh` / `start_eval_web.sh` 也要同步升级: - -- 读取 `REPO_EVAL_DATASET_ID` -- 保留 `REPO_EVAL_QUERIES` 兼容模式,但新用法优先 `REPO_EVAL_DATASET_ID` - -额外要修正的一点: - -- 当前 `--reset-artifacts` 会删整个 SQLite 和整个 `query_builds/` -- 多 dataset 后这个行为太危险 -- 应拆成更明确的选项,例如: - - `--reset-dataset-build-artifacts` - - `--purge-shared-label-cache`(显式危险操作,默认不要碰) - -调参框架联动设计 - -`tune_fusion.py`、`start_coarse_fusion_tuning_long.sh`、`resume_coarse_fusion_tuning_long.sh` 也必须带 dataset 维度,否则之后同一套 coarse rank 参数可能分别在 53 条集和 771 条集上跑出完全不同的结论,但 leaderboard 会混在一起。 - -建议: - -- `tune_fusion.py` 增加 `--dataset-id` -- `summary.json` / `leaderboard.csv` / `trials.jsonl` 记录 `dataset_id` -- 调参时调用 eval-web batch API,也传 `dataset_id` -- `seed-report` 如果来自历史 batch 报告,也校验 `dataset_id` 一致 - -迁移方案 - -建议采用兼容迁移,而不是硬切: - -1. 先在配置中注册两个 dataset - -- `core_queries` -> `scripts/evaluation/queries/queries.txt` -- `clothing_top771` -> `scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k.clothing_filtered` - -2. 旧历史记录回填 dataset 元信息 - -- 如果历史记录没有 `dataset_id`,且 query 列表 hash 与 `queries.txt` 一致,则回填为 `core_queries` -- 无法确认的旧记录,标记为 `legacy_unknown` - -3. UI 默认只展示 registry 中 `enabled=true` 的 dataset - -4. 保留一段时间旧 CLI 参数,但 README、新脚本、新前端只文档化 dataset 模式 - -实施顺序 - -建议按下面顺序做,避免半途出现“后端支持了但前端看不出来”或者“前端能选但历史存不准”: - -1. 配置层:引入 dataset registry 与解析器 -2. 公共帮助层:统一的 dataset resolve / snapshot / artifact path helper -3. SQLite:`batch_runs` / `build_runs` 增加 dataset 元字段 -4. Framework:`build` / `batch` / `audit` 全面改为 dataset-aware -5. Web API:新增 `/api/datasets`,History 支持 dataset filter -6. eval-web 前端:selector + dataset-scoped queries/history/batch -7. 调参脚本:`--dataset-id` 全链路打通 -8. README / issue / 运维脚本更新 - -这套设计的关键点 - -- “评估集”是显式主键,不再靠文件路径暗示 -- “标签缓存”继续按 `(tenant_id, query, spu_id)` 共享复用 -- “历史报告”按 dataset 严格隔离并带 snapshot -- “UI 交互”始终围绕当前 dataset 上下文展开 -- “调参结果”必须标记 dataset,防止不同集合上的指标被误比 - -结论 - -这件事的统一做法,不是给现有单评估集逻辑加几个 if/else,而是把 eval framework 从“单 query 文件模式”升级为“多 dataset registry 模式”。 - -如果按这套方案落地,后面新增第三个评估集时,应该只需要: - -1. 在 `config.yaml` 注册一个新 dataset -2. 跑对应 build -3. 在 UI 中选择它做 batch / 看 history -4. 在调参脚本里指定 `--dataset-id` - -而不需要再次改数据模型和交互模型。 - - - -二、在大标注集上寻参 - -我以前经过过一轮调参,是基于54个评测样本(queries.txt),过程中发现的最优的参数是这一组: -0.641241 {'es_bias': '7.214', 'es_exponent': '0.2025', 'text_bias': '4.0', 'text_exponent': '1.584', 'text_translation_weight': '1.4441', 'knn_text_weight': '0.1', 'knn_image_weight': '5.6232', 'knn_tie_breaker': - '0.021', 'knn_bias': '0.0019', 'knn_exponent': '11.8477', 'knn_text_bias': '2.3125', 'knn_text_exponent': '1.1547', 'knn_image_bias': '0.9641', 'knn_image_exponent': '5.8671'} - -这一组参数分布比较极端,text_bias太大(文本项得分事0~1的,加上4被稀释的很大),图片的exponent太大,不过在这个数据集上面确实是最好的,我觉得有过拟合的可能,因此要扩大数据集,先扩展标注集,然后使用扩展的标注集,继续进行寻参。 - -我已经新建了一个标注集合,请启动寻参任务,把任务跑起来,以后等程序跑完了应该能拿到寻参结果,下次你可以结合程序执行的结果进行结论分析。 - - -以前的一轮调参: -我当时的调参需求: - -请对coarse_rank fusion公式进行调参: - 目前的baseline是这一组,Primary_Metric_Score: 0.637642: - coarse_rank: - ... - fusion: - es_bias: 10.0 - es_exponent: 0.05 - text_bias: 0.1 - text_exponent: 0.35 - text_translation_weight: 1.0 - knn_text_weight: 1.0 - knn_image_weight: 2.0 - knn_tie_breaker: 0.3 - knn_bias: 0.2 - knn_exponent: 5.6 - knn_text_bias: 0.2 - knn_text_exponent: 0.0 - knn_image_bias: 0.2 - knn_image_exponent: 0.0 - 评估指标在/data/saas-search/artifacts/search_evaluation/batch_reports/batch_20260415T150754Z_00b6a8aa3d.md - 请以这个为基准,发散思维,进行宽一点的范围调参。因为每次重启、评测都需要几分钟,请写一个调参的框架,基于框架、设定好多组参数、写好脚本,每轮收集效果自动的调整参数分布(因为笛卡尔积、多种参数调参成本太高,因此考虑贝叶斯调参等方法通 - 过多轮迭代通过脚本自动收敛) - 每次调整参数后需要重启backend (有时候重启backend后eval-web好像也挂了,如果有可以追查原因并修复) - ./restart.sh backend - 注意:请你调试脚本、进行一轮分析,最终要沉淀一套调参脚本,我下次可以重新跑(还是针对这组参数调参),能自动迭代(调整参数分布)、收集每组参数的指标、调优到最优的参数组合。 - - - -你给出的调参脚本: - - 长时间自动调参脚本已经配好,核心文件是: - - - 调参主程序:scripts/evaluation/tune_fusion.py - - 宽范围搜索空间:scripts/evaluation/tuning/coarse_rank_fusion_space.yaml - - 后台长跑启动脚本:scripts/evaluation/start_coarse_fusion_tuning_long.sh - - 续跑脚本:scripts/evaluation/resume_coarse_fusion_tuning_long.sh - - 使用说明:scripts/evaluation/tuning/README.md - - 这套方案不是笛卡尔积穷举,而是“种子实验 + 随机探索 + 基于历史最优分布的自适应采样”反复迭代。搜索范围已经放宽,新增了多组偏离 baseline 很远的 seed,也保留了 baseline 附近抖动,适合长时间慢慢收敛。 - - 直接启动一轮: - - ./scripts/evaluation/start_coarse_fusion_tuning_long.sh - - 如果想跑更久一点: - - MAX_EVALS=48 BATCH_SIZE=3 CANDIDATE_POOL_SIZE=512 RUN_NAME=coarse_fusion_long_001 \ - ./scripts/evaluation/start_coarse_fusion_tuning_long.sh - - 看日志: - - tail -f artifacts/search_evaluation/tuning_launches/.log - - 看结果目录: - - ls artifacts/search_evaluation/tuning_runs// - - 续跑: - - ./scripts/evaluation/resume_coarse_fusion_tuning_long.sh - - 每轮会自动做这些事: - - - 生成一批候选 coarse_rank.fusion 参数 - - 写入配置并重启 backend - - 检查 eval-web,必要时自愈拉起 - - 跑评测并收集 Primary_Metric_Score - - 更新 trials.jsonl、leaderboard.csv、summary.md - - 基于历史结果调整下一轮采样分布 - - 你先跑一轮长任务。跑完后把 run_name 或 scripts/evaluation/tuning/README.md 里对应实验目录发我,我再基于结果做下一步分析和收缩搜索空间。 - - - - -› 我运行了:MAX_EVALS=400 BATCH_SIZE=3 CANDIDATE_POOL_SIZE=512 RUN_NAME=coarse_fusion_long_001 ./scripts/evaluation/start_coarse_fusion_tuning_long.sh - - run_name=coarse_fusion_long_001 - pid=2218620 - log=artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.log - pid_file=artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.pid - cmd_file=artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.cmd - run_dir=artifacts/search_evaluation/tuning_runs/coarse_fusion_long_001 - - tail -f artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.log - cat artifacts/search_evaluation/tuning_runs/coarse_fusion_long_001/leaderboard.csv - - -• 我先看这次长跑的日志和当前排行榜,确认任务是否正常推进、是否有异常中断,以及目前最优参数落在哪个方向。 - -• Explored - └ Read coarse_fusion_long_001.log, leaderboard.csv - List coarse_fusion_long_001 - Read summary.md - -─── - -• 结果已经足够看出明显趋势了。我再补一层聚合分析,看看 top 区间里哪些参数方向反复出现,避免只盯着单个最优点。 - -• Ran ps -p 2218620 -o pid=,etimes=,cmd= - └ (no output) - -• Ran python - <<'PY' - │ import csv, statistics, math - │ from pathlib import Path - │ … +15 lines - └ rows 385 - - … +61 lines (ctrl + t to view transcript) - knn_image_bias: mean=0.5631 median=0.2000 min=0.0010 max=4.0000 - knn_image_exponent: mean=4.6316 median=5.8680 min=0.0000 max=6.0000 - -• Ran python - <<'PY' - │ import csv - │ from pathlib import Path - │ … +19 lines - └ baseline_score 0.637642 - - … +30 lines (ctrl + t to view transcript) - 10 bo_234 0.641241 {'es_bias': '7.214', 'es_exponent': '0.2025', 'text_bias': '4.0', 'text_exponent': '1.584', 'text_translation_weight': '1.4441', 'knn_text_weight': '0.1', 'knn_image_weight': '5.6232', 'knn_tie_breaker': - '0.021', 'knn_bias': '0.0019', 'knn_exponent': '11.8477', 'knn_text_bias': '2.3125', 'knn_text_exponent': '1.1547', 'knn_image_bias': '0.9641', 'knn_image_exponent': '5.8671'} - -这一次因为外部原因(磁盘满)终止了,以上是最好的一组参数。 diff --git a/docs/issues/issue-2026-04-16-bayes寻参-数据集扩增-clothing_top771.md b/docs/issues/issue-2026-04-16-bayes寻参-数据集扩增-clothing_top771.md new file mode 100644 index 0000000..c79bdd1 --- /dev/null +++ b/docs/issues/issue-2026-04-16-bayes寻参-数据集扩增-clothing_top771.md @@ -0,0 +1,70 @@ +Prompt - 1 + +参考当前的评估框架 +@scripts/evaluation/README.md @scripts/evaluation/eval_framework/framework.py +@start_eval.sh.sh +当前,是基于54个评测样本(queries.txt),建立了自动化评估的系统,便于发现策略在这个评估集上的效果。 + +我需要扩大评估样本,使用all_keywords.txt.top1w.shuf.top1k.clothing_filtered(771条)为query集合,走标注流程,从而新建一个标注集。 +那么以后eval-web服务,现在的Batch Evaluation按钮,应该支持多个评估集合,左侧的History,也有对应多个评估集合的评估结果,请你考虑如何支持、如何设计。请进行统一的设计,不要补丁式的支持。 + + + +Response - 1 + +设计已经整理并写回到 [issue-2026-04-16-bayes寻参-TODO.md](/data/saas-search/docs/issues/issue-2026-04-16-bayes寻参-TODO.md:24)。这次我没有做补丁式方案,而是把它统一成“多 dataset registry 模式”:`dataset_id` 成为一等主键,`eval-web` 的 Queries / Batch Evaluation / History / 调参脚本都围绕当前 dataset 工作,历史产物按 dataset 隔离,并且每次 batch/build 都固化一份 dataset snapshot,避免以后 query 文件变更导致历史失真。 + +这份设计里最关键的取舍是:评估集和标签缓存分层。`relevance_labels` / `rerank_scores` 继续按 `(tenant_id, query, spu_id)` 共享复用,不按 dataset 重复标注;真正带 `dataset_id` 的是 `build_runs`、`batch_runs`、artifact 目录、Web API 和前端上下文。文档里也补了配置结构、API 设计、UI 交互、迁移方案,以及调参链路如何接入 `--dataset-id`。另外我顺手核对了当前文件规模:`queries.txt` 现在仓库里是 53 条非空 query,`all_keywords.txt.top1w.shuf.top1k.clothing_filtered` 是 771 条。 + +如果你要我继续落地实现,我建议下一步先做 3 件事:dataset registry + artifact path helper、`batch_runs/build_runs` 存储升级、`/api/datasets` 与 History 按 dataset 过滤。此次没有跑测试,因为目前是文档设计更新,没有改功能代码。 + + + + + +prompt - 2 + +请推进、完成这些事项、并且把llm标注跑起来 + +response - 2 + +多评估集这套已经落地到代码里了,核心改动在 [config/config.yaml](/data/saas-search/config/config.yaml:48)、[config/schema.py](/data/saas-search/config/schema.py:410)、[config/loader.py](/data/saas-search/config/loader.py:304) 和新加的 [datasets.py](/data/saas-search/scripts/evaluation/eval_framework/datasets.py:1)。现在 `search_evaluation.datasets` 里正式注册了 `core_queries` 和 `clothing_top771`,CLI / store / framework / Web API / 前端都改成以 `dataset_id` 为主轴了,关键实现分别在 [cli.py](/data/saas-search/scripts/evaluation/eval_framework/cli.py:1)、[store.py](/data/saas-search/scripts/evaluation/eval_framework/store.py:1)、[framework.py](/data/saas-search/scripts/evaluation/eval_framework/framework.py:1)、[web_app.py](/data/saas-search/scripts/evaluation/eval_framework/web_app.py:1)、[index.html](/data/saas-search/scripts/evaluation/eval_framework/static/index.html:1)、[eval_web.js](/data/saas-search/scripts/evaluation/eval_framework/static/eval_web.js:1)。调参入口也已经带上 `--dataset-id`,在 [tune_fusion.py](/data/saas-search/scripts/evaluation/tune_fusion.py:1) 和几个启动脚本里都打通了;文档同步写到了 [scripts/evaluation/README.md](/data/saas-search/scripts/evaluation/README.md:1) 和 [issue-2026-04-16-bayes寻参-TODO.md](/data/saas-search/docs/issues/issue-2026-04-16-bayes寻参-TODO.md:35)。 + +验证这边我跑过一轮编译检查,另外新增了 [tests/test_search_evaluation_datasets.py](/data/saas-search/tests/test_search_evaluation_datasets.py:1),`./.venv/bin/pytest -q tests/test_search_evaluation_datasets.py` 结果是 `2 passed`。`eval-web` 代码已经按新模型重启过一次并健康通过,但在当前机器资源占用下它后来没有稳定驻留,我没把这个问题硬掩过去;如果你要立刻看 UI,我建议等这轮大标注稳定后再单独 `./scripts/service_ctl.sh restart eval-web` 复位。 + +LLM 标注已经跑起来了,目标 dataset 是 `clothing_top771`。我先手动拉起了 reranker 并确认 `http://127.0.0.1:6007/health` 返回 `{"status":"ok"...}`,然后启动了 rebuild build。当前进度已经进入第 1 个 query `白色oversized T-shirt` 的 LLM 批量标注阶段,最新日志大约推进到 `llm_batch=24/40`。你现在可以直接看 [logs/eval.log](/data/saas-search/logs/eval.log:1) 跟主进度,看 [logs/verbose/eval_verbose.log](/data/saas-search/logs/verbose/eval_verbose.log:1) 跟完整 LLM I/O。唯一要注意的是:这轮 build 依赖我手动拉起的 reranker,因为当前 `search.rerank.enabled=false`,`service_ctl` 会跳过它。 + + + + + +prompt - 3 + +“LLM 标注已经跑起来了,目标 dataset 是 clothing_top771。” +好像进程在第 48 条 build上停了 +"query 雪纺衬衫 —— 正好是 query 文件里 第 47 行。 + + + +response - 3 + +当前不是卡死,已经越过你说的第 48 条。`clothing_top771` 的 `query_builds` 现在是 **54**,正在跑第 **55/771**(`barbie backpack`),第 47/48(`雪纺衬衫` / `convertible zip-off hiking pants`)都已被识别为已完成并跳过续跑。 + +我把这次流程做成了可持续的“统一续跑”能力,避免再靠临时脚本: +- 在 [scripts/evaluation/eval_framework/cli.py](/data/saas-search/scripts/evaluation/eval_framework/cli.py) 新增 `build --resume-missing --continue-on-error --max-retries-per-query --retry-backoff-sec`,并修正默认参数逻辑(有 `dataset_id` 时不再强塞 legacy `queries_file`)。 +- 在 [scripts/evaluation/start_eval.sh](/data/saas-search/scripts/evaluation/start_eval.sh) 新增 `batch-rebuild-resume` 入口,统一用 dataset 模式,`REPO_EVAL_QUERIES` 仅作可选覆盖。 +- 在 [scripts/start_eval_web.sh](/data/saas-search/scripts/start_eval_web.sh) 做同样的 dataset/queries 统一化。 +- 在 [scripts/evaluation/README.md](/data/saas-search/scripts/evaluation/README.md) 补了中断续跑说明和新命令。 +- 已验证 `eval-web` 多数据集接口正常(`/api/datasets`、`/api/history?dataset_id=...` 均返回 `core_queries` 与 `clothing_top771` 分域结果)。 + +当前在线进程: +- LLM 标注:`PID 2062901`(`build ... --dataset-id clothing_top771 --resume-missing ...`) +- reranker:`PID 2065235`(6007,`/health` 返回 `ok`) + +如果你要自己盯进度,直接看: +```bash +tail -f logs/eval.log +ls -1 artifacts/search_evaluation/datasets/clothing_top771/query_builds | wc -l +curl -sS http://127.0.0.1:6007/health +``` + diff --git a/docs/issues/issue-2026-04-16-bayes寻参-数据集扩增.md b/docs/issues/issue-2026-04-16-bayes寻参-数据集扩增.md deleted file mode 100644 index e69de29..0000000 --- a/docs/issues/issue-2026-04-16-bayes寻参-数据集扩增.md +++ /dev/null diff --git a/docs/issues/issue-2026-04-16-数据集扩增&bayes寻参-TODO.md b/docs/issues/issue-2026-04-16-数据集扩增&bayes寻参-TODO.md new file mode 100644 index 0000000..0630e64 --- /dev/null +++ b/docs/issues/issue-2026-04-16-数据集扩增&bayes寻参-TODO.md @@ -0,0 +1,508 @@ + + +一、扩展评估标注集 +二、在大标注集上寻参 +三、(暂时不做,克制,业务发展初期不要做!做通用性强维护成本低的!)coarse rank使用LTR(各个因子做多个非线性映射+FM拟合 pairwise,参考ranknet) + + + +0、得到all_keywords.txt.top1w.shuf.top1k.clothing_filtered(done) + +方法1(目前这么做的): +用awk,读取not_clothing.txt作为set,对all_keywords.txt.top1w.shuf.top1k每一行,如果该行在set中,则过滤,得到过滤后的文件,生成文件:all_keywords.txt.top1w.shuf.top1k.clothing_filtered + +方法2: +scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k +这个文件还混杂了一些非“服饰鞋帽”类搜索词,请先做一遍清理。 +用llm做剔出,每次输入50条,提示词是: +Please filter out the queries from the following list that do not belong to the clothing, shoes, and accessories category. Output the original list of queries, one query per line, without any additional content. + +然后将返回的,从原始query剔出。 +生成文件:all_keywords.txt.top1w.shuf.top1k.clothing_filtered + + + +一、扩展评估标注集 + +参考当前的评估框架 +@scripts/evaluation/README.md @scripts/evaluation/eval_framework/framework.py +@start_eval.sh.sh +当前,是基于54个评测样本(queries.txt),建立了自动化评估的系统,便于发现策略在这个评估集上的效果。 + +我需要扩大评估样本,使用all_keywords.txt.top1w.shuf.top1k.clothing_filtered(771条)为query集合,走标注流程,从而新建一个标注集。 +那么以后eval-web服务,现在的Batch Evaluation按钮,应该支持多个评估集合,左侧的History,也有对应多个评估集合的评估结果,请你考虑如何支持、如何设计。请进行统一的设计,不要补丁式的支持。 + +统一设计方案(2026-04-17) + +先校正一下现状口径: + +- `scripts/evaluation/queries/queries.txt` 当前仓库里是 53 条非空 query,不是 54 条。 +- `scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k.clothing_filtered` 当前是 771 条。 + +当前实现的问题,不只是 UI 没有下拉框,而是“评估集”这个概念在系统里还不是一等公民: + +- 配置层只有一个全局 `search_evaluation.queries_file` +- Web UI 左侧 Queries/History 默认只服务这一份 query 文件 +- `batch_runs` / `build_runs` 历史记录没有 `dataset_id` +- 产物目录是全局平铺的 `batch_reports/`、`query_builds/` +- `start_eval.sh` / `start_eval_web.sh` / `tune_fusion.py` 都是通过 `queries_file` 隐式指定评估集 +- `--reset-artifacts` 现在会清空整套 SQLite + query_builds,多评估集后这个语义会变得危险 + +所以这里要做的,不是“给 batch API 多传一个文件路径”,而是把“评估集”抽成贯穿配置、存储、API、UI、产物、调参脚本的一层统一模型。 + +设计目标 + +1. 一个 eval-web 服务同时支持多个评估集。 +2. Batch Evaluation、History、调参任务都必须明确绑定某个评估集。 +3. 历史结果必须可追溯到“当时到底用了哪一批 query”,不能因为 query 文件后续变更而失真。 +4. 相同 `(tenant_id, query, spu_id)` 的标签尽量复用,不因为 query 同时出现在两个评估集里就重复标注。 +5. 扩展到第三个、第四个评估集时,不需要再改表结构思路或前端交互模型。 + +核心抽象:区分“评估集”与“标签缓存” + +- 评估集(Evaluation Dataset):一组有稳定 `dataset_id` 的 query 集合,用来驱动 build、batch、history、调参。 +- 标签缓存(Label Cache):对 `(tenant_id, query_text, spu_id)` 的相关性判断结果。 + +这两者不要混为一谈。 + +建议保留现有 `relevance_labels` / `rerank_scores` 的“按 query 共享缓存”设计,不按 dataset 拆表,原因: + +- 同一个 query 如果同时属于 `core_queries` 和 `clothing_top771`,其 `(query, spu_id)` 标签语义本质相同,应该复用。 +- 这样新增大评估集时,只需要补齐新 query 的标签,不会对已有 query 重复做 LLM 标注。 +- 真正需要 dataset 维度的是:运行历史、构建历史、覆盖率统计、产物归档、UI 选择上下文。 + +配置设计 + +把当前单一 `queries_file` 升级为“评估集注册表”。建议在 `config.yaml` 中变成: + +```yaml +search_evaluation: + artifact_root: artifacts/search_evaluation + default_dataset_id: core_queries + datasets: + - dataset_id: core_queries + display_name: Core Queries + description: Legacy baseline query set from queries.txt + query_file: scripts/evaluation/queries/queries.txt + tenant_id: "163" + language: en + enabled: true + - dataset_id: clothing_top771 + display_name: Clothing Filtered 771 + description: 771 filtered clothing/shoes/accessories queries + query_file: scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k.clothing_filtered + tenant_id: "163" + language: en + enabled: true + + # 保留这些作为全局默认值;dataset 没显式覆盖时继承 + batch_top_k: 100 + audit_top_k: 100 + build_search_depth: 1000 + build_rerank_depth: 10000 +``` + +建议点: + +- `dataset_id` 是稳定主键,前后端、SQLite、历史记录、调参脚本都只认它,不认文件路径。 +- `query_file` 只是这个 dataset 当前版本的来源,不是外部协议的一部分。 +- 继续保留全局默认参数;以后如果某个 dataset 需要特殊 top_k / language,再支持局部覆盖。 +- 为兼容老脚本,可暂时保留 `queries_file`,但只作为 fallback,在 loader 里自动转换成一个隐式 dataset;新代码不再直接依赖它。 + +产物目录设计 + +当前所有 batch 报告都平铺在 `artifacts/search_evaluation/batch_reports/` 下,后面 dataset 一多会很乱。建议改成“共享缓存 + dataset 独立产物目录”: + +```text +artifacts/search_evaluation/ + search_eval.sqlite3 # 共享标签缓存/共享 rerank 缓存/运行索引 + datasets/ + core_queries/ + batch_reports/ + / + report.json + report.md + config_snapshot.json + dataset_snapshot.json + queries.txt + query_builds/ + .json + audits/ + ... + clothing_top771/ + batch_reports/ + / + ... + query_builds/ + .json + audits/ + ... +``` + +重点是每次 batch/build 都要固化 dataset snapshot: + +- `dataset_id` +- `display_name` +- `query_file` +- `query_count` +- `query_sha1` +- 当次实际 queries 副本 `queries.txt` + +这样即使以后 `all_keywords...clothing_filtered` 文件被重新清洗、条数变化,历史 batch 仍然可复现“当时到底评了哪些 query”。 + +SQLite / 存储层设计 + +共享缓存表可以继续保留: + +- `relevance_labels(tenant_id, query_text, spu_id, ...)` +- `rerank_scores(tenant_id, query_text, spu_id, ...)` +- `query_profiles(tenant_id, query_text, prompt_version, ...)` + +需要升级的是运行历史表: + +1. `build_runs` 增加 + - `dataset_id` + - `dataset_display_name` + - `dataset_query_file` + - `dataset_query_count` + - `dataset_query_sha1` + +2. `batch_runs` 增加 + - `dataset_id` + - `dataset_display_name` + - `dataset_query_file` + - `dataset_query_count` + - `dataset_query_sha1` + +3. `list_batch_runs()` / `get_batch_run()` / `insert_batch_run()` 全部变成 dataset-aware + +4. 覆盖率统计接口按 dataset 聚合,而不是简单按全库 query 聚合 + + - 当前 `list_query_label_stats(tenant_id)` 是“全量 query_text 分组” + - 以后应该是“给定 dataset_id 后,只统计该 dataset queries 的覆盖情况” + +这里建议不要额外把 query 全量写进 SQLite 做注册表主数据,query 主数据仍从 config + query_file 解析即可;SQLite 只负责记录 run 时的 snapshot 元数据。 + +API 设计 + +建议把 Web API 升级成以 dataset 为主轴,而不是默认只服务一个 `query_file`: + +1. `GET /api/datasets` + +返回所有可用评估集: + +- `dataset_id` +- `display_name` +- `description` +- `query_count` +- `query_file` +- `tenant_id` +- `language` +- `coverage_summary` + +2. `GET /api/datasets/{dataset_id}/queries` + +返回该 dataset 的 query 列表,以及 dataset 元信息。 + +3. `POST /api/search-eval` + +请求体增加可选 `dataset_id`。 + +- 单 query 评估本身仍然可以支持任意 query 文本 +- 但当页面处于某个 dataset 上下文时,返回里也带上该 dataset 信息,便于 UI 一致展示 + +4. `POST /api/batch-eval` + +请求体优先使用 `dataset_id`,不再默认依赖服务启动时绑定的唯一 `query_file`。 + +建议请求模型变成: + +```json +{ + "dataset_id": "clothing_top771", + "top_k": 100, + "auto_annotate": false, + "language": "en", + "force_refresh_labels": false +} +``` + +`queries` 字段可保留为高级/调试能力,但 UI 主路径和调参脚本主路径都应该走 `dataset_id`。 + +5. `GET /api/history?dataset_id=clothing_top771&limit=20` + +History 默认按当前 dataset 过滤;如有需要再支持 `all=true` 看全量。 + +6. `GET /api/history/{batch_id}/report` + +返回报告时补充 dataset 元信息,前端 report modal 里能看到这是哪个 dataset 的报告。 + +前端 / eval-web 交互设计 + +现在左侧栏写死了: + +- Queries 来自 `queries.txt` +- History 没有 dataset 维度 + +建议改成三层结构: + +1. 左上增加 Dataset Selector + +- 下拉框或 tabs,显示 `Core Queries (53)`、`Clothing Filtered 771 (771)` +- 当前选中的 dataset 决定左侧 query 列表和默认 history 过滤 + +2. Queries 区域绑定当前 dataset + +- 标题显示 dataset 名称 + query 数 +- 副标题显示 query 文件路径 +- 点击 query 触发单 query 评估 + +3. History 区域绑定当前 dataset + +- 默认只显示当前 dataset 的 batch history +- 每个 item 显示 `dataset badge + batch_id + created_at + query_count + primary metrics` +- 可选再加一个 “All Datasets” 开关,但默认视角一定要是“当前 dataset” + +4. 主区 Batch Evaluation 按钮绑定当前 dataset + +- 点击时执行当前 dataset 的 batch,而不是对服务启动时唯一 query_file 执行 +- 按钮文案建议带上 dataset 名,例如:`Batch Evaluate: Clothing Filtered 771` + +5. 页面顶端增加当前 dataset 概览卡片 + +- `dataset_id` +- query 数 +- 已有标签 query 数 / 覆盖率 +- 最近一次 batch 时间 + +这样进入页面时,用户始终知道自己正在看哪个评估集,不会把 53 条基线集和 771 条大集合的结果混在一起。 + +CLI / 启动脚本设计 + +需要把 `--dataset-id` 提升为第一入口参数: + +- `build_annotation_set.py build --dataset-id clothing_top771` +- `build_annotation_set.py batch --dataset-id clothing_top771` +- `build_annotation_set.py audit --dataset-id clothing_top771` +- `serve_eval_web.py serve --dataset-id core_queries` + +说明: + +- `serve` 的 `--dataset-id` 只决定页面初始选中哪个 dataset,不应该再把整个服务绑定死到一个 query 文件。 +- `--queries-file` 可以保留一段时间做兼容,但内部先解析 registry;如果能映射到某个 dataset,就统一转成 `dataset_id` 处理。 + +`start_eval.sh` / `start_eval_web.sh` 也要同步升级: + +- 读取 `REPO_EVAL_DATASET_ID` +- 保留 `REPO_EVAL_QUERIES` 兼容模式,但新用法优先 `REPO_EVAL_DATASET_ID` + +额外要修正的一点: + +- 当前 `--reset-artifacts` 会删整个 SQLite 和整个 `query_builds/` +- 多 dataset 后这个行为太危险 +- 应拆成更明确的选项,例如: + - `--reset-dataset-build-artifacts` + - `--purge-shared-label-cache`(显式危险操作,默认不要碰) + +调参框架联动设计 + +`tune_fusion.py`、`start_coarse_fusion_tuning_long.sh`、`resume_coarse_fusion_tuning_long.sh` 也必须带 dataset 维度,否则之后同一套 coarse rank 参数可能分别在 53 条集和 771 条集上跑出完全不同的结论,但 leaderboard 会混在一起。 + +建议: + +- `tune_fusion.py` 增加 `--dataset-id` +- `summary.json` / `leaderboard.csv` / `trials.jsonl` 记录 `dataset_id` +- 调参时调用 eval-web batch API,也传 `dataset_id` +- `seed-report` 如果来自历史 batch 报告,也校验 `dataset_id` 一致 + +迁移方案 + +建议采用兼容迁移,而不是硬切: + +1. 先在配置中注册两个 dataset + +- `core_queries` -> `scripts/evaluation/queries/queries.txt` +- `clothing_top771` -> `scripts/evaluation/queries/all_keywords.txt.top1w.shuf.top1k.clothing_filtered` + +2. 旧历史记录回填 dataset 元信息 + +- 如果历史记录没有 `dataset_id`,且 query 列表 hash 与 `queries.txt` 一致,则回填为 `core_queries` +- 无法确认的旧记录,标记为 `legacy_unknown` + +3. UI 默认只展示 registry 中 `enabled=true` 的 dataset + +4. 保留一段时间旧 CLI 参数,但 README、新脚本、新前端只文档化 dataset 模式 + +实施顺序 + +建议按下面顺序做,避免半途出现“后端支持了但前端看不出来”或者“前端能选但历史存不准”: + +1. 配置层:引入 dataset registry 与解析器 +2. 公共帮助层:统一的 dataset resolve / snapshot / artifact path helper +3. SQLite:`batch_runs` / `build_runs` 增加 dataset 元字段 +4. Framework:`build` / `batch` / `audit` 全面改为 dataset-aware +5. Web API:新增 `/api/datasets`,History 支持 dataset filter +6. eval-web 前端:selector + dataset-scoped queries/history/batch +7. 调参脚本:`--dataset-id` 全链路打通 +8. README / issue / 运维脚本更新 + +这套设计的关键点 + +- “评估集”是显式主键,不再靠文件路径暗示 +- “标签缓存”继续按 `(tenant_id, query, spu_id)` 共享复用 +- “历史报告”按 dataset 严格隔离并带 snapshot +- “UI 交互”始终围绕当前 dataset 上下文展开 +- “调参结果”必须标记 dataset,防止不同集合上的指标被误比 + +结论 + +这件事的统一做法,不是给现有单评估集逻辑加几个 if/else,而是把 eval framework 从“单 query 文件模式”升级为“多 dataset registry 模式”。 + +如果按这套方案落地,后面新增第三个评估集时,应该只需要: + +1. 在 `config.yaml` 注册一个新 dataset +2. 跑对应 build +3. 在 UI 中选择它做 batch / 看 history +4. 在调参脚本里指定 `--dataset-id` + +而不需要再次改数据模型和交互模型。 + + + +二、在大标注集上寻参 + +我以前经过过一轮调参,是基于54个评测样本(queries.txt),过程中发现的最优的参数是这一组: +0.641241 {'es_bias': '7.214', 'es_exponent': '0.2025', 'text_bias': '4.0', 'text_exponent': '1.584', 'text_translation_weight': '1.4441', 'knn_text_weight': '0.1', 'knn_image_weight': '5.6232', 'knn_tie_breaker': + '0.021', 'knn_bias': '0.0019', 'knn_exponent': '11.8477', 'knn_text_bias': '2.3125', 'knn_text_exponent': '1.1547', 'knn_image_bias': '0.9641', 'knn_image_exponent': '5.8671'} + +这一组参数分布比较极端,text_bias太大(文本项得分事0~1的,加上4被稀释的很大),图片的exponent太大,不过在这个数据集上面确实是最好的,我觉得有过拟合的可能,因此要扩大数据集,先扩展标注集,然后使用扩展的标注集,继续进行寻参。 + +我已经新建了一个标注集合,请启动寻参任务,把任务跑起来,以后等程序跑完了应该能拿到寻参结果,下次你可以结合程序执行的结果进行结论分析。 + + +以前的一轮调参: +我当时的调参需求: + +请对coarse_rank fusion公式进行调参: + 目前的baseline是这一组,Primary_Metric_Score: 0.637642: + coarse_rank: + ... + fusion: + es_bias: 10.0 + es_exponent: 0.05 + text_bias: 0.1 + text_exponent: 0.35 + text_translation_weight: 1.0 + knn_text_weight: 1.0 + knn_image_weight: 2.0 + knn_tie_breaker: 0.3 + knn_bias: 0.2 + knn_exponent: 5.6 + knn_text_bias: 0.2 + knn_text_exponent: 0.0 + knn_image_bias: 0.2 + knn_image_exponent: 0.0 + 评估指标在/data/saas-search/artifacts/search_evaluation/batch_reports/batch_20260415T150754Z_00b6a8aa3d.md + 请以这个为基准,发散思维,进行宽一点的范围调参。因为每次重启、评测都需要几分钟,请写一个调参的框架,基于框架、设定好多组参数、写好脚本,每轮收集效果自动的调整参数分布(因为笛卡尔积、多种参数调参成本太高,因此考虑贝叶斯调参等方法通 + 过多轮迭代通过脚本自动收敛) + 每次调整参数后需要重启backend (有时候重启backend后eval-web好像也挂了,如果有可以追查原因并修复) + ./restart.sh backend + 注意:请你调试脚本、进行一轮分析,最终要沉淀一套调参脚本,我下次可以重新跑(还是针对这组参数调参),能自动迭代(调整参数分布)、收集每组参数的指标、调优到最优的参数组合。 + + + +你给出的调参脚本: + + 长时间自动调参脚本已经配好,核心文件是: + + - 调参主程序:scripts/evaluation/tune_fusion.py + - 宽范围搜索空间:scripts/evaluation/tuning/coarse_rank_fusion_space.yaml + - 后台长跑启动脚本:scripts/evaluation/start_coarse_fusion_tuning_long.sh + - 续跑脚本:scripts/evaluation/resume_coarse_fusion_tuning_long.sh + - 使用说明:scripts/evaluation/tuning/README.md + + 这套方案不是笛卡尔积穷举,而是“种子实验 + 随机探索 + 基于历史最优分布的自适应采样”反复迭代。搜索范围已经放宽,新增了多组偏离 baseline 很远的 seed,也保留了 baseline 附近抖动,适合长时间慢慢收敛。 + + 直接启动一轮: + + ./scripts/evaluation/start_coarse_fusion_tuning_long.sh + + 如果想跑更久一点: + + MAX_EVALS=48 BATCH_SIZE=3 CANDIDATE_POOL_SIZE=512 RUN_NAME=coarse_fusion_long_001 \ + ./scripts/evaluation/start_coarse_fusion_tuning_long.sh + + 看日志: + + tail -f artifacts/search_evaluation/tuning_launches/.log + + 看结果目录: + + ls artifacts/search_evaluation/tuning_runs// + + 续跑: + + ./scripts/evaluation/resume_coarse_fusion_tuning_long.sh + + 每轮会自动做这些事: + + - 生成一批候选 coarse_rank.fusion 参数 + - 写入配置并重启 backend + - 检查 eval-web,必要时自愈拉起 + - 跑评测并收集 Primary_Metric_Score + - 更新 trials.jsonl、leaderboard.csv、summary.md + - 基于历史结果调整下一轮采样分布 + + 你先跑一轮长任务。跑完后把 run_name 或 scripts/evaluation/tuning/README.md 里对应实验目录发我,我再基于结果做下一步分析和收缩搜索空间。 + + + + +› 我运行了:MAX_EVALS=400 BATCH_SIZE=3 CANDIDATE_POOL_SIZE=512 RUN_NAME=coarse_fusion_long_001 ./scripts/evaluation/start_coarse_fusion_tuning_long.sh + + run_name=coarse_fusion_long_001 + pid=2218620 + log=artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.log + pid_file=artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.pid + cmd_file=artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.cmd + run_dir=artifacts/search_evaluation/tuning_runs/coarse_fusion_long_001 + + tail -f artifacts/search_evaluation/tuning_launches/coarse_fusion_long_001.log + cat artifacts/search_evaluation/tuning_runs/coarse_fusion_long_001/leaderboard.csv + + +• 我先看这次长跑的日志和当前排行榜,确认任务是否正常推进、是否有异常中断,以及目前最优参数落在哪个方向。 + +• Explored + └ Read coarse_fusion_long_001.log, leaderboard.csv + List coarse_fusion_long_001 + Read summary.md + +─── + +• 结果已经足够看出明显趋势了。我再补一层聚合分析,看看 top 区间里哪些参数方向反复出现,避免只盯着单个最优点。 + +• Ran ps -p 2218620 -o pid=,etimes=,cmd= + └ (no output) + +• Ran python - <<'PY' + │ import csv, statistics, math + │ from pathlib import Path + │ … +15 lines + └ rows 385 + + … +61 lines (ctrl + t to view transcript) + knn_image_bias: mean=0.5631 median=0.2000 min=0.0010 max=4.0000 + knn_image_exponent: mean=4.6316 median=5.8680 min=0.0000 max=6.0000 + +• Ran python - <<'PY' + │ import csv + │ from pathlib import Path + │ … +19 lines + └ baseline_score 0.637642 + + … +30 lines (ctrl + t to view transcript) + 10 bo_234 0.641241 {'es_bias': '7.214', 'es_exponent': '0.2025', 'text_bias': '4.0', 'text_exponent': '1.584', 'text_translation_weight': '1.4441', 'knn_text_weight': '0.1', 'knn_image_weight': '5.6232', 'knn_tie_breaker': + '0.021', 'knn_bias': '0.0019', 'knn_exponent': '11.8477', 'knn_text_bias': '2.3125', 'knn_text_exponent': '1.1547', 'knn_image_bias': '0.9641', 'knn_image_exponent': '5.8671'} + +这一次因为外部原因(磁盘满)终止了,以上是最好的一组参数。 diff --git a/docs/issues/issue-2026-04-20-款式意图下SKU选取增强(option,taxonomy,图像)-0420done.md b/docs/issues/issue-2026-04-20-款式意图下SKU选取增强(option,taxonomy,图像)-0420done.md new file mode 100644 index 0000000..0b0edbd --- /dev/null +++ b/docs/issues/issue-2026-04-20-款式意图下SKU选取增强(option,taxonomy,图像)-0420done.md @@ -0,0 +1,510 @@ +# 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 + } + } + } + }, -- libgit2 0.21.2