Commit 1f071951dc0ac6987c18bfa9fa4058a41c6809c6
1 parent
edd38328
补充调试信息,记录包括各个阶段的 比如query分析结果 检索表达式 各阶段耗时 ES搜索的检索表达式
Showing
5 changed files
with
133 additions
and
54 deletions
Show diff stats
api/models.py
| ... | ... | @@ -17,6 +17,7 @@ class SearchRequest(BaseModel): |
| 17 | 17 | aggregations: Optional[Dict[str, Any]] = Field(None, description="Aggregation specifications") |
| 18 | 18 | sort_by: Optional[str] = Field(None, description="Sort field name") |
| 19 | 19 | sort_order: Optional[str] = Field("desc", description="Sort order: 'asc' or 'desc'") |
| 20 | + debug: bool = Field(False, description="Enable debug information output") | |
| 20 | 21 | |
| 21 | 22 | |
| 22 | 23 | class ImageSearchRequest(BaseModel): |
| ... | ... | @@ -35,6 +36,7 @@ class SearchResponse(BaseModel): |
| 35 | 36 | aggregations: Dict[str, Any] = Field(default_factory=dict, description="Aggregation results") |
| 36 | 37 | query_info: Dict[str, Any] = Field(default_factory=dict, description="Query processing information") |
| 37 | 38 | performance_info: Optional[Dict[str, Any]] = Field(None, description="Detailed performance timing information") |
| 39 | + debug_info: Optional[Dict[str, Any]] = Field(None, description="Debug information (only when debug=True)") | |
| 38 | 40 | |
| 39 | 41 | |
| 40 | 42 | class DocumentResponse(BaseModel): | ... | ... |
api/routes/search.py
| ... | ... | @@ -71,7 +71,8 @@ async def search(request: SearchRequest, http_request: Request): |
| 71 | 71 | context=context, |
| 72 | 72 | aggregations=request.aggregations, |
| 73 | 73 | sort_by=request.sort_by, |
| 74 | - sort_order=request.sort_order | |
| 74 | + sort_order=request.sort_order, | |
| 75 | + debug=request.debug | |
| 75 | 76 | ) |
| 76 | 77 | |
| 77 | 78 | # Include performance summary in response |
| ... | ... | @@ -85,7 +86,8 @@ async def search(request: SearchRequest, http_request: Request): |
| 85 | 86 | took_ms=result.took_ms, |
| 86 | 87 | aggregations=result.aggregations, |
| 87 | 88 | query_info=result.query_info, |
| 88 | - performance_info=performance_summary | |
| 89 | + performance_info=performance_summary, | |
| 90 | + debug_info=result.debug_info | |
| 89 | 91 | ) |
| 90 | 92 | |
| 91 | 93 | except Exception as e: | ... | ... |
frontend/index.html
| ... | ... | @@ -109,10 +109,10 @@ |
| 109 | 109 | <!-- Pagination --> |
| 110 | 110 | <div class="pagination" id="pagination" style="display: none;"></div> |
| 111 | 111 | |
| 112 | - <!-- Debug Info (Collapsible) --> | |
| 112 | + <!-- Debug Info --> | |
| 113 | 113 | <details class="debug-info"> |
| 114 | - <summary>Debug Info (Query Details)</summary> | |
| 115 | - <div id="queryInfo" class="query-info-content"></div> | |
| 114 | + <summary>调试信息</summary> | |
| 115 | + <div id="debugInfo"></div> | |
| 116 | 116 | </details> |
| 117 | 117 | </div> |
| 118 | 118 | ... | ... |
frontend/static/js/app.js
| ... | ... | @@ -13,12 +13,15 @@ let state = { |
| 13 | 13 | sortBy: '', |
| 14 | 14 | sortOrder: 'desc', |
| 15 | 15 | aggregations: null, |
| 16 | - lastSearchData: null | |
| 16 | + lastSearchData: null, | |
| 17 | + debug: true // Always enable debug mode for test frontend | |
| 17 | 18 | }; |
| 18 | 19 | |
| 19 | 20 | // Initialize |
| 20 | 21 | document.addEventListener('DOMContentLoaded', function() { |
| 21 | 22 | console.log('SearchEngine loaded'); |
| 23 | + console.log('Debug mode: always enabled (test frontend)'); | |
| 24 | + | |
| 22 | 25 | document.getElementById('searchInput').focus(); |
| 23 | 26 | }); |
| 24 | 27 | |
| ... | ... | @@ -100,7 +103,8 @@ async function performSearch(page = 1) { |
| 100 | 103 | filters: Object.keys(state.filters).length > 0 ? state.filters : null, |
| 101 | 104 | aggregations: aggregations, |
| 102 | 105 | sort_by: state.sortBy || null, |
| 103 | - sort_order: state.sortOrder | |
| 106 | + sort_order: state.sortOrder, | |
| 107 | + debug: state.debug | |
| 104 | 108 | }) |
| 105 | 109 | }); |
| 106 | 110 | |
| ... | ... | @@ -116,7 +120,7 @@ async function performSearch(page = 1) { |
| 116 | 120 | displayResults(data); |
| 117 | 121 | displayAggregations(data.aggregations); |
| 118 | 122 | displayPagination(); |
| 119 | - displayQueryInfo(data.query_info); | |
| 123 | + displayDebugInfo(data); | |
| 120 | 124 | updateProductCount(data.total); |
| 121 | 125 | updateClearFiltersButton(); |
| 122 | 126 | |
| ... | ... | @@ -483,58 +487,92 @@ function goToPage(page) { |
| 483 | 487 | window.scrollTo({ top: 0, behavior: 'smooth' }); |
| 484 | 488 | } |
| 485 | 489 | |
| 486 | -// Display query info | |
| 487 | -function displayQueryInfo(queryInfo) { | |
| 488 | - if (!queryInfo) return; | |
| 490 | +// Display debug info | |
| 491 | +function displayDebugInfo(data) { | |
| 492 | + const debugInfoDiv = document.getElementById('debugInfo'); | |
| 493 | + | |
| 494 | + if (!state.debug || !data.debug_info) { | |
| 495 | + // If debug mode is off or no debug info, show basic query info | |
| 496 | + if (data.query_info) { | |
| 497 | + let html = '<div style="padding: 10px;">'; | |
| 498 | + html += `<div><strong>查询:</strong> ${escapeHtml(data.query_info.original_query || 'N/A')}</div>`; | |
| 499 | + html += `<div><strong>语言:</strong> ${getLanguageName(data.query_info.detected_language)}</div>`; | |
| 500 | + html += '</div>'; | |
| 501 | + debugInfoDiv.innerHTML = html; | |
| 502 | + } else { | |
| 503 | + debugInfoDiv.innerHTML = ''; | |
| 504 | + } | |
| 505 | + return; | |
| 506 | + } | |
| 489 | 507 | |
| 490 | - const queryInfoDiv = document.getElementById('queryInfo'); | |
| 508 | + // Display comprehensive debug info when debug mode is on | |
| 509 | + const debugInfo = data.debug_info; | |
| 510 | + let html = '<div style="padding: 10px; font-family: monospace; font-size: 12px;">'; | |
| 511 | + | |
| 512 | + // Query Analysis | |
| 513 | + if (debugInfo.query_analysis) { | |
| 514 | + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">查询分析:</strong>'; | |
| 515 | + html += `<div>原始查询: ${escapeHtml(debugInfo.query_analysis.original_query || 'N/A')}</div>`; | |
| 516 | + html += `<div>标准化: ${escapeHtml(debugInfo.query_analysis.normalized_query || 'N/A')}</div>`; | |
| 517 | + html += `<div>重写后: ${escapeHtml(debugInfo.query_analysis.rewritten_query || 'N/A')}</div>`; | |
| 518 | + html += `<div>检测语言: ${getLanguageName(debugInfo.query_analysis.detected_language)}</div>`; | |
| 519 | + html += `<div>域: ${escapeHtml(debugInfo.query_analysis.domain || 'default')}</div>`; | |
| 520 | + html += `<div>简单查询: ${debugInfo.query_analysis.is_simple_query ? '是' : '否'}</div>`; | |
| 521 | + | |
| 522 | + if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) { | |
| 523 | + html += '<div>翻译: '; | |
| 524 | + for (const [lang, translation] of Object.entries(debugInfo.query_analysis.translations)) { | |
| 525 | + if (translation) { | |
| 526 | + html += `${getLanguageName(lang)}: ${escapeHtml(translation)}; `; | |
| 527 | + } | |
| 528 | + } | |
| 529 | + html += '</div>'; | |
| 530 | + } | |
| 531 | + | |
| 532 | + if (debugInfo.query_analysis.boolean_ast) { | |
| 533 | + html += `<div>布尔AST: ${escapeHtml(debugInfo.query_analysis.boolean_ast)}</div>`; | |
| 534 | + } | |
| 535 | + | |
| 536 | + html += `<div>向量嵌入: ${debugInfo.query_analysis.has_vector ? '已启用' : '未启用'}</div>`; | |
| 537 | + html += '</div>'; | |
| 538 | + } | |
| 491 | 539 | |
| 492 | - let html = '<div class="info-grid">'; | |
| 540 | + // Feature Flags | |
| 541 | + if (debugInfo.feature_flags) { | |
| 542 | + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">功能开关:</strong>'; | |
| 543 | + html += `<div>翻译: ${debugInfo.feature_flags.translation_enabled ? '启用' : '禁用'}</div>`; | |
| 544 | + html += `<div>嵌入: ${debugInfo.feature_flags.embedding_enabled ? '启用' : '禁用'}</div>`; | |
| 545 | + html += `<div>重排序: ${debugInfo.feature_flags.rerank_enabled ? '启用' : '禁用'}</div>`; | |
| 546 | + html += '</div>'; | |
| 547 | + } | |
| 493 | 548 | |
| 494 | - html += ` | |
| 495 | - <div class="info-item"> | |
| 496 | - <strong>Original Query</strong> | |
| 497 | - <span>${escapeHtml(queryInfo.original_query || 'N/A')}</span> | |
| 498 | - </div> | |
| 499 | - <div class="info-item"> | |
| 500 | - <strong>Rewritten Query</strong> | |
| 501 | - <span>${escapeHtml(queryInfo.rewritten_query || 'N/A')}</span> | |
| 502 | - </div> | |
| 503 | - <div class="info-item"> | |
| 504 | - <strong>Detected Language</strong> | |
| 505 | - <span>${getLanguageName(queryInfo.detected_language)}</span> | |
| 506 | - </div> | |
| 507 | - <div class="info-item"> | |
| 508 | - <strong>Domain</strong> | |
| 509 | - <span>${escapeHtml(queryInfo.domain || 'default')}</span> | |
| 510 | - </div> | |
| 511 | - `; | |
| 549 | + // ES Response | |
| 550 | + if (debugInfo.es_response) { | |
| 551 | + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES响应:</strong>'; | |
| 552 | + html += `<div>耗时: ${debugInfo.es_response.took_ms}ms</div>`; | |
| 553 | + html += `<div>总命中数: ${debugInfo.es_response.total_hits}</div>`; | |
| 554 | + html += `<div>最高分: ${debugInfo.es_response.max_score?.toFixed(3) || 0}</div>`; | |
| 555 | + html += '</div>'; | |
| 556 | + } | |
| 512 | 557 | |
| 513 | - if (queryInfo.translations && Object.keys(queryInfo.translations).length > 0) { | |
| 514 | - for (const [lang, translation] of Object.entries(queryInfo.translations)) { | |
| 515 | - if (translation) { | |
| 516 | - html += ` | |
| 517 | - <div class="info-item"> | |
| 518 | - <strong>Translation (${getLanguageName(lang)})</strong> | |
| 519 | - <span>${escapeHtml(translation)}</span> | |
| 520 | - </div> | |
| 521 | - `; | |
| 522 | - } | |
| 558 | + // Stage Timings | |
| 559 | + if (debugInfo.stage_timings) { | |
| 560 | + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">各阶段耗时:</strong>'; | |
| 561 | + for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) { | |
| 562 | + html += `<div>${stage}: ${duration.toFixed(2)}ms</div>`; | |
| 523 | 563 | } |
| 564 | + html += '</div>'; | |
| 524 | 565 | } |
| 525 | 566 | |
| 526 | - if (queryInfo.has_vector) { | |
| 527 | - html += ` | |
| 528 | - <div class="info-item"> | |
| 529 | - <strong>Semantic Search</strong> | |
| 530 | - <span style="color: #27ae60;">✓ Enabled (Vector)</span> | |
| 531 | - </div> | |
| 532 | - `; | |
| 567 | + // ES Query | |
| 568 | + if (debugInfo.es_query) { | |
| 569 | + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES查询DSL:</strong>'; | |
| 570 | + html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 400px;">${escapeHtml(JSON.stringify(debugInfo.es_query, null, 2))}</pre>`; | |
| 571 | + html += '</div>'; | |
| 533 | 572 | } |
| 534 | 573 | |
| 535 | 574 | html += '</div>'; |
| 536 | - | |
| 537 | - queryInfoDiv.innerHTML = html; | |
| 575 | + debugInfoDiv.innerHTML = html; | |
| 538 | 576 | } |
| 539 | 577 | |
| 540 | 578 | // Helper functions | ... | ... |
search/searcher.py
| ... | ... | @@ -28,7 +28,8 @@ class SearchResult: |
| 28 | 28 | max_score: float, |
| 29 | 29 | took_ms: int, |
| 30 | 30 | aggregations: Optional[Dict[str, Any]] = None, |
| 31 | - query_info: Optional[Dict[str, Any]] = None | |
| 31 | + query_info: Optional[Dict[str, Any]] = None, | |
| 32 | + debug_info: Optional[Dict[str, Any]] = None | |
| 32 | 33 | ): |
| 33 | 34 | self.hits = hits |
| 34 | 35 | self.total = total |
| ... | ... | @@ -36,10 +37,11 @@ class SearchResult: |
| 36 | 37 | self.took_ms = took_ms |
| 37 | 38 | self.aggregations = aggregations or {} |
| 38 | 39 | self.query_info = query_info or {} |
| 40 | + self.debug_info = debug_info | |
| 39 | 41 | |
| 40 | 42 | def to_dict(self) -> Dict[str, Any]: |
| 41 | 43 | """Convert to dictionary representation.""" |
| 42 | - return { | |
| 44 | + result = { | |
| 43 | 45 | "hits": self.hits, |
| 44 | 46 | "total": self.total, |
| 45 | 47 | "max_score": self.max_score, |
| ... | ... | @@ -47,6 +49,9 @@ class SearchResult: |
| 47 | 49 | "aggregations": self.aggregations, |
| 48 | 50 | "query_info": self.query_info |
| 49 | 51 | } |
| 52 | + if self.debug_info is not None: | |
| 53 | + result["debug_info"] = self.debug_info | |
| 54 | + return result | |
| 50 | 55 | |
| 51 | 56 | |
| 52 | 57 | class Searcher: |
| ... | ... | @@ -106,7 +111,8 @@ class Searcher: |
| 106 | 111 | context: Optional[RequestContext] = None, |
| 107 | 112 | aggregations: Optional[Dict[str, Any]] = None, |
| 108 | 113 | sort_by: Optional[str] = None, |
| 109 | - sort_order: Optional[str] = "desc" | |
| 114 | + sort_order: Optional[str] = "desc", | |
| 115 | + debug: bool = False | |
| 110 | 116 | ) -> SearchResult: |
| 111 | 117 | """ |
| 112 | 118 | Execute search query. |
| ... | ... | @@ -121,6 +127,7 @@ class Searcher: |
| 121 | 127 | aggregations: Aggregation specifications for faceted search |
| 122 | 128 | sort_by: Field name for sorting |
| 123 | 129 | sort_order: Sort order: 'asc' or 'desc' |
| 130 | + debug: Enable debug information output | |
| 124 | 131 | |
| 125 | 132 | Returns: |
| 126 | 133 | SearchResult object |
| ... | ... | @@ -408,6 +415,35 @@ class Searcher: |
| 408 | 415 | total_duration = context.end_stage(RequestContextStage.TOTAL) |
| 409 | 416 | context.performance_metrics.total_duration = total_duration |
| 410 | 417 | |
| 418 | + # Collect debug information if requested | |
| 419 | + debug_info = None | |
| 420 | + if debug: | |
| 421 | + debug_info = { | |
| 422 | + "query_analysis": { | |
| 423 | + "original_query": context.query_analysis.original_query, | |
| 424 | + "normalized_query": context.query_analysis.normalized_query, | |
| 425 | + "rewritten_query": context.query_analysis.rewritten_query, | |
| 426 | + "detected_language": context.query_analysis.detected_language, | |
| 427 | + "translations": context.query_analysis.translations, | |
| 428 | + "has_vector": context.query_analysis.query_vector is not None, | |
| 429 | + "is_simple_query": context.query_analysis.is_simple_query, | |
| 430 | + "boolean_ast": context.get_intermediate_result('boolean_ast'), | |
| 431 | + "domain": context.query_analysis.domain | |
| 432 | + }, | |
| 433 | + "es_query": context.get_intermediate_result('es_query', {}), | |
| 434 | + "es_response": { | |
| 435 | + "took_ms": es_response.get('took', 0), | |
| 436 | + "total_hits": total_value, | |
| 437 | + "max_score": max_score, | |
| 438 | + "shards": es_response.get('_shards', {}) | |
| 439 | + }, | |
| 440 | + "feature_flags": context.metadata.get('feature_flags', {}), | |
| 441 | + "stage_timings": { | |
| 442 | + k: round(v, 2) for k, v in context.performance_metrics.stage_timings.items() | |
| 443 | + }, | |
| 444 | + "search_params": context.metadata.get('search_params', {}) | |
| 445 | + } | |
| 446 | + | |
| 411 | 447 | # Build result |
| 412 | 448 | result = SearchResult( |
| 413 | 449 | hits=hits, |
| ... | ... | @@ -415,7 +451,8 @@ class Searcher: |
| 415 | 451 | max_score=max_score, |
| 416 | 452 | took_ms=int(total_duration), |
| 417 | 453 | aggregations=aggregations, |
| 418 | - query_info=parsed_query.to_dict() | |
| 454 | + query_info=parsed_query.to_dict(), | |
| 455 | + debug_info=debug_info | |
| 419 | 456 | ) |
| 420 | 457 | |
| 421 | 458 | # Log complete performance summary | ... | ... |