检索调参与 LTR 工作流
1. 目标
这份文档记录本仓库当前可复用的调参与分析流程,适用于:
- 搜索相关性调优
- rerank fusion 参数实验
- bad case 定位
- 后续 Learning to Rank(LTR)特征设计
2. 先看什么指标
当前主指标不是单一指标,而是一组主 scorecard:
NDCG@20NDCG@50ERR@10Strong_Precision@10Strong_Precision@20Useful_Precision@50Avg_Grade@10Gain_Recall@20
实验排序时使用:
Primary_Metric_Score
说明:
Primary_Metric_Score是上述 8 个指标的均值。- 其中
Avg_Grade@10先除以3再参与平均。 - 这个分数适合做实验排序,但真正决定是否采纳配置,仍然要看关键 query 和各主指标分布。
3. 调参顺序
推荐按以下顺序做,不要一上来只盯最终 rerank 分数。
确认标注覆盖
- 先看 query 是否已有离线标签。
- 没有标签的 query,不要拿它做正式 batch 结论。
- 例如这次
patterned bodysuit不在 canonicalqueries.txt内,默认没有离线标签,只能做 live diagnosis。
判断问题属于哪一层
missing_relevant很多:优先怀疑召回不够。- 召回到了但没进前 20:优先看
coarse_rank/rerank_window。 - 进了 rerank 仍排不好:再看 fusion 或 reranker 行为。
先改覆盖,再改融合
- 如果高相关商品连候选都进不来,只改 rerank fusion 没有意义。
- 典型动作:
- 增大
knn_text_k/knn_text_num_candidates - 增大
knn_image_k/knn_image_num_candidates - 增大
coarse_rank.output_window - 增大
rerank.rerank_window
- 增大
最后才微调 fusion
- 适合调整:
es_bias,es_exponentrerank_bias,rerank_exponenttext_bias,text_exponentknn_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_docstext_knn_docsimage_knn_docstext_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_scoretext_scoreknn_scorererank_scoresource_scoretranslation_scoretext_knn_scoreimage_knn_scorehas_translation_matchhas_text_knnhas_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. 推荐的下次工作
- 给
patterned bodysuit建离线标签集。 - 单独验证“baseline fusion + 更大 KNN/窗口”的 batch 效果。
- 从
debug_info.per_result[].ranking_funnel.*.ltr_features导出训练样本。 - 先做轻量 LambdaMART / GBDT 排序实验,再决定是否继续手工调乘法融合。
8. LTR 项目视角下的数据闭环
如果把后续工作从“手工调 fusion”升级为“正式 LTR 项目”,建议把整个流程理解成一条固定的数据闭环,而不是一次性的离线实验。
核心链路如下:
在线检索服务产生日志特征
searcher.py在debug_info中输出:retrieval_planltr_summaryper_resultranking_funnel
rerank_client.py在_build_ltr_feature_block中把各阶段稳定特征整理成统一结构。
评估框架持久化离线标签
- 标注缓存保存在
artifacts/search_evaluation/search_eval.sqlite3 - 关键表是
relevance_labels - 标签由
scripts/evaluation/eval_framework/constants.py定义,为 4 档:Exact MatchHigh RelevantLow RelevantIrrelevant
- 标注缓存保存在
离线脚本把“日志特征 + 离线标签”拼成训练样本
- 当前实验脚本为
scripts/evaluation/offline_ltr_fit.py - 日志源默认是
logs/backend_verbose.log - 训练粒度是
query x doc - 每个 query 默认取日志中 top-100 结果
- 当前实验脚本为
用 group-aware 排序目标做离线拟合
- 当前先用简单版
RankNet Pairwise Loss - 训练函数用 FM(Factorization Machine)
- 验证方式包含:
- query-group K-fold cross validation
- 单独留出 10 个 query 作为 holdout test
- 当前先用简单版
根据离线结果判断是否能进入正式线上化
- 不是只看训练集和全量拟合表现
- 最关键是看:
- 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_scoretext_scoreknn_scorererank_scorefine_scoresource_scoretranslation_scoretext_primary_scoretext_support_scoretext_knn_scoreimage_knn_scoreknn_primary_scoreknn_support_scorestyle_booststage_scorehas_text_matchhas_translation_matchhas_text_knnhas_image_knntext_score_fallback_to_eshas_style_boost
这些特征的优点是:
- 来源清晰
- 各阶段定义稳定
- 可以直接落盘
- 后续加新特征时不需要破坏旧特征含义
9.3 funnel 级特征
ranking_funnel 很重要,因为它不仅告诉我们“最终分数是什么”,还告诉我们“这个 doc 是怎么一路走到最终排序的”。
例如当前可以直接抽出的 funnel 信息包括:
initial_rankcoarse_rankrerank_rankfinal_rankes_score_normalizedcoarse_scorefused_scorecoarse_stage_scorererank_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 映射为:
Exact Match -> 3High Relevant -> 2Low Relevant -> 1Irrelevant -> 0
因此:
Exact Match会压过其它三档High Relevant会压过Low Relevant和IrrelevantLow 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_idquery_textspu_id
去查 label,并拼回日志中的 query-doc 样本。
11.2 label 语义
4 档标签不是“点击率标签”,而是人工/LLM 语义相关性标签。因此它更接近:
- 检索相关性监督
- 召回和排序质量监督
而不是:
- 转化率预测
- CTR / CVR 预估
所以这套 LTR 更适合作为 semantic relevance ranker,而不是最终商业目标排序器。
11.3 label 的边界与风险
这点需要在项目初期说清楚。
当前标签有几个天然限制:
标签覆盖的是固定 query 集
- 未标注 query 不能直接拿来做正式结论
标签覆盖的是 query 对 doc 的语义相关性
- 不含价格、库存、点击、转化等业务偏好
标签池来自当前检索系统可见候选
- 如果上游召回漏掉了大量相关 doc,LTR 只能在“已见候选”上学习
LLM 标注存在噪声
- 特别是
Exact和High的边界可能不够稳定
- 特别是
因此正式项目中,label 质量治理要作为单独工作项推进,而不是把模型问题和标签问题混在一起。
12. 当前离线特征工程思路
12.1 为什么要做特征工程
虽然 ltr_features 已经比较完整,但直接喂原始特征给 FM 还不够。
原因是:
- 很多数值特征分布偏斜
- 不同特征的量纲差异很大
- 排序规律往往不是线性的
所以当前离线实验里做了轻量级特征展开。
12.2 当前使用的特征展开
对大部分数值特征,会扩展出:
rawlog1psqrtsquareinv = 1 / (1 + x)
比如:
text_score__rawtext_score__log1ptext_score__sqrttext_score__squaretext_score__inv
这类展开的目的很简单:
- 让模型能表达“分数高一点”和“分数非常高”不是同一个效应
- 让模型能表达某些特征的边际收益递减
- 让一些长尾数值特征更稳定
12.3 当前额外构造的组合特征
除了单特征变换外,还增加了少量人工组合特征:
translation_share = translation_score / text_scoresource_share = source_score / text_scoreimage_knn_share = image_knn_score / knn_scoretext_knn_share = text_knn_score / knn_scorererank_x_textrerank_x_knnrerank_x_estext_minus_esknn_minus_textcoarse_minus_rerank
这些特征本质上在回答几类问题:
- 当前 doc 更偏 lexical 还是 translation
- 当前 doc 更偏 text knn 还是 image knn
- rerank 分和上游分数是否协同
- coarse 与 rerank 是否发生明显冲突
12.4 当前特征工程的结论
这套特征工程足够支持“第一版是否有信号”的判断,但还不够适合直接进入生产。
原因是目前的强特征中,已经出现了不少明显依赖现有排序器状态的项,例如:
initial_rank__loginitial_rank__invfused_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
但在当前阶段,首要目标不是追求“最强模型”,而是先回答两个问题:
- 当前日志特征有没有可学习信号?
- 这些信号能否超越手工 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 如何理解这个结果
更高层地说,这说明我们已经知道:
LTR 是值得继续做的
- 因为 CV 有提升
当前特征集合还不够干净
- 因为 holdout 掉分
当前模型还不是可上线模型
- 因为泛化未验证通过
所以正式项目下一步不是“直接部署”,而是“收敛特征与验证体系”。
15. FM 权重解释:如何读 feature importance
当前离线脚本已经输出两类权重文件:
feature_importance_linear.csvfeature_importance_interactions.csv
15.1 一阶权重
可以理解为单特征对排序分的直接影响。
当前绝对值较高的一阶特征包括:
text_knn_score__squareknn_primary_score__squarehas_translation_matchknn_score__squaretext_support_score__square
直觉上,这说明模型目前非常关注:
- KNN 主信号是否强
- text 支撑项是否强
- translation 是否参与命中
但其中 has_translation_match 权重很大且为负,也提示一个风险:
- 模型可能在学习“当前数据中,依赖 translation 的结果往往不够好”
- 这有可能是合理现象,也有可能只是当前样本分布偏差
15.2 二阶交互权重
FM 的核心价值在二阶交互。
当前绝对值较高的交互项包括:
text_score_fallback_to_es * initial_rank__logtext_support_score__log1p * initial_rank__logtext_knn_score__square * initial_rank__loghas_text_knn * initial_rank__logtranslation_share * source_share
这里最值得注意的是:很多强交互都和 initial_rank__log 有关。
这通常意味着:
- 模型高度依赖“原排序位置”作为先验
- 它在做的事情更像“在现有排序上修修补补”
- 而不是“真正只根据语义相关特征重建排序”
这就是当前 holdout 泛化不佳的重要嫌疑之一。
15.3 正式项目中如何用 importance
feature importance 不应该只拿来“看着开心”,而应该直接指导迭代:
如果重要特征都来自原始可解释信号
- 说明特征方向健康
如果重要特征高度集中在 rank / stage / fused score
- 说明模型可能在记忆旧排序器
如果高权重特征与业务直觉相反
- 要排查:
- label 噪声
- query 覆盖偏差
- 特征定义异常
- 要排查:
16. 正式开展 LTR 项目的建议路线
16.1 第一阶段:把数据资产固定下来
目标是建立一个稳定、可反复复用的样本生产链路。
建议动作:
固定日志导出格式
- 保证
query-level、doc-level、funnel-level字段名稳定
- 保证
固定样本导出脚本
- 不只从
backend_verbose.log临时读 - 后续可直接产出训练 parquet / csv
- 不只从
固定 label join 规则
- 严格按
tenant_id + query_text + spu_id
- 严格按
固定评估切分方式
- 始终按 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 的一个额外因子
当前更推荐的顺序是:
- 先离线验证
- 再 shadow score
- 再小流量 AB
而不是直接替换线上排序。
17. 当前最推荐的下一个动作
如果要推动 LTR 项目正式开展,建议下一步按优先级做:
- 固定一份长期 holdout query 集,不随实验随机变化。
- 对当前特征做一次系统性裁剪,优先移除强依赖
initial_rank、fused_score、stage_score的项。 - 做 feature family ablation,确认“原始语义特征”本身是否足够支撑泛化。
- 给更多 query 补齐高质量离线标签,尤其是当前容易失真的风格类和属性组合类 query。
- 在 FM baseline 收敛后,再进入 LambdaMART / GBDT 路线。
一句话总结当前阶段:
- 日志体系已经足够支持 LTR 样本化
- 离线标签体系已经足够支持第一版监督训练
- FM + RankNet 已经证明“有信号可学”
- 但 holdout 结果说明“离正式可上线还有一段特征治理与泛化治理工作要做”