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,6 +17,7 @@ class SearchRequest(BaseModel): | ||
| 17 | aggregations: Optional[Dict[str, Any]] = Field(None, description="Aggregation specifications") | 17 | aggregations: Optional[Dict[str, Any]] = Field(None, description="Aggregation specifications") |
| 18 | sort_by: Optional[str] = Field(None, description="Sort field name") | 18 | sort_by: Optional[str] = Field(None, description="Sort field name") |
| 19 | sort_order: Optional[str] = Field("desc", description="Sort order: 'asc' or 'desc'") | 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 | class ImageSearchRequest(BaseModel): | 23 | class ImageSearchRequest(BaseModel): |
| @@ -35,6 +36,7 @@ class SearchResponse(BaseModel): | @@ -35,6 +36,7 @@ class SearchResponse(BaseModel): | ||
| 35 | aggregations: Dict[str, Any] = Field(default_factory=dict, description="Aggregation results") | 36 | aggregations: Dict[str, Any] = Field(default_factory=dict, description="Aggregation results") |
| 36 | query_info: Dict[str, Any] = Field(default_factory=dict, description="Query processing information") | 37 | query_info: Dict[str, Any] = Field(default_factory=dict, description="Query processing information") |
| 37 | performance_info: Optional[Dict[str, Any]] = Field(None, description="Detailed performance timing information") | 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 | class DocumentResponse(BaseModel): | 42 | class DocumentResponse(BaseModel): |
api/routes/search.py
| @@ -71,7 +71,8 @@ async def search(request: SearchRequest, http_request: Request): | @@ -71,7 +71,8 @@ async def search(request: SearchRequest, http_request: Request): | ||
| 71 | context=context, | 71 | context=context, |
| 72 | aggregations=request.aggregations, | 72 | aggregations=request.aggregations, |
| 73 | sort_by=request.sort_by, | 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 | # Include performance summary in response | 78 | # Include performance summary in response |
| @@ -85,7 +86,8 @@ async def search(request: SearchRequest, http_request: Request): | @@ -85,7 +86,8 @@ async def search(request: SearchRequest, http_request: Request): | ||
| 85 | took_ms=result.took_ms, | 86 | took_ms=result.took_ms, |
| 86 | aggregations=result.aggregations, | 87 | aggregations=result.aggregations, |
| 87 | query_info=result.query_info, | 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 | except Exception as e: | 93 | except Exception as e: |
frontend/index.html
| @@ -109,10 +109,10 @@ | @@ -109,10 +109,10 @@ | ||
| 109 | <!-- Pagination --> | 109 | <!-- Pagination --> |
| 110 | <div class="pagination" id="pagination" style="display: none;"></div> | 110 | <div class="pagination" id="pagination" style="display: none;"></div> |
| 111 | 111 | ||
| 112 | - <!-- Debug Info (Collapsible) --> | 112 | + <!-- Debug Info --> |
| 113 | <details class="debug-info"> | 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 | </details> | 116 | </details> |
| 117 | </div> | 117 | </div> |
| 118 | 118 |
frontend/static/js/app.js
| @@ -13,12 +13,15 @@ let state = { | @@ -13,12 +13,15 @@ let state = { | ||
| 13 | sortBy: '', | 13 | sortBy: '', |
| 14 | sortOrder: 'desc', | 14 | sortOrder: 'desc', |
| 15 | aggregations: null, | 15 | aggregations: null, |
| 16 | - lastSearchData: null | 16 | + lastSearchData: null, |
| 17 | + debug: true // Always enable debug mode for test frontend | ||
| 17 | }; | 18 | }; |
| 18 | 19 | ||
| 19 | // Initialize | 20 | // Initialize |
| 20 | document.addEventListener('DOMContentLoaded', function() { | 21 | document.addEventListener('DOMContentLoaded', function() { |
| 21 | console.log('SearchEngine loaded'); | 22 | console.log('SearchEngine loaded'); |
| 23 | + console.log('Debug mode: always enabled (test frontend)'); | ||
| 24 | + | ||
| 22 | document.getElementById('searchInput').focus(); | 25 | document.getElementById('searchInput').focus(); |
| 23 | }); | 26 | }); |
| 24 | 27 | ||
| @@ -100,7 +103,8 @@ async function performSearch(page = 1) { | @@ -100,7 +103,8 @@ async function performSearch(page = 1) { | ||
| 100 | filters: Object.keys(state.filters).length > 0 ? state.filters : null, | 103 | filters: Object.keys(state.filters).length > 0 ? state.filters : null, |
| 101 | aggregations: aggregations, | 104 | aggregations: aggregations, |
| 102 | sort_by: state.sortBy || null, | 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,7 +120,7 @@ async function performSearch(page = 1) { | ||
| 116 | displayResults(data); | 120 | displayResults(data); |
| 117 | displayAggregations(data.aggregations); | 121 | displayAggregations(data.aggregations); |
| 118 | displayPagination(); | 122 | displayPagination(); |
| 119 | - displayQueryInfo(data.query_info); | 123 | + displayDebugInfo(data); |
| 120 | updateProductCount(data.total); | 124 | updateProductCount(data.total); |
| 121 | updateClearFiltersButton(); | 125 | updateClearFiltersButton(); |
| 122 | 126 | ||
| @@ -483,58 +487,92 @@ function goToPage(page) { | @@ -483,58 +487,92 @@ function goToPage(page) { | ||
| 483 | window.scrollTo({ top: 0, behavior: 'smooth' }); | 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 | html += '</div>'; | 574 | html += '</div>'; |
| 536 | - | ||
| 537 | - queryInfoDiv.innerHTML = html; | 575 | + debugInfoDiv.innerHTML = html; |
| 538 | } | 576 | } |
| 539 | 577 | ||
| 540 | // Helper functions | 578 | // Helper functions |
search/searcher.py
| @@ -28,7 +28,8 @@ class SearchResult: | @@ -28,7 +28,8 @@ class SearchResult: | ||
| 28 | max_score: float, | 28 | max_score: float, |
| 29 | took_ms: int, | 29 | took_ms: int, |
| 30 | aggregations: Optional[Dict[str, Any]] = None, | 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 | self.hits = hits | 34 | self.hits = hits |
| 34 | self.total = total | 35 | self.total = total |
| @@ -36,10 +37,11 @@ class SearchResult: | @@ -36,10 +37,11 @@ class SearchResult: | ||
| 36 | self.took_ms = took_ms | 37 | self.took_ms = took_ms |
| 37 | self.aggregations = aggregations or {} | 38 | self.aggregations = aggregations or {} |
| 38 | self.query_info = query_info or {} | 39 | self.query_info = query_info or {} |
| 40 | + self.debug_info = debug_info | ||
| 39 | 41 | ||
| 40 | def to_dict(self) -> Dict[str, Any]: | 42 | def to_dict(self) -> Dict[str, Any]: |
| 41 | """Convert to dictionary representation.""" | 43 | """Convert to dictionary representation.""" |
| 42 | - return { | 44 | + result = { |
| 43 | "hits": self.hits, | 45 | "hits": self.hits, |
| 44 | "total": self.total, | 46 | "total": self.total, |
| 45 | "max_score": self.max_score, | 47 | "max_score": self.max_score, |
| @@ -47,6 +49,9 @@ class SearchResult: | @@ -47,6 +49,9 @@ class SearchResult: | ||
| 47 | "aggregations": self.aggregations, | 49 | "aggregations": self.aggregations, |
| 48 | "query_info": self.query_info | 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 | class Searcher: | 57 | class Searcher: |
| @@ -106,7 +111,8 @@ class Searcher: | @@ -106,7 +111,8 @@ class Searcher: | ||
| 106 | context: Optional[RequestContext] = None, | 111 | context: Optional[RequestContext] = None, |
| 107 | aggregations: Optional[Dict[str, Any]] = None, | 112 | aggregations: Optional[Dict[str, Any]] = None, |
| 108 | sort_by: Optional[str] = None, | 113 | sort_by: Optional[str] = None, |
| 109 | - sort_order: Optional[str] = "desc" | 114 | + sort_order: Optional[str] = "desc", |
| 115 | + debug: bool = False | ||
| 110 | ) -> SearchResult: | 116 | ) -> SearchResult: |
| 111 | """ | 117 | """ |
| 112 | Execute search query. | 118 | Execute search query. |
| @@ -121,6 +127,7 @@ class Searcher: | @@ -121,6 +127,7 @@ class Searcher: | ||
| 121 | aggregations: Aggregation specifications for faceted search | 127 | aggregations: Aggregation specifications for faceted search |
| 122 | sort_by: Field name for sorting | 128 | sort_by: Field name for sorting |
| 123 | sort_order: Sort order: 'asc' or 'desc' | 129 | sort_order: Sort order: 'asc' or 'desc' |
| 130 | + debug: Enable debug information output | ||
| 124 | 131 | ||
| 125 | Returns: | 132 | Returns: |
| 126 | SearchResult object | 133 | SearchResult object |
| @@ -408,6 +415,35 @@ class Searcher: | @@ -408,6 +415,35 @@ class Searcher: | ||
| 408 | total_duration = context.end_stage(RequestContextStage.TOTAL) | 415 | total_duration = context.end_stage(RequestContextStage.TOTAL) |
| 409 | context.performance_metrics.total_duration = total_duration | 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 | # Build result | 447 | # Build result |
| 412 | result = SearchResult( | 448 | result = SearchResult( |
| 413 | hits=hits, | 449 | hits=hits, |
| @@ -415,7 +451,8 @@ class Searcher: | @@ -415,7 +451,8 @@ class Searcher: | ||
| 415 | max_score=max_score, | 451 | max_score=max_score, |
| 416 | took_ms=int(total_duration), | 452 | took_ms=int(total_duration), |
| 417 | aggregations=aggregations, | 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 | # Log complete performance summary | 458 | # Log complete performance summary |