Commit 8ae95af035b04223384ac1c087887c9aed2c4bca
1 parent
4650fcec
1. Stage Timings: 为每个阶段耗时补充起止时间戳。
2, 漏了一些重要的stage,比如「款式意图 SKU 预筛选(StyleSkuSelector.prepare_hits)」,补上这个stage
Showing
4 changed files
with
99 additions
and
16 deletions
Show diff stats
context/request_context.py
| ... | ... | @@ -27,6 +27,8 @@ class RequestContextStage(Enum): |
| 27 | 27 | ELASTICSEARCH_PAGE_FILL = "elasticsearch_page_fill" |
| 28 | 28 | RESULT_PROCESSING = "result_processing" |
| 29 | 29 | RERANKING = "reranking" |
| 30 | + # 款式意图 SKU 预筛选(StyleSkuSelector.prepare_hits) | |
| 31 | + STYLE_SKU_PREPARE_HITS = "style_sku_prepare_hits" | |
| 30 | 32 | |
| 31 | 33 | |
| 32 | 34 | @dataclass |
| ... | ... | @@ -48,6 +50,8 @@ class PerformanceMetrics: |
| 48 | 50 | """性能指标""" |
| 49 | 51 | stage_timings: Dict[str, float] = field(default_factory=dict) |
| 50 | 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 | 55 | total_duration: float = 0.0 |
| 52 | 56 | extra_metrics: Dict[str, Any] = field(default_factory=dict) |
| 53 | 57 | |
| ... | ... | @@ -141,8 +145,13 @@ class RequestContext: |
| 141 | 145 | return 0.0 |
| 142 | 146 | |
| 143 | 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 | 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 | 156 | self.logger.debug( |
| 148 | 157 | f"End stage | {stage.value} | duration: {duration_ms:.2f}ms", |
| ... | ... | @@ -280,6 +289,12 @@ class RequestContext: |
| 280 | 289 | 'stage_timings_ms': { |
| 281 | 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 | 298 | 'stage_percentages': self.calculate_stage_percentages() |
| 284 | 299 | }, |
| 285 | 300 | 'results': { |
| ... | ... | @@ -307,9 +322,17 @@ class RequestContext: |
| 307 | 322 | # 添加各阶段耗时 |
| 308 | 323 | if summary['performance']['stage_timings_ms']: |
| 309 | 324 | msg_parts.append("Stage durations:") |
| 325 | + bounds_map = summary['performance'].get('stage_time_bounds_ms') or {} | |
| 310 | 326 | for stage, duration in summary['performance']['stage_timings_ms'].items(): |
| 311 | 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 | 338 | if summary['query_analysis']['original_query']: | ... | ... |
| ... | ... | @@ -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 | 927 | // Stage Timings |
| 928 | 928 | if (debugInfo.stage_timings) { |
| 929 | 929 | html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Stage Timings:</strong>'; |
| 930 | + const bounds = debugInfo.stage_time_bounds_ms || {}; | |
| 930 | 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 | 939 | html += '</div>'; |
| 934 | 940 | } | ... | ... |
search/searcher.py
| ... | ... | @@ -256,13 +256,19 @@ class Searcher: |
| 256 | 256 | parsed_query: ParsedQuery, |
| 257 | 257 | context: Optional[RequestContext] = None, |
| 258 | 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 | 273 | def search( |
| 268 | 274 | self, |
| ... | ... | @@ -667,6 +673,15 @@ class Searcher: |
| 667 | 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 | 685 | # Step 5: Result processing |
| 671 | 686 | context.start_stage(RequestContextStage.RESULT_PROCESSING) |
| 672 | 687 | try: |
| ... | ... | @@ -701,12 +716,6 @@ class Searcher: |
| 701 | 716 | es_hits, |
| 702 | 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 | 720 | # Format results using ResultFormatter |
| 712 | 721 | formatted_results = ResultFormatter.format_search_results( |
| ... | ... | @@ -832,6 +841,12 @@ class Searcher: |
| 832 | 841 | "stage_timings": { |
| 833 | 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 | 850 | "search_params": context.metadata.get('search_params', {}) |
| 836 | 851 | } |
| 837 | 852 | if per_result_debug: | ... | ... |