# 检索调参与 LTR 工作流 ## 1. 目标 这份文档记录本仓库当前可复用的调参与分析流程,适用于: - 搜索相关性调优 - rerank fusion 参数实验 - bad case 定位 - 后续 Learning to Rank(LTR)特征设计 ## 2. 先看什么指标 当前主指标不是单一指标,而是一组主 scorecard: - `NDCG@20` - `NDCG@50` - `ERR@10` - `Strong_Precision@10` - `Strong_Precision@20` - `Useful_Precision@50` - `Avg_Grade@10` - `Gain_Recall@20` 实验排序时使用: - `Primary_Metric_Score` 说明: - `Primary_Metric_Score` 是上述 8 个指标的均值。 - 其中 `Avg_Grade@10` 先除以 `3` 再参与平均。 - 这个分数适合做实验排序,但真正决定是否采纳配置,仍然要看关键 query 和各主指标分布。 ## 3. 调参顺序 推荐按以下顺序做,不要一上来只盯最终 rerank 分数。 1. **确认标注覆盖** - 先看 query 是否已有离线标签。 - 没有标签的 query,不要拿它做正式 batch 结论。 - 例如这次 `patterned bodysuit` 不在 canonical `queries.txt` 内,默认没有离线标签,只能做 live diagnosis。 2. **判断问题属于哪一层** - `missing_relevant` 很多:优先怀疑召回不够。 - 召回到了但没进前 20:优先看 `coarse_rank` / `rerank_window`。 - 进了 rerank 仍排不好:再看 fusion 或 reranker 行为。 3. **先改覆盖,再改融合** - 如果高相关商品连候选都进不来,只改 rerank fusion 没有意义。 - 典型动作: - 增大 `knn_text_k` / `knn_text_num_candidates` - 增大 `knn_image_k` / `knn_image_num_candidates` - 增大 `coarse_rank.output_window` - 增大 `rerank.rerank_window` 4. **最后才微调 fusion** - 适合调整: - `es_bias`, `es_exponent` - `rerank_bias`, `rerank_exponent` - `text_bias`, `text_exponent` - `knn_image_weight`, `knn_bias`, `knn_exponent` ## 4. 看日志的方法 `debug=true` 时,优先读这三块: ### 4.1 `retrieval_plan` 确认本次 query 实际用了哪套 KNN 计划: - text KNN 的 `k` - text KNN 的 `num_candidates` - image KNN 的 `k` - image KNN 的 `num_candidates` - 是否走了 long-query 分支 ### 4.2 `ltr_summary` 看最终 top-N 里信号结构是否异常: - `translation_match_docs` - `text_knn_docs` - `image_knn_docs` - `text_fallback_to_es_docs` - 各类 score 平均值 经验: - `translation_match_docs` 很高且 `source_score` 很低,通常说明结果主要依赖翻译召回。 - `image_knn_docs` 很高但精度下降,通常说明 image KNN 权重过强或候选过宽。 - `text_fallback_to_es_docs` 很高,说明 named query 子分信息不稳定,后续要重点检查 query plan。 ### 4.3 `ranking_funnel.*.ltr_features` 每个 doc 在各阶段都有稳定特征块,核心字段: - `es_score` - `text_score` - `knn_score` - `rerank_score` - `source_score` - `translation_score` - `text_knn_score` - `image_knn_score` - `has_translation_match` - `has_text_knn` - `has_image_knn` 最常见的判断模式: - `source_score > 0` 且 `translation_score = 0` 说明主路是原文 lexical。 - `source_score = 0` 且 `translation_score > 0` 说明主路基本完全靠翻译。 - `rerank_score` 很强但仍然上不来 说明 `text_score/knn_score` 或上游窗口裁剪在压制。 - `coarse_rank` 排名很差、`rerank_rank` 没机会出现 说明问题在 coarse 阶段,不在 rerank 阶段。 ## 5. 这次几个 key query 的经验 ### `sock boots` 问题模式: - lexical 会把 `sock` 和 `boots` 分开吃,导致袜子、boot socks、cowboy boots 大量混入。 - 高相关 boots 里不少根本没进最终候选。 调参启示: - 单纯增大 rerank 权重不够。 - 先扩大候选覆盖更有效,尤其是 image/text KNN 和 rerank window。 - image KNN 对这类 footwear query 有价值,但不能放得过猛,否则会拉入太多“像鞋但不是 sock boots”的商品。 ### `minimalist top` 问题模式: - 很多结果的 `source_score=0`,主要靠 translation 命中。 - query 意图偏抽象,容易把“简约风”“纯色上衣”“性感短上衣”等都混进来。 调参启示: - 这类 query 对翻译和 broad semantic 非常敏感。 - `text_exponent` 降太多会丢掉本来还不错的 lexical 排序稳定性。 - 更稳妥的方式是保留 baseline fusion,只做小范围偏置微调。 ### `tassel maxi skirt` 问题模式: - 结果里会出现“maxi skirt”“tassel dress”“sequin tassel skirt”混排。 - 放大 image KNN / KNN exponent 后,容易把视觉上像裙装或带流苏元素的商品抬太高。 调参启示: - 这类 query 说明 image KNN 很容易帮助,也很容易过拟合。 - 一旦 `knn_image_weight` 与 `knn_exponent` 同时上调过猛,整体长尾 query 容易掉分。 ### `patterned bodysuit` 问题模式: - 默认没有离线标签,不能直接纳入 batch 指标结论。 - live 结果能用于观察“patterned / lace / mesh / floral”这些属性如何被 text 与 rerank 解释。 调参启示: - 先补标签,再讨论是否加入长期评估集。 ## 6. 这次实验的结论 本轮实验里,**已验证**的现象是: - 扩大 KNN 候选与 rerank 窗口,能改善 `sock boots` 一类“高相关没召回”的问题。 - 但如果同时明显放大 `knn_image_weight`、`knn_exponent` 并显著降低 `text_exponent`,会让 `tassel maxi skirt` 一类 query 变差,并拖累整体 batch 分数。 - 两轮全量 batch 里,激进配置都没有超过已验证 baseline。 因此当前建议是: - 先把 **日志、特征、主指标体系** 升级好。 - 在后续真正推进配置采纳前,继续做“只扩候选、不大改 fusion”的实验。 - 如果未来做 LTR,优先把当前 `ltr_features` 直接沉淀成训练样本特征,而不是继续纯手调 fusion。 ## 7. 推荐的下次工作 1. 给 `patterned bodysuit` 建离线标签集。 2. 单独验证“baseline fusion + 更大 KNN/窗口”的 batch 效果。 3. 从 `debug_info.per_result[].ranking_funnel.*.ltr_features` 导出训练样本。 4. 先做轻量 LambdaMART / GBDT 排序实验,再决定是否继续手工调乘法融合。 ## 8. LTR 项目视角下的数据闭环 如果把后续工作从“手工调 fusion”升级为“正式 LTR 项目”,建议把整个流程理解成一条固定的数据闭环,而不是一次性的离线实验。 核心链路如下: 1. **在线检索服务产生日志特征** - `searcher.py` 在 `debug_info` 中输出: - `retrieval_plan` - `ltr_summary` - `per_result` - `ranking_funnel` - `rerank_client.py` 在 `_build_ltr_feature_block` 中把各阶段稳定特征整理成统一结构。 2. **评估框架持久化离线标签** - 标注缓存保存在 `artifacts/search_evaluation/search_eval.sqlite3` - 关键表是 `relevance_labels` - 标签由 `scripts/evaluation/eval_framework/constants.py` 定义,为 4 档: - `Fully Relevant` - `Mostly Relevant` - `Weakly Relevant` - `Irrelevant` 3. **离线脚本把“日志特征 + 离线标签”拼成训练样本** - 当前实验脚本为 `scripts/evaluation/offline_ltr_fit.py` - 日志源默认是 `logs/backend_verbose.log` - 训练粒度是 `query x doc` - 每个 query 默认取日志中 top-100 结果 4. **用 group-aware 排序目标做离线拟合** - 当前先用简单版 `RankNet Pairwise Loss` - 训练函数用 FM(Factorization Machine) - 验证方式包含: - query-group K-fold cross validation - 单独留出 10 个 query 作为 holdout test 5. **根据离线结果判断是否能进入正式线上化** - 不是只看训练集和全量拟合表现 - 最关键是看: - cross-validation 是否稳定优于 baseline - holdout test 是否仍优于 baseline - feature importance 是否符合业务直觉 - 是否出现明显的“记住当前样本窗口”的过拟合信号 这条闭环的意义是:之后每次改特征、改候选、改标签、改 loss,都能复用同一条训练与验证路径,而不是重新临时拼脚本。 ## 9. 当前日志结构如何支撑 LTR 这次日志增强的价值,不是“方便人工 debug”这么简单,而是已经具备了进入 LTR 训练样本层的必要条件。 ### 9.1 query 级信息 主要来自 `debug_info.query_analysis` 与 `debug_info.retrieval_plan`: - 原始 query / rewrite query - 检测语言 - translation 结果 - query tokens - text KNN 是否开启 - text KNN 的 `k` - text KNN 的 `num_candidates` - 是否走了 long-query 计划 - image KNN 是否开启 - image KNN 的 `k` - image KNN 的 `num_candidates` 这类信息本质上是 **query context feature**。即使某个 doc 的 `ltr_features` 不变,不同 query plan 下它的含义也会不同。 ### 9.2 doc 级稳定特征 主要来自 `per_result[].ltr_features` 和 `ranking_funnel.*.ltr_features`。 当前最核心的数值/布尔特征包括: - `es_score` - `text_score` - `knn_score` - `rerank_score` - `fine_score` - `source_score` - `translation_score` - `text_primary_score` - `text_support_score` - `text_knn_score` - `image_knn_score` - `knn_primary_score` - `knn_support_score` - `style_boost` - `stage_score` - `has_text_match` - `has_translation_match` - `has_text_knn` - `has_image_knn` - `text_score_fallback_to_es` - `has_style_boost` 这些特征的优点是: - 来源清晰 - 各阶段定义稳定 - 可以直接落盘 - 后续加新特征时不需要破坏旧特征含义 ### 9.3 funnel 级特征 `ranking_funnel` 很重要,因为它不仅告诉我们“最终分数是什么”,还告诉我们“这个 doc 是怎么一路走到最终排序的”。 例如当前可以直接抽出的 funnel 信息包括: - `initial_rank` - `coarse_rank` - `rerank_rank` - `final_rank` - `es_score_normalized` - `coarse_score` - `fused_score` - `coarse_stage_score` - `rerank_stage_score` 这类特征在离线拟合时非常强,但也最容易带来过拟合。原因是它们会携带一部分“现有排序器已经做过的决定”。 因此正式项目里要把 funnel 特征分成两类看: - **可长期保留的过程特征** - 例如 `coarse_rank`、`initial_rank` - **容易泄漏当前排序器决策的特征** - 例如 `final_rank` - 或者与当前 fusion 公式几乎等价的派生项 当前离线实验里,我们保留了大部分 funnel 特征,是为了先快速判断“是否存在可学信号”;正式收敛时需要再做一轮特征裁剪。 ## 10. 样本定义:LTR 训练样本到底是什么 LTR 项目里最容易混淆的点之一,是“训练样本”到底按什么定义。 ### 10.1 基础样本单元 基础单元不是 pair,而是: - 一条 query - 在该 query 下的一个 doc - 这条 doc 的 feature vector - 这条 doc 的 relevance label 也就是一个标准的 `query-doc` 样本。 ### 10.2 为什么最终训练用的是 pair 当前采用的是 `RankNet Pairwise Loss`,所以优化时不是直接回归 label,而是把同一 query 下 label 不同的两个 doc 组成一个 pair。 构造规则很简单: - 同一 query 内 - 若 `label_i != label_j` - 且 `grade_i > grade_j` - 就构造一个正负有序 pair:`(doc_i, doc_j)` 在当前 4 档标注下,grade 映射为: - `Fully Relevant -> 3` - `Mostly Relevant -> 2` - `Weakly Relevant -> 1` - `Irrelevant -> 0` 因此: - `Fully Relevant` 会压过其它三档 - `Mostly Relevant` 会压过 `Weakly Relevant` 和 `Irrelevant` - `Weakly Relevant` 会压过 `Irrelevant` 这样做的好处是: - 不需要先定义复杂的 gain 权重 - 逻辑与人工排序直觉一致 - 很适合作为第一版 baseline ### 10.3 为什么不用 doc 级随机切分 排序任务必须按 query 分组切分,不能把同一个 query 下的 doc 同时放到 train 和 test。 否则模型只是在“见过这个 query 的局部排序关系”的前提下做插值,泛化评估会明显偏乐观。 所以当前脚本里: - cross-validation 用 `GroupKFold` - 额外再留出 `10` 个 query 做 holdout test 这是正式项目里必须坚持的评估原则。 ## 11. label 获取与质量边界 ### 11.1 label 来源 当前 label 来自评估框架的离线缓存: - DB 路径:`artifacts/search_evaluation/search_eval.sqlite3` - 表:`relevance_labels` 脚本会按: - `tenant_id` - `query_text` - `spu_id` 去查 label,并拼回日志中的 `query-doc` 样本。 ### 11.2 label 语义 4 档标签不是“点击率标签”,而是人工/LLM 语义相关性标签。因此它更接近: - 检索相关性监督 - 召回和排序质量监督 而不是: - 转化率预测 - CTR / CVR 预估 所以这套 LTR 更适合作为 **semantic relevance ranker**,而不是最终商业目标排序器。 ### 11.3 label 的边界与风险 这点需要在项目初期说清楚。 当前标签有几个天然限制: 1. **标签覆盖的是固定 query 集** - 未标注 query 不能直接拿来做正式结论 2. **标签覆盖的是 query 对 doc 的语义相关性** - 不含价格、库存、点击、转化等业务偏好 3. **标签池来自当前检索系统可见候选** - 如果上游召回漏掉了大量相关 doc,LTR 只能在“已见候选”上学习 4. **LLM 标注存在噪声** - 特别是 `Exact` 和 `High` 的边界可能不够稳定 因此正式项目中,label 质量治理要作为单独工作项推进,而不是把模型问题和标签问题混在一起。 ## 12. 当前离线特征工程思路 ### 12.1 为什么要做特征工程 虽然 `ltr_features` 已经比较完整,但直接喂原始特征给 FM 还不够。 原因是: - 很多数值特征分布偏斜 - 不同特征的量纲差异很大 - 排序规律往往不是线性的 所以当前离线实验里做了轻量级特征展开。 ### 12.2 当前使用的特征展开 对大部分数值特征,会扩展出: - `raw` - `log1p` - `sqrt` - `square` - `inv = 1 / (1 + x)` 比如: - `text_score__raw` - `text_score__log1p` - `text_score__sqrt` - `text_score__square` - `text_score__inv` 这类展开的目的很简单: - 让模型能表达“分数高一点”和“分数非常高”不是同一个效应 - 让模型能表达某些特征的边际收益递减 - 让一些长尾数值特征更稳定 ### 12.3 当前额外构造的组合特征 除了单特征变换外,还增加了少量人工组合特征: - `translation_share = translation_score / text_score` - `source_share = source_score / text_score` - `image_knn_share = image_knn_score / knn_score` - `text_knn_share = text_knn_score / knn_score` - `rerank_x_text` - `rerank_x_knn` - `rerank_x_es` - `text_minus_es` - `knn_minus_text` - `coarse_minus_rerank` 这些特征本质上在回答几类问题: - 当前 doc 更偏 lexical 还是 translation - 当前 doc 更偏 text knn 还是 image knn - rerank 分和上游分数是否协同 - coarse 与 rerank 是否发生明显冲突 ### 12.4 当前特征工程的结论 这套特征工程足够支持“第一版是否有信号”的判断,但还不够适合直接进入生产。 原因是目前的强特征中,已经出现了不少明显依赖现有排序器状态的项,例如: - `initial_rank__log` - `initial_rank__inv` - `fused_score__*` - `stage_score__*` - `rerank_stage_score__*` 这些特征有助于模型快速拟合当前数据,但可能会让模型过度学习“旧排序器的决策痕迹”。 正式项目里建议将特征分桶: - **A 类:原始可解释特征** - text / knn / rerank / translation / style - **B 类:轻量派生特征** - ratio / log / sqrt / square - **C 类:过程排序特征** - rank / stage score / fused score 第一阶段可以保留 A+B+C 做诊断; 第二阶段应重点验证 A+B 是否足够支撑泛化。 ## 13. 模型与 loss:为什么先选 FM + RankNet ### 13.1 为什么先不用复杂模型 正式 LTR 项目可以有很多路线: - LambdaMART / GBDT - DNN - DSSM / cross encoder 后融合 - listwise ranking 但在当前阶段,首要目标不是追求“最强模型”,而是先回答两个问题: 1. 当前日志特征有没有可学习信号? 2. 这些信号能否超越手工 fusion baseline? 所以第一版需要: - 足够简单 - 可解释 - 容易离线验证 - 便于做 feature ablation ### 13.2 FM 的角色 Factorization Machine 的价值在于: - 一阶部分可以学习单特征权重 - 二阶部分可以学习特征交互 - 相比显式枚举所有交叉项,更节省参数 在当前场景里,FM 很适合回答: - `translation` 和 `text_score` 是否存在稳定交互 - `rerank_score` 与 `knn_score` 是否协同 - `initial_rank` 与某些语义特征是否共同决定最终排序 ### 13.3 当前 loss 选择 当前用的是简化版 RankNet Pairwise Loss: - 对每个有序 pair `(i, j)`,希望 `score_i > score_j` - 用 `softplus(-(s_i - s_j))` 作为损失 也就是: - 若 `s_i - s_j` 很大,loss 很小 - 若 `s_i - s_j` 不够大甚至反了,loss 会增大 当前**不加 deltaNDCG 权重**,目的是先把流程跑通,让误差来源更容易解释。 ### 13.4 为什么暂时不加 LambdaRank 权重 因为当前阶段更重要的是“建立稳定 baseline”: - pair 采样是否正确 - query-group split 是否正确 - 特征是否有用 - 模型是否明显过拟合 如果一开始就加入复杂的 listwise 权重,很容易把问题混在一起,导致难以定位: - 是 label 问题 - 是 feature 问题 - 是 weighting 问题 - 还是 train/valid split 问题 因此现在的选择是合理的:先简单,后增强。 ## 14. 当前离线实验结果与含义 ### 14.1 数据规模 当前一次离线实验使用: - `54` 个 query - 每个 query `100` 个 doc - 共 `5400` 个 `query-doc` 样本 - 共 `154592` 个有效 pair 标签分布大致为: - grade `3`: `1035` - grade `2`: `1633` - grade `1`: `1191` - grade `0`: `1541` 这说明数据量对“验证可学性”已经够用,但对“高置信正式定型”还不算很大。 ### 14.2 cross-validation 结果 当前 FM 模型在 query-group cross-validation 下: - `Primary_Metric_Score = 0.654043` 当前线上 fusion baseline: - `Primary_Metric_Score = 0.641844` 这说明: - 特征里确实有可学习信号 - 模型在交叉验证中可以平均超过手工 fusion ### 14.3 holdout test 结果 为了进一步检查泛化,额外留出 `10` 个 query 作为 holdout test。 holdout 上: - FM:`0.53056` - 当前 fusion baseline:`0.5674` 这说明一个很关键的事实: - **当前模型存在明显泛化风险** - 虽然 cross-validation 平均提升,但在这组留出 query 上没有赢 baseline 这不是坏消息,反而是好消息,因为它告诉我们: - 当前项目已经进入“可以认真做泛化治理”的阶段 - 不是完全没信号,而是信号与过拟合同时存在 ### 14.4 如何理解这个结果 更高层地说,这说明我们已经知道: 1. **LTR 是值得继续做的** - 因为 CV 有提升 2. **当前特征集合还不够干净** - 因为 holdout 掉分 3. **当前模型还不是可上线模型** - 因为泛化未验证通过 所以正式项目下一步不是“直接部署”,而是“收敛特征与验证体系”。 ## 15. FM 权重解释:如何读 feature importance 当前离线脚本已经输出两类权重文件: - `feature_importance_linear.csv` - `feature_importance_interactions.csv` ### 15.1 一阶权重 可以理解为单特征对排序分的直接影响。 当前绝对值较高的一阶特征包括: - `text_knn_score__square` - `knn_primary_score__square` - `has_translation_match` - `knn_score__square` - `text_support_score__square` 直觉上,这说明模型目前非常关注: - KNN 主信号是否强 - text 支撑项是否强 - translation 是否参与命中 但其中 `has_translation_match` 权重很大且为负,也提示一个风险: - 模型可能在学习“当前数据中,依赖 translation 的结果往往不够好” - 这有可能是合理现象,也有可能只是当前样本分布偏差 ### 15.2 二阶交互权重 FM 的核心价值在二阶交互。 当前绝对值较高的交互项包括: - `text_score_fallback_to_es * initial_rank__log` - `text_support_score__log1p * initial_rank__log` - `text_knn_score__square * initial_rank__log` - `has_text_knn * initial_rank__log` - `translation_share * source_share` 这里最值得注意的是:很多强交互都和 `initial_rank__log` 有关。 这通常意味着: - 模型高度依赖“原排序位置”作为先验 - 它在做的事情更像“在现有排序上修修补补” - 而不是“真正只根据语义相关特征重建排序” 这就是当前 holdout 泛化不佳的重要嫌疑之一。 ### 15.3 正式项目中如何用 importance feature importance 不应该只拿来“看着开心”,而应该直接指导迭代: 1. 如果重要特征都来自原始可解释信号 - 说明特征方向健康 2. 如果重要特征高度集中在 rank / stage / fused score - 说明模型可能在记忆旧排序器 3. 如果高权重特征与业务直觉相反 - 要排查: - label 噪声 - query 覆盖偏差 - 特征定义异常 ## 16. 正式开展 LTR 项目的建议路线 ### 16.1 第一阶段:把数据资产固定下来 目标是建立一个稳定、可反复复用的样本生产链路。 建议动作: 1. 固定日志导出格式 - 保证 `query-level`、`doc-level`、`funnel-level` 字段名稳定 2. 固定样本导出脚本 - 不只从 `backend_verbose.log` 临时读 - 后续可直接产出训练 parquet / csv 3. 固定 label join 规则 - 严格按 `tenant_id + query_text + spu_id` 4. 固定评估切分方式 - 始终按 query 分组 - 始终保留固定 holdout query 集 ### 16.2 第二阶段:做 feature ablation 建议按特征族做消融: - 只用 A 类原始特征 - A + B - A + B + C 重点回答: - 去掉 `initial_rank` 相关特征后,是否还能赢 baseline - 去掉 `fused_score` / `stage_score` 后,泛化是否更稳 - translation 相关特征是否真的有效 - image KNN 相关特征是否会引入 query 类型偏差 ### 16.3 第三阶段:扩模型但不破坏验证纪律 只有当: - cross-validation 稳定提升 - holdout test 也提升 再考虑进入下一步: - LambdaRank / LambdaMART - 更强的 GBDT 排序器 - 加入 query-level 特征桶 - 更细粒度 pair weighting ### 16.4 第四阶段:考虑线上集成方式 正式上线前,需要先决定 LTR 放在哪一层: - 替代当前 rerank fusion - 作为 coarse_rank 之后的再排序器 - 作为最终 fused score 的一个额外因子 当前更推荐的顺序是: 1. 先离线验证 2. 再 shadow score 3. 再小流量 AB 而不是直接替换线上排序。 ## 17. 当前最推荐的下一个动作 如果要推动 LTR 项目正式开展,建议下一步按优先级做: 1. 固定一份长期 holdout query 集,不随实验随机变化。 2. 对当前特征做一次系统性裁剪,优先移除强依赖 `initial_rank`、`fused_score`、`stage_score` 的项。 3. 做 feature family ablation,确认“原始语义特征”本身是否足够支撑泛化。 4. 给更多 query 补齐高质量离线标签,尤其是当前容易失真的风格类和属性组合类 query。 5. 在 FM baseline 收敛后,再进入 LambdaMART / GBDT 路线。 一句话总结当前阶段: - **日志体系已经足够支持 LTR 样本化** - **离线标签体系已经足够支持第一版监督训练** - **FM + RankNet 已经证明“有信号可学”** - **但 holdout 结果说明“离正式可上线还有一段特征治理与泛化治理工作要做”**