Commit 8ae95af035b04223384ac1c087887c9aed2c4bca

Authored by tangwang
1 parent 4650fcec

1. Stage Timings: 为每个阶段耗时补充起止时间戳。

2, 漏了一些重要的stage,比如「款式意图 SKU
预筛选(StyleSkuSelector.prepare_hits)」,补上这个stage
context/request_context.py
@@ -27,6 +27,8 @@ class RequestContextStage(Enum): @@ -27,6 +27,8 @@ class RequestContextStage(Enum):
27 ELASTICSEARCH_PAGE_FILL = "elasticsearch_page_fill" 27 ELASTICSEARCH_PAGE_FILL = "elasticsearch_page_fill"
28 RESULT_PROCESSING = "result_processing" 28 RESULT_PROCESSING = "result_processing"
29 RERANKING = "reranking" 29 RERANKING = "reranking"
  30 + # 款式意图 SKU 预筛选(StyleSkuSelector.prepare_hits)
  31 + STYLE_SKU_PREPARE_HITS = "style_sku_prepare_hits"
30 32
31 33
32 @dataclass 34 @dataclass
@@ -48,6 +50,8 @@ class PerformanceMetrics: @@ -48,6 +50,8 @@ class PerformanceMetrics:
48 """性能指标""" 50 """性能指标"""
49 stage_timings: Dict[str, float] = field(default_factory=dict) 51 stage_timings: Dict[str, float] = field(default_factory=dict)
50 stage_start_times: Dict[str, float] = field(default_factory=dict) 52 stage_start_times: Dict[str, float] = field(default_factory=dict)
  53 + # 各阶段起止时间(Unix 毫秒,与 time.time() 一致)
  54 + stage_time_bounds_ms: Dict[str, Dict[str, float]] = field(default_factory=dict)
51 total_duration: float = 0.0 55 total_duration: float = 0.0
52 extra_metrics: Dict[str, Any] = field(default_factory=dict) 56 extra_metrics: Dict[str, Any] = field(default_factory=dict)
53 57
@@ -141,8 +145,13 @@ class RequestContext: @@ -141,8 +145,13 @@ class RequestContext:
141 return 0.0 145 return 0.0
142 146
143 start_time = self.performance_metrics.stage_start_times[stage.value] 147 start_time = self.performance_metrics.stage_start_times[stage.value]
144 - duration_ms = (time.time() - start_time) * 1000 148 + end_time = time.time()
  149 + duration_ms = (end_time - start_time) * 1000
145 self.performance_metrics.stage_timings[stage.value] = duration_ms 150 self.performance_metrics.stage_timings[stage.value] = duration_ms
  151 + self.performance_metrics.stage_time_bounds_ms[stage.value] = {
  152 + "start_unix_ms": round(start_time * 1000, 3),
  153 + "end_unix_ms": round(end_time * 1000, 3),
  154 + }
146 155
147 self.logger.debug( 156 self.logger.debug(
148 f"End stage | {stage.value} | duration: {duration_ms:.2f}ms", 157 f"End stage | {stage.value} | duration: {duration_ms:.2f}ms",
@@ -280,6 +289,12 @@ class RequestContext: @@ -280,6 +289,12 @@ class RequestContext:
280 'stage_timings_ms': { 289 'stage_timings_ms': {
281 k: round(v, 2) for k, v in self.performance_metrics.stage_timings.items() 290 k: round(v, 2) for k, v in self.performance_metrics.stage_timings.items()
282 }, 291 },
  292 + 'stage_time_bounds_ms': {
  293 + stage: {
  294 + kk: round(vv, 3) for kk, vv in bounds.items()
  295 + }
  296 + for stage, bounds in self.performance_metrics.stage_time_bounds_ms.items()
  297 + },
283 'stage_percentages': self.calculate_stage_percentages() 298 'stage_percentages': self.calculate_stage_percentages()
284 }, 299 },
285 'results': { 300 'results': {
@@ -307,9 +322,17 @@ class RequestContext: @@ -307,9 +322,17 @@ class RequestContext:
307 # 添加各阶段耗时 322 # 添加各阶段耗时
308 if summary['performance']['stage_timings_ms']: 323 if summary['performance']['stage_timings_ms']:
309 msg_parts.append("Stage durations:") 324 msg_parts.append("Stage durations:")
  325 + bounds_map = summary['performance'].get('stage_time_bounds_ms') or {}
310 for stage, duration in summary['performance']['stage_timings_ms'].items(): 326 for stage, duration in summary['performance']['stage_timings_ms'].items():
311 percentage = summary['performance']['stage_percentages'].get(stage, 0) 327 percentage = summary['performance']['stage_percentages'].get(stage, 0)
312 - msg_parts.append(f" - {stage}: {duration:.2f}ms ({percentage}%)") 328 + b = bounds_map.get(stage) or {}
  329 + if b.get('start_unix_ms') is not None and b.get('end_unix_ms') is not None:
  330 + msg_parts.append(
  331 + f" - {stage}: {duration:.2f}ms ({percentage}%) "
  332 + f"[{b['start_unix_ms']:.3f} -> {b['end_unix_ms']:.3f} ms]"
  333 + )
  334 + else:
  335 + msg_parts.append(f" - {stage}: {duration:.2f}ms ({percentage}%)")
313 336
314 # 添加查询信息 337 # 添加查询信息
315 if summary['query_analysis']['original_query']: 338 if summary['query_analysis']['original_query']:
docs/TODO-prompts.md 0 → 100644
@@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
  1 +
  2 +1. debug信息展示方面
  3 +在这次重构过程中,你基本上了解了从query析到 ES 搜索,再到重排的全流程。
  4 +在这些环节中,有哪些重要的信息对搜索结果产生较大影响,先回顾,适当的完善、优化日志。
  5 +debug_info也给前端更充分的调试信息,包括,丰富每条结果的ranking debug。目前只有这些
  6 +Ranking Debug
  7 +spu_id
  8 +ES score
  9 +ES normalized
  10 +ES norm (rerank input):
  11 +Rerank score
  12 +Fused score
  13 +title.en
  14 +title.zh
  15 +
  16 +还不够,需要详细思考,设计一个更加完整的展示的体系
  17 +比如:
  18 +position(重排前的,重排后最终的)
  19 +ES打分,从ES检索原始打分、相关的因子(minmax归一化的因子。到底用了哪些因子,你自己关注下,用到的要体现出来,让人知道从原始打分怎么得到的ES norm (rerank input))。另外ES normalized,ES norm,要梳理清楚,如果代码不清晰则改代码,展示不清晰则要梳理展示。
  20 +Rerank这一块,除了rerank_score,输入的信息比如输入的doc模板,被选中的sku及其用于辅助reranker相关性的、加到title后面的后缀
  21 +融合公式这一块,除了fused_score,还要包含rerank_factor, text_factor, knn_factor等因子,以及text_score, knn_score, text_source_score, text_translation_score, … 等变量,要清晰的展示。
  22 +证据 matched_queries
  23 +
  24 +
  25 +耗时记录方面:
  26 +1. Stage Timings: 为每个阶段耗时补充起止时间戳。
  27 +2, 我看到total_search大幅度大于上面各个Stage 的总和,可能漏了一些重要的stage,比如「款式意图 SKU 预筛选(StyleSkuSelector.prepare_hits)」,补上这个stage
  28 +
  29 +但是也要注意,不要影响不带debug_info的请求的性能。
  30 +
  31 +
  32 +2. 日志方面也需要完善
  33 +从 query 到 ES 再到重排,对结果影响大的信息,语种检测 + index_languages,Query 向量,ES查询的构建过程中所使用的关键的变量、关键的中间结果,rerank相关信息(intput/window,query/doc 模板,融合公式的详细信息和关键的因子、输入原始值以及中间变量、到最终的融合公式的因子,Page fill,sku_filter_dimension等
  34 +
  35 +
  36 +3. 测试用例方面
  37 +子串匹配(Direct 阶段)是否可能产生误判 — 与尺码/短词强相关
  38 +_is_direct_match 使用 normalized_value in query_text(见 sku_intent_selector.py)。
  39 +词表里 l、m、s、xl,会在常见英文 query 里产生字符级子串命中,例如"l" 是否会匹配 "large …" 里的 l,注意要用分词匹配,检查分词匹配逻辑是否容易产生badcase。
frontend/static/js/app.js
@@ -927,8 +927,14 @@ function displayDebugInfo(data) { @@ -927,8 +927,14 @@ function displayDebugInfo(data) {
927 // Stage Timings 927 // Stage Timings
928 if (debugInfo.stage_timings) { 928 if (debugInfo.stage_timings) {
929 html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Stage Timings:</strong>'; 929 html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Stage Timings:</strong>';
  930 + const bounds = debugInfo.stage_time_bounds_ms || {};
930 for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) { 931 for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) {
931 - html += `<div>${stage}: ${duration.toFixed(2)}ms</div>`; 932 + const b = bounds[stage];
  933 + if (b && b.start_unix_ms != null && b.end_unix_ms != null) {
  934 + html += `<div>${stage}: ${Number(duration).toFixed(2)}ms <span style="color:#666">(start ${b.start_unix_ms} → end ${b.end_unix_ms} unix ms)</span></div>`;
  935 + } else {
  936 + html += `<div>${stage}: ${Number(duration).toFixed(2)}ms</div>`;
  937 + }
932 } 938 }
933 html += '</div>'; 939 html += '</div>';
934 } 940 }
search/searcher.py
@@ -256,13 +256,19 @@ class Searcher: @@ -256,13 +256,19 @@ class Searcher:
256 parsed_query: ParsedQuery, 256 parsed_query: ParsedQuery,
257 context: Optional[RequestContext] = None, 257 context: Optional[RequestContext] = None,
258 ) -> Dict[str, SkuSelectionDecision]: 258 ) -> Dict[str, SkuSelectionDecision]:
259 - decisions = self.style_sku_selector.prepare_hits(es_hits, parsed_query)  
260 - if decisions and context is not None:  
261 - context.store_intermediate_result(  
262 - "style_intent_sku_decisions",  
263 - {doc_id: decision.to_dict() for doc_id, decision in decisions.items()},  
264 - )  
265 - return decisions 259 + if context is not None:
  260 + context.start_stage(RequestContextStage.STYLE_SKU_PREPARE_HITS)
  261 + try:
  262 + decisions = self.style_sku_selector.prepare_hits(es_hits, parsed_query)
  263 + if decisions and context is not None:
  264 + context.store_intermediate_result(
  265 + "style_intent_sku_decisions",
  266 + {doc_id: decision.to_dict() for doc_id, decision in decisions.items()},
  267 + )
  268 + return decisions
  269 + finally:
  270 + if context is not None:
  271 + context.end_stage(RequestContextStage.STYLE_SKU_PREPARE_HITS)
266 272
267 def search( 273 def search(
268 self, 274 self,
@@ -667,6 +673,15 @@ class Searcher: @@ -667,6 +673,15 @@ class Searcher:
667 extra={'reqid': context.reqid, 'uid': context.uid} 673 extra={'reqid': context.reqid, 'uid': context.uid}
668 ) 674 )
669 675
  676 + # 非重排窗口:款式意图在 result_processing 之前执行,便于单独计时且与 ES 召回阶段衔接
  677 + if self._has_style_intent(parsed_query) and not in_rerank_window:
  678 + es_hits_pre = es_response.get("hits", {}).get("hits") or []
  679 + style_intent_decisions = self._apply_style_intent_to_hits(
  680 + es_hits_pre,
  681 + parsed_query,
  682 + context=context,
  683 + )
  684 +
670 # Step 5: Result processing 685 # Step 5: Result processing
671 context.start_stage(RequestContextStage.RESULT_PROCESSING) 686 context.start_stage(RequestContextStage.RESULT_PROCESSING)
672 try: 687 try:
@@ -701,12 +716,6 @@ class Searcher: @@ -701,12 +716,6 @@ class Searcher:
701 es_hits, 716 es_hits,
702 style_intent_decisions, 717 style_intent_decisions,
703 ) 718 )
704 - elif not in_rerank_window:  
705 - style_intent_decisions = self._apply_style_intent_to_hits(  
706 - es_hits,  
707 - parsed_query,  
708 - context=context,  
709 - )  
710 719
711 # Format results using ResultFormatter 720 # Format results using ResultFormatter
712 formatted_results = ResultFormatter.format_search_results( 721 formatted_results = ResultFormatter.format_search_results(
@@ -832,6 +841,12 @@ class Searcher: @@ -832,6 +841,12 @@ class Searcher:
832 "stage_timings": { 841 "stage_timings": {
833 k: round(v, 2) for k, v in context.performance_metrics.stage_timings.items() 842 k: round(v, 2) for k, v in context.performance_metrics.stage_timings.items()
834 }, 843 },
  844 + "stage_time_bounds_ms": {
  845 + stage: {
  846 + kk: round(vv, 3) for kk, vv in bounds.items()
  847 + }
  848 + for stage, bounds in context.performance_metrics.stage_time_bounds_ms.items()
  849 + },
835 "search_params": context.metadata.get('search_params', {}) 850 "search_params": context.metadata.get('search_params', {})
836 } 851 }
837 if per_result_debug: 852 if per_result_debug: