Commit 16d28bf873926a1801d39fb6cffbaacab52d3c27
1 parent
8c8b9d84
漏斗信息呈现,便于调整参数
Showing
3 changed files
with
527 additions
and
202 deletions
Show diff stats
frontend/static/css/style.css
| ... | ... | @@ -357,16 +357,22 @@ body { |
| 357 | 357 | color: #555; |
| 358 | 358 | border-left: 1px dashed #eee; |
| 359 | 359 | padding-left: 12px; |
| 360 | - max-height: 260px; | |
| 360 | + max-height: 540px; | |
| 361 | 361 | overflow: auto; |
| 362 | 362 | } |
| 363 | 363 | |
| 364 | 364 | .product-debug-title { |
| 365 | 365 | font-weight: 600; |
| 366 | - margin-bottom: 6px; | |
| 366 | + margin-bottom: 8px; | |
| 367 | 367 | color: #333; |
| 368 | 368 | } |
| 369 | 369 | |
| 370 | +.product-debug-subtitle { | |
| 371 | + margin: 10px 0 6px; | |
| 372 | + font-weight: 600; | |
| 373 | + color: #666; | |
| 374 | +} | |
| 375 | + | |
| 370 | 376 | .product-debug-line { |
| 371 | 377 | margin-bottom: 2px; |
| 372 | 378 | } |
| ... | ... | @@ -418,6 +424,191 @@ body { |
| 418 | 424 | word-break: break-word; |
| 419 | 425 | } |
| 420 | 426 | |
| 427 | +.debug-panel { | |
| 428 | + display: flex; | |
| 429 | + flex-direction: column; | |
| 430 | + gap: 14px; | |
| 431 | + padding: 12px; | |
| 432 | + font-family: Menlo, Consolas, "Courier New", monospace; | |
| 433 | + font-size: 12px; | |
| 434 | +} | |
| 435 | + | |
| 436 | +.debug-section-block { | |
| 437 | + background: #fff; | |
| 438 | + border: 1px solid #e8e8e8; | |
| 439 | + border-radius: 10px; | |
| 440 | + padding: 14px; | |
| 441 | +} | |
| 442 | + | |
| 443 | +.debug-section-title { | |
| 444 | + font-size: 13px; | |
| 445 | + font-weight: 700; | |
| 446 | + color: #222; | |
| 447 | + margin-bottom: 10px; | |
| 448 | +} | |
| 449 | + | |
| 450 | +.debug-stage-grid { | |
| 451 | + display: grid; | |
| 452 | + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); | |
| 453 | + gap: 12px; | |
| 454 | +} | |
| 455 | + | |
| 456 | +.debug-stage-card { | |
| 457 | + border: 1px solid #ececec; | |
| 458 | + border-radius: 8px; | |
| 459 | + padding: 12px; | |
| 460 | + background: linear-gradient(180deg, #fff 0%, #fafafa 100%); | |
| 461 | +} | |
| 462 | + | |
| 463 | +.debug-stage-title { | |
| 464 | + font-size: 13px; | |
| 465 | + font-weight: 700; | |
| 466 | + color: #333; | |
| 467 | +} | |
| 468 | + | |
| 469 | +.debug-stage-subtitle { | |
| 470 | + margin: 4px 0 8px; | |
| 471 | + color: #888; | |
| 472 | + font-size: 11px; | |
| 473 | +} | |
| 474 | + | |
| 475 | +.debug-metrics { | |
| 476 | + display: grid; | |
| 477 | + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); | |
| 478 | + gap: 8px; | |
| 479 | +} | |
| 480 | + | |
| 481 | +.debug-metric { | |
| 482 | + padding: 8px 9px; | |
| 483 | + background: #f7f7f7; | |
| 484 | + border-radius: 6px; | |
| 485 | + border: 1px solid #efefef; | |
| 486 | +} | |
| 487 | + | |
| 488 | +.debug-metric-label { | |
| 489 | + font-size: 11px; | |
| 490 | + color: #777; | |
| 491 | + margin-bottom: 2px; | |
| 492 | +} | |
| 493 | + | |
| 494 | +.debug-metric-value { | |
| 495 | + color: #222; | |
| 496 | + font-weight: 600; | |
| 497 | + word-break: break-word; | |
| 498 | +} | |
| 499 | + | |
| 500 | +.debug-score-pills { | |
| 501 | + display: flex; | |
| 502 | + flex-wrap: wrap; | |
| 503 | + gap: 8px; | |
| 504 | + margin-bottom: 6px; | |
| 505 | +} | |
| 506 | + | |
| 507 | +.debug-score-pill { | |
| 508 | + display: inline-flex; | |
| 509 | + align-items: center; | |
| 510 | + gap: 8px; | |
| 511 | + padding: 6px 10px; | |
| 512 | + border-radius: 999px; | |
| 513 | + border: 1px solid #e3e3e3; | |
| 514 | + background: #f7f7f7; | |
| 515 | +} | |
| 516 | + | |
| 517 | +.debug-score-pill-label { | |
| 518 | + color: #666; | |
| 519 | +} | |
| 520 | + | |
| 521 | +.debug-score-pill-value { | |
| 522 | + color: #111; | |
| 523 | + font-weight: 700; | |
| 524 | +} | |
| 525 | + | |
| 526 | +.tone-es { | |
| 527 | + background: #f8f1ff; | |
| 528 | + border-color: #e6d5ff; | |
| 529 | +} | |
| 530 | + | |
| 531 | +.tone-coarse { | |
| 532 | + background: #eef8ff; | |
| 533 | + border-color: #cae8ff; | |
| 534 | +} | |
| 535 | + | |
| 536 | +.tone-fine { | |
| 537 | + background: #f3fbef; | |
| 538 | + border-color: #d8f1c8; | |
| 539 | +} | |
| 540 | + | |
| 541 | +.tone-rerank { | |
| 542 | + background: #fff4e8; | |
| 543 | + border-color: #ffd9b0; | |
| 544 | +} | |
| 545 | + | |
| 546 | +.tone-final { | |
| 547 | + background: #fff1f0; | |
| 548 | + border-color: #ffc9c4; | |
| 549 | +} | |
| 550 | + | |
| 551 | +.tone-neutral { | |
| 552 | + background: #f5f5f5; | |
| 553 | +} | |
| 554 | + | |
| 555 | +.debug-details { | |
| 556 | + margin-top: 10px; | |
| 557 | +} | |
| 558 | + | |
| 559 | +.debug-details summary { | |
| 560 | + cursor: pointer; | |
| 561 | + color: #555; | |
| 562 | + font-weight: 600; | |
| 563 | +} | |
| 564 | + | |
| 565 | +.debug-json-pre { | |
| 566 | + margin-top: 8px; | |
| 567 | + padding: 10px; | |
| 568 | + background: #f5f5f5; | |
| 569 | + border-radius: 6px; | |
| 570 | + overflow: auto; | |
| 571 | + max-height: 240px; | |
| 572 | + white-space: pre-wrap; | |
| 573 | + word-break: break-word; | |
| 574 | +} | |
| 575 | + | |
| 576 | +.debug-timing-list { | |
| 577 | + display: flex; | |
| 578 | + flex-direction: column; | |
| 579 | + gap: 8px; | |
| 580 | +} | |
| 581 | + | |
| 582 | +.debug-timing-row { | |
| 583 | + display: grid; | |
| 584 | + grid-template-columns: 220px 1fr 90px; | |
| 585 | + gap: 10px; | |
| 586 | + align-items: center; | |
| 587 | +} | |
| 588 | + | |
| 589 | +.debug-timing-label { | |
| 590 | + color: #444; | |
| 591 | +} | |
| 592 | + | |
| 593 | +.debug-timing-bar-wrap { | |
| 594 | + height: 10px; | |
| 595 | + background: #f0f0f0; | |
| 596 | + border-radius: 999px; | |
| 597 | + overflow: hidden; | |
| 598 | +} | |
| 599 | + | |
| 600 | +.debug-timing-bar { | |
| 601 | + height: 100%; | |
| 602 | + background: linear-gradient(90deg, #f39c12 0%, #e74c3c 100%); | |
| 603 | + border-radius: 999px; | |
| 604 | +} | |
| 605 | + | |
| 606 | +.debug-timing-value { | |
| 607 | + text-align: right; | |
| 608 | + color: #666; | |
| 609 | + font-weight: 600; | |
| 610 | +} | |
| 611 | + | |
| 421 | 612 | .product-debug-link { |
| 422 | 613 | display: inline-block; |
| 423 | 614 | margin-top: 0; |
| ... | ... | @@ -687,10 +878,41 @@ footer span { |
| 687 | 878 | } |
| 688 | 879 | |
| 689 | 880 | .product-grid { |
| 690 | - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | |
| 691 | - gap: 15px; | |
| 692 | 881 | padding: 15px; |
| 693 | 882 | } |
| 883 | + | |
| 884 | + .product-card { | |
| 885 | + flex-direction: column; | |
| 886 | + } | |
| 887 | + | |
| 888 | + .product-main { | |
| 889 | + width: 100%; | |
| 890 | + } | |
| 891 | + | |
| 892 | + .product-image-wrapper { | |
| 893 | + width: 100%; | |
| 894 | + max-width: 320px; | |
| 895 | + } | |
| 896 | + | |
| 897 | + .product-debug { | |
| 898 | + width: 100%; | |
| 899 | + border-left: none; | |
| 900 | + border-top: 1px dashed #eee; | |
| 901 | + padding-left: 0; | |
| 902 | + padding-top: 12px; | |
| 903 | + } | |
| 904 | + | |
| 905 | + .debug-stage-grid { | |
| 906 | + grid-template-columns: 1fr; | |
| 907 | + } | |
| 908 | + | |
| 909 | + .debug-timing-row { | |
| 910 | + grid-template-columns: 1fr; | |
| 911 | + } | |
| 912 | + | |
| 913 | + .debug-timing-value { | |
| 914 | + text-align: left; | |
| 915 | + } | |
| 694 | 916 | |
| 695 | 917 | .pagination { |
| 696 | 918 | padding: 20px 15px; |
| ... | ... | @@ -699,10 +921,6 @@ footer span { |
| 699 | 921 | } |
| 700 | 922 | |
| 701 | 923 | @media (max-width: 480px) { |
| 702 | - .product-grid { | |
| 703 | - grid-template-columns: repeat(2, 1fr); | |
| 704 | - } | |
| 705 | - | |
| 706 | 924 | .header-left { |
| 707 | 925 | gap: 15px; |
| 708 | 926 | } | ... | ... |
frontend/static/js/app.js
| ... | ... | @@ -407,89 +407,12 @@ function displayResults(data) { |
| 407 | 407 | |
| 408 | 408 | let debugHtml = ''; |
| 409 | 409 | if (debug) { |
| 410 | - const esScore = typeof debug.es_score === 'number' ? debug.es_score.toFixed(4) : String(debug.es_score ?? ''); | |
| 411 | - const es_score_normalized = typeof debug.es_score_normalized === 'number' | |
| 412 | - ? debug.es_score_normalized.toFixed(4) | |
| 413 | - : (debug.es_score_normalized == null ? '' : String(debug.es_score_normalized)); | |
| 414 | - const rerankScore = typeof debug.rerank_score === 'number' | |
| 415 | - ? debug.rerank_score.toFixed(4) | |
| 416 | - : (debug.rerank_score == null ? '' : String(debug.rerank_score)); | |
| 417 | - | |
| 418 | - const fusedScore = typeof debug.fused_score === 'number' | |
| 419 | - ? debug.fused_score.toFixed(4) | |
| 420 | - : (debug.fused_score == null ? '' : String(debug.fused_score)); | |
| 421 | - | |
| 422 | - // Build multilingual title info | |
| 423 | - let titleLines = ''; | |
| 424 | - if (debug.title_multilingual && typeof debug.title_multilingual === 'object') { | |
| 425 | - Object.entries(debug.title_multilingual).forEach(([lang, val]) => { | |
| 426 | - if (val) { | |
| 427 | - titleLines += `<div class="product-debug-line">title.${escapeHtml(String(lang))}: ${escapeHtml(String(val))}</div>`; | |
| 428 | - } | |
| 429 | - }); | |
| 430 | - } | |
| 431 | - | |
| 432 | - const resultJson = customStringify(result); | |
| 433 | - const rawUrl = `${API_BASE_URL}/search/es-doc/${encodeURIComponent(spuId)}?tenant_id=${encodeURIComponent(tenantId)}`; | |
| 434 | - const rerankInputHtml = debug.rerank_input | |
| 435 | - ? ` | |
| 436 | - <details open> | |
| 437 | - <summary>Rerank input</summary> | |
| 438 | - <pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 220px;">${escapeHtml(customStringify(debug.rerank_input))}</pre> | |
| 439 | - </details> | |
| 440 | - ` | |
| 441 | - : ''; | |
| 442 | - const styleIntentHtml = debug.style_intent_sku | |
| 443 | - ? ` | |
| 444 | - <details open> | |
| 445 | - <summary>Selected SKU</summary> | |
| 446 | - <pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 220px;">${escapeHtml(customStringify(debug.style_intent_sku))}</pre> | |
| 447 | - </details> | |
| 448 | - ` | |
| 449 | - : ''; | |
| 450 | - const matchedQueriesHtml = debug.matched_queries | |
| 451 | - ? ` | |
| 452 | - <details open> | |
| 453 | - <summary>matched_queries</summary> | |
| 454 | - <pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 220px;">${escapeHtml(customStringify(debug.matched_queries))}</pre> | |
| 455 | - </details> | |
| 456 | - ` | |
| 457 | - : ''; | |
| 458 | - | |
| 459 | - debugHtml = ` | |
| 460 | - <div class="product-debug"> | |
| 461 | - <div class="product-debug-title">Ranking Debug</div> | |
| 462 | - <div class="product-debug-line">spu_id: ${escapeHtml(String(spuId || ''))}</div> | |
| 463 | - <div class="product-debug-line">Position before rerank: ${escapeHtml(String(debug.initial_rank ?? ''))}</div> | |
| 464 | - <div class="product-debug-line">Position after rerank: ${escapeHtml(String(debug.final_rank ?? ''))}</div> | |
| 465 | - <div class="product-debug-line">ES score: ${esScore}</div> | |
| 466 | - <div class="product-debug-line">ES normalized: ${es_score_normalized}</div> | |
| 467 | - <div class="product-debug-line">Rerank score: ${rerankScore}</div> | |
| 468 | - <div class="product-debug-line">rerank_factor: ${escapeHtml(String(debug.rerank_factor ?? ''))}</div> | |
| 469 | - <div class="product-debug-line">text_score: ${escapeHtml(String(debug.text_score ?? ''))}</div> | |
| 470 | - <div class="product-debug-line">text_factor: ${escapeHtml(String(debug.text_factor ?? ''))}</div> | |
| 471 | - <div class="product-debug-line">knn_score: ${escapeHtml(String(debug.knn_score ?? ''))}</div> | |
| 472 | - <div class="product-debug-line">knn_factor: ${escapeHtml(String(debug.knn_factor ?? ''))}</div> | |
| 473 | - <div class="product-debug-line">Fused score: ${fusedScore}</div> | |
| 474 | - ${titleLines} | |
| 475 | - ${rerankInputHtml} | |
| 476 | - ${styleIntentHtml} | |
| 477 | - ${matchedQueriesHtml} | |
| 478 | - <div class="product-debug-actions"> | |
| 479 | - <button type="button" class="product-debug-inline-result-btn" | |
| 480 | - data-action="toggle-result-inline-doc" | |
| 481 | - data-result-json="${escapeAttr(resultJson)}"> | |
| 482 | - 在结果中显示当前结果数据 | |
| 483 | - </button> | |
| 484 | - <a class="product-debug-link" href="${rawUrl}" target="_blank" rel="noopener noreferrer"> | |
| 485 | - 查看 ES 原始文档 | |
| 486 | - </a> | |
| 487 | - </div> | |
| 488 | - <div class="product-result-doc-panel" hidden> | |
| 489 | - <pre class="product-result-doc-pre"></pre> | |
| 490 | - </div> | |
| 491 | - </div> | |
| 492 | - `; | |
| 410 | + debugHtml = buildProductDebugHtml({ | |
| 411 | + debug, | |
| 412 | + result, | |
| 413 | + spuId, | |
| 414 | + tenantId, | |
| 415 | + }); | |
| 493 | 416 | } |
| 494 | 417 | |
| 495 | 418 | html += ` |
| ... | ... | @@ -527,6 +450,126 @@ function displayResults(data) { |
| 527 | 450 | grid.innerHTML = html; |
| 528 | 451 | } |
| 529 | 452 | |
| 453 | +function formatDebugNumber(value, digits = 4) { | |
| 454 | + if (typeof value === 'number' && Number.isFinite(value)) { | |
| 455 | + return value.toFixed(digits); | |
| 456 | + } | |
| 457 | + return value == null || value === '' ? 'N/A' : String(value); | |
| 458 | +} | |
| 459 | + | |
| 460 | +function renderMetricList(items) { | |
| 461 | + const rows = items | |
| 462 | + .filter((item) => item && item.value !== undefined && item.value !== null && item.value !== '') | |
| 463 | + .map((item) => ` | |
| 464 | + <div class="debug-metric"> | |
| 465 | + <div class="debug-metric-label">${escapeHtml(item.label)}</div> | |
| 466 | + <div class="debug-metric-value">${escapeHtml(String(item.value))}</div> | |
| 467 | + </div> | |
| 468 | + `) | |
| 469 | + .join(''); | |
| 470 | + return rows ? `<div class="debug-metrics">${rows}</div>` : ''; | |
| 471 | +} | |
| 472 | + | |
| 473 | +function renderScorePills(items) { | |
| 474 | + const pills = items | |
| 475 | + .filter((item) => item && item.value !== undefined && item.value !== null && item.value !== '') | |
| 476 | + .map((item) => ` | |
| 477 | + <div class="debug-score-pill ${item.tone || ''}"> | |
| 478 | + <span class="debug-score-pill-label">${escapeHtml(item.label)}</span> | |
| 479 | + <span class="debug-score-pill-value">${escapeHtml(String(item.value))}</span> | |
| 480 | + </div> | |
| 481 | + `) | |
| 482 | + .join(''); | |
| 483 | + return pills ? `<div class="debug-score-pills">${pills}</div>` : ''; | |
| 484 | +} | |
| 485 | + | |
| 486 | +function renderJsonDetails(title, payload, open = false) { | |
| 487 | + if (!payload || (typeof payload === 'object' && Object.keys(payload).length === 0)) { | |
| 488 | + return ''; | |
| 489 | + } | |
| 490 | + return ` | |
| 491 | + <details class="debug-details" ${open ? 'open' : ''}> | |
| 492 | + <summary>${escapeHtml(title)}</summary> | |
| 493 | + <pre class="debug-json-pre">${escapeHtml(customStringify(payload))}</pre> | |
| 494 | + </details> | |
| 495 | + `; | |
| 496 | +} | |
| 497 | + | |
| 498 | +function buildProductDebugHtml({ debug, result, spuId, tenantId }) { | |
| 499 | + const resultJson = customStringify(result); | |
| 500 | + const rawUrl = `${API_BASE_URL}/search/es-doc/${encodeURIComponent(spuId)}?tenant_id=${encodeURIComponent(tenantId)}`; | |
| 501 | + | |
| 502 | + const rankSummary = renderMetricList([ | |
| 503 | + { label: 'Initial Rank', value: debug.initial_rank ?? 'N/A' }, | |
| 504 | + { label: 'Final Rank', value: debug.final_rank ?? 'N/A' }, | |
| 505 | + { label: 'Rank Delta', value: (debug.initial_rank && debug.final_rank) ? String(debug.initial_rank - debug.final_rank) : 'N/A' }, | |
| 506 | + { label: 'SPU', value: spuId || 'N/A' }, | |
| 507 | + ]); | |
| 508 | + | |
| 509 | + const stageScores = renderScorePills([ | |
| 510 | + { label: 'ES', value: formatDebugNumber(debug.es_score), tone: 'tone-es' }, | |
| 511 | + { label: 'ES Norm', value: formatDebugNumber(debug.es_score_normalized), tone: 'tone-neutral' }, | |
| 512 | + { label: 'Coarse', value: formatDebugNumber(debug.coarse_score), tone: 'tone-coarse' }, | |
| 513 | + { label: 'Fine', value: formatDebugNumber(debug.fine_score), tone: 'tone-fine' }, | |
| 514 | + { label: 'Rerank', value: formatDebugNumber(debug.rerank_score), tone: 'tone-rerank' }, | |
| 515 | + { label: 'Fused', value: formatDebugNumber(debug.fused_score), tone: 'tone-final' }, | |
| 516 | + ]); | |
| 517 | + | |
| 518 | + const factorMetrics = renderMetricList([ | |
| 519 | + { label: 'coarse_text_factor', value: formatDebugNumber(debug.coarse_text_factor) }, | |
| 520 | + { label: 'coarse_knn_factor', value: formatDebugNumber(debug.coarse_knn_factor) }, | |
| 521 | + { label: 'text_factor', value: formatDebugNumber(debug.text_factor) }, | |
| 522 | + { label: 'knn_factor', value: formatDebugNumber(debug.knn_factor) }, | |
| 523 | + { label: 'fine_factor', value: formatDebugNumber(debug.fine_factor) }, | |
| 524 | + { label: 'rerank_factor', value: formatDebugNumber(debug.rerank_factor) }, | |
| 525 | + ]); | |
| 526 | + | |
| 527 | + const signalMetrics = renderMetricList([ | |
| 528 | + { label: 'text_score', value: formatDebugNumber(debug.text_score) }, | |
| 529 | + { label: 'text_source', value: formatDebugNumber(debug.text_source_score) }, | |
| 530 | + { label: 'text_translation', value: formatDebugNumber(debug.text_translation_score) }, | |
| 531 | + { label: 'text_primary', value: formatDebugNumber(debug.text_primary_score) }, | |
| 532 | + { label: 'text_support', value: formatDebugNumber(debug.text_support_score) }, | |
| 533 | + { label: 'knn_score', value: formatDebugNumber(debug.knn_score) }, | |
| 534 | + { label: 'text_knn', value: formatDebugNumber(debug.text_knn_score) }, | |
| 535 | + { label: 'image_knn', value: formatDebugNumber(debug.image_knn_score) }, | |
| 536 | + ]); | |
| 537 | + | |
| 538 | + const titlePayload = {}; | |
| 539 | + if (debug.title_multilingual) titlePayload.title = debug.title_multilingual; | |
| 540 | + if (debug.brief_multilingual) titlePayload.brief = debug.brief_multilingual; | |
| 541 | + if (debug.vendor_multilingual) titlePayload.vendor = debug.vendor_multilingual; | |
| 542 | + | |
| 543 | + return ` | |
| 544 | + <div class="product-debug"> | |
| 545 | + <div class="product-debug-title">Ranking Funnel</div> | |
| 546 | + ${rankSummary} | |
| 547 | + ${stageScores} | |
| 548 | + <div class="product-debug-subtitle">Fusion Factors</div> | |
| 549 | + ${factorMetrics} | |
| 550 | + <div class="product-debug-subtitle">Signal Breakdown</div> | |
| 551 | + ${signalMetrics} | |
| 552 | + ${renderJsonDetails('Rerank Input', debug.rerank_input, true)} | |
| 553 | + ${renderJsonDetails('Selected SKU', debug.style_intent_sku, true)} | |
| 554 | + ${renderJsonDetails('Matched Queries', debug.matched_queries, false)} | |
| 555 | + ${renderJsonDetails('Multilingual Fields', titlePayload, false)} | |
| 556 | + <div class="product-debug-actions"> | |
| 557 | + <button type="button" class="product-debug-inline-result-btn" | |
| 558 | + data-action="toggle-result-inline-doc" | |
| 559 | + data-result-json="${escapeAttr(resultJson)}"> | |
| 560 | + 在结果中显示当前结果数据 | |
| 561 | + </button> | |
| 562 | + <a class="product-debug-link" href="${rawUrl}" target="_blank" rel="noopener noreferrer"> | |
| 563 | + 查看 ES 原始文档 | |
| 564 | + </a> | |
| 565 | + </div> | |
| 566 | + <div class="product-result-doc-panel" hidden> | |
| 567 | + <pre class="product-result-doc-pre"></pre> | |
| 568 | + </div> | |
| 569 | + </div> | |
| 570 | + `; | |
| 571 | +} | |
| 572 | + | |
| 530 | 573 | // Display facets as filter tags (一级分类 + 三个属性分面) |
| 531 | 574 | function displayFacets(facets) { |
| 532 | 575 | if (!facets || !Array.isArray(facets)) { |
| ... | ... | @@ -919,127 +962,174 @@ function formatIntentDetectionHtml(intent) { |
| 919 | 962 | return block; |
| 920 | 963 | } |
| 921 | 964 | |
| 965 | +function buildStageCard(title, subtitle, metrics, extraHtml = '') { | |
| 966 | + return ` | |
| 967 | + <div class="debug-stage-card"> | |
| 968 | + <div class="debug-stage-title">${escapeHtml(title)}</div> | |
| 969 | + ${subtitle ? `<div class="debug-stage-subtitle">${escapeHtml(subtitle)}</div>` : ''} | |
| 970 | + ${renderMetricList(metrics)} | |
| 971 | + ${extraHtml} | |
| 972 | + </div> | |
| 973 | + `; | |
| 974 | +} | |
| 975 | + | |
| 976 | +function renderTimingBars(stageTimings) { | |
| 977 | + if (!stageTimings || typeof stageTimings !== 'object') { | |
| 978 | + return ''; | |
| 979 | + } | |
| 980 | + const orderedStages = [ | |
| 981 | + 'query_parsing', | |
| 982 | + 'query_building', | |
| 983 | + 'elasticsearch_search_primary', | |
| 984 | + 'coarse_ranking', | |
| 985 | + 'style_sku_prepare_hits', | |
| 986 | + 'fine_ranking', | |
| 987 | + 'reranking', | |
| 988 | + 'elasticsearch_page_fill', | |
| 989 | + 'result_processing', | |
| 990 | + 'total_search', | |
| 991 | + ]; | |
| 992 | + const entries = Object.entries(stageTimings) | |
| 993 | + .sort((a, b) => { | |
| 994 | + const ai = orderedStages.indexOf(a[0]); | |
| 995 | + const bi = orderedStages.indexOf(b[0]); | |
| 996 | + return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); | |
| 997 | + }); | |
| 998 | + const total = Number(stageTimings.total_search || 0); | |
| 999 | + return ` | |
| 1000 | + <div class="debug-timing-list"> | |
| 1001 | + ${entries.map(([stage, duration]) => { | |
| 1002 | + const numeric = Number(duration) || 0; | |
| 1003 | + const width = total > 0 ? Math.max(2, Math.round((numeric / total) * 100)) : 2; | |
| 1004 | + return ` | |
| 1005 | + <div class="debug-timing-row"> | |
| 1006 | + <div class="debug-timing-label">${escapeHtml(stage)}</div> | |
| 1007 | + <div class="debug-timing-bar-wrap"><div class="debug-timing-bar" style="width:${width}%"></div></div> | |
| 1008 | + <div class="debug-timing-value">${numeric.toFixed(2)}ms</div> | |
| 1009 | + </div> | |
| 1010 | + `; | |
| 1011 | + }).join('')} | |
| 1012 | + </div> | |
| 1013 | + `; | |
| 1014 | +} | |
| 1015 | + | |
| 1016 | +function buildGlobalFunnelHtml(data, debugInfo) { | |
| 1017 | + const queryAnalysis = debugInfo.query_analysis || {}; | |
| 1018 | + const searchParams = debugInfo.search_params || {}; | |
| 1019 | + const featureFlags = debugInfo.feature_flags || {}; | |
| 1020 | + const esResponse = debugInfo.es_response || {}; | |
| 1021 | + const esQueryContext = debugInfo.es_query_context || {}; | |
| 1022 | + const coarseInfo = debugInfo.coarse_rank || {}; | |
| 1023 | + const fineInfo = debugInfo.fine_rank || {}; | |
| 1024 | + const rerankInfo = debugInfo.rerank || {}; | |
| 1025 | + const translations = queryAnalysis.translations || {}; | |
| 1026 | + | |
| 1027 | + const summaryHtml = ` | |
| 1028 | + <div class="debug-section-block"> | |
| 1029 | + <div class="debug-section-title">Query Context</div> | |
| 1030 | + ${renderMetricList([ | |
| 1031 | + { label: 'original_query', value: queryAnalysis.original_query || 'N/A' }, | |
| 1032 | + { label: 'rewritten_query', value: queryAnalysis.rewritten_query || 'N/A' }, | |
| 1033 | + { label: 'detected_language', value: queryAnalysis.detected_language || 'N/A' }, | |
| 1034 | + { label: 'index_languages', value: (queryAnalysis.index_languages || []).join(', ') || 'N/A' }, | |
| 1035 | + { label: 'query_tokens', value: (queryAnalysis.query_tokens || []).join(', ') || 'N/A' }, | |
| 1036 | + { label: 'translation_enabled', value: featureFlags.translation_enabled ? 'enabled' : 'disabled' }, | |
| 1037 | + { label: 'embedding_enabled', value: featureFlags.embedding_enabled ? 'enabled' : 'disabled' }, | |
| 1038 | + { label: 'style_intent_active', value: featureFlags.style_intent_active ? 'yes' : 'no' }, | |
| 1039 | + ])} | |
| 1040 | + ${Object.keys(translations).length ? renderJsonDetails('Translations', translations, true) : ''} | |
| 1041 | + ${formatIntentDetectionHtml(queryAnalysis.intent_detection ?? queryAnalysis.style_intent_profile)} | |
| 1042 | + </div> | |
| 1043 | + `; | |
| 1044 | + | |
| 1045 | + const funnelHtml = ` | |
| 1046 | + <div class="debug-section-block"> | |
| 1047 | + <div class="debug-section-title">Ranking Funnel</div> | |
| 1048 | + <div class="debug-stage-grid"> | |
| 1049 | + ${buildStageCard('ES Recall', 'First-pass retrieval', [ | |
| 1050 | + { label: 'fetch_from', value: searchParams.es_fetch_from ?? 0 }, | |
| 1051 | + { label: 'fetch_size', value: searchParams.es_fetch_size ?? 'N/A' }, | |
| 1052 | + { label: 'total_hits', value: esResponse.total_hits ?? 'N/A' }, | |
| 1053 | + { label: 'es_took_ms', value: esResponse.took_ms ?? 'N/A' }, | |
| 1054 | + { label: 'include_named_queries_score', value: esQueryContext.include_named_queries_score ? 'yes' : 'no' }, | |
| 1055 | + ])} | |
| 1056 | + ${buildStageCard('Coarse Rank', 'Lexical + vector fusion only', [ | |
| 1057 | + { label: 'docs_in', value: coarseInfo.docs_in ?? searchParams.es_fetch_size ?? 'N/A' }, | |
| 1058 | + { label: 'docs_out', value: coarseInfo.docs_out ?? 'N/A' }, | |
| 1059 | + { label: 'formula', value: 'text x knn' }, | |
| 1060 | + ], coarseInfo.fusion ? renderJsonDetails('Coarse Fusion', coarseInfo.fusion, false) : '')} | |
| 1061 | + ${buildStageCard('Fine Rank', 'Lightweight reranker', [ | |
| 1062 | + { label: 'service_url', value: fineInfo.service_url || 'N/A' }, | |
| 1063 | + { label: 'docs', value: fineInfo.docs ?? fineInfo.top_n ?? 'N/A' }, | |
| 1064 | + { label: 'top_n', value: fineInfo.top_n ?? 'N/A' }, | |
| 1065 | + { label: 'query_template', value: fineInfo.query_template || 'N/A' }, | |
| 1066 | + ], fineInfo.meta ? renderJsonDetails('Fine Meta', fineInfo.meta, false) : '')} | |
| 1067 | + ${buildStageCard('Final Rerank', 'Heavy reranker + final fusion', [ | |
| 1068 | + { label: 'service_url', value: rerankInfo.service_url || 'N/A' }, | |
| 1069 | + { label: 'docs', value: rerankInfo.docs ?? 'N/A' }, | |
| 1070 | + { label: 'top_n', value: rerankInfo.top_n ?? 'N/A' }, | |
| 1071 | + { label: 'query_template', value: rerankInfo.query_template || 'N/A' }, | |
| 1072 | + ], `${rerankInfo.fusion ? renderJsonDetails('Final Fusion', rerankInfo.fusion, false) : ''}${rerankInfo.meta ? renderJsonDetails('Rerank Meta', rerankInfo.meta, false) : ''}`)} | |
| 1073 | + ${buildStageCard('Page Return', 'Final slice returned to UI', [ | |
| 1074 | + { label: 'from', value: searchParams.from_ ?? 0 }, | |
| 1075 | + { label: 'size', value: searchParams.size ?? 'N/A' }, | |
| 1076 | + { label: 'returned', value: (data.results || []).length }, | |
| 1077 | + { label: 'max_score', value: formatDebugNumber(esResponse.max_score, 3) }, | |
| 1078 | + ])} | |
| 1079 | + </div> | |
| 1080 | + </div> | |
| 1081 | + `; | |
| 1082 | + | |
| 1083 | + const timingHtml = ` | |
| 1084 | + <div class="debug-section-block"> | |
| 1085 | + <div class="debug-section-title">Timing Breakdown</div> | |
| 1086 | + ${renderTimingBars(debugInfo.stage_timings)} | |
| 1087 | + </div> | |
| 1088 | + `; | |
| 1089 | + | |
| 1090 | + const rawPayloadHtml = ` | |
| 1091 | + <div class="debug-section-block"> | |
| 1092 | + <div class="debug-section-title">Raw Payloads</div> | |
| 1093 | + ${renderJsonDetails('ES Query DSL', debugInfo.es_query, false)} | |
| 1094 | + ${renderJsonDetails('ES Query Context', debugInfo.es_query_context, false)} | |
| 1095 | + ${renderJsonDetails('Search Params', debugInfo.search_params, false)} | |
| 1096 | + </div> | |
| 1097 | + `; | |
| 1098 | + | |
| 1099 | + return ` | |
| 1100 | + <div class="debug-panel"> | |
| 1101 | + ${summaryHtml} | |
| 1102 | + ${funnelHtml} | |
| 1103 | + ${timingHtml} | |
| 1104 | + ${rawPayloadHtml} | |
| 1105 | + </div> | |
| 1106 | + `; | |
| 1107 | +} | |
| 1108 | + | |
| 922 | 1109 | // Display debug info |
| 923 | 1110 | function displayDebugInfo(data) { |
| 924 | 1111 | const debugInfoDiv = document.getElementById('debugInfo'); |
| 925 | - | |
| 1112 | + | |
| 926 | 1113 | if (!state.debug || !data.debug_info) { |
| 927 | - // If debug mode is off or no debug info, show basic query info | |
| 928 | 1114 | if (data.query_info) { |
| 929 | - let html = '<div style="padding: 10px;">'; | |
| 930 | - html += `<div><strong>original_query:</strong> ${escapeHtml(data.query_info.original_query || 'N/A')}</div>`; | |
| 931 | - html += `<div><strong>detected_language:</strong> ${escapeHtml(data.query_info.detected_language || 'N/A')}</div>`; | |
| 932 | - html += '</div>'; | |
| 933 | - debugInfoDiv.innerHTML = html; | |
| 1115 | + debugInfoDiv.innerHTML = ` | |
| 1116 | + <div class="debug-panel"> | |
| 1117 | + <div class="debug-section-block"> | |
| 1118 | + <div class="debug-section-title">Query Context</div> | |
| 1119 | + ${renderMetricList([ | |
| 1120 | + { label: 'original_query', value: data.query_info.original_query || 'N/A' }, | |
| 1121 | + { label: 'detected_language', value: data.query_info.detected_language || 'N/A' }, | |
| 1122 | + ])} | |
| 1123 | + </div> | |
| 1124 | + </div> | |
| 1125 | + `; | |
| 934 | 1126 | } else { |
| 935 | 1127 | debugInfoDiv.innerHTML = ''; |
| 936 | 1128 | } |
| 937 | 1129 | return; |
| 938 | 1130 | } |
| 939 | - | |
| 940 | - // Display comprehensive debug info when debug mode is on | |
| 941 | - const debugInfo = data.debug_info; | |
| 942 | - let html = '<div style="padding: 10px; font-family: monospace; font-size: 12px;">'; | |
| 943 | - | |
| 944 | - // Query Analysis | |
| 945 | - if (debugInfo.query_analysis) { | |
| 946 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Query Analysis:</strong>'; | |
| 947 | - html += `<div>original_query: ${escapeHtml(debugInfo.query_analysis.original_query || 'N/A')}</div>`; | |
| 948 | - html += `<div>query_normalized: ${escapeHtml(debugInfo.query_analysis.query_normalized || 'N/A')}</div>`; | |
| 949 | - html += `<div>rewritten_query: ${escapeHtml(debugInfo.query_analysis.rewritten_query || 'N/A')}</div>`; | |
| 950 | - html += `<div>detected_language: ${escapeHtml(debugInfo.query_analysis.detected_language || 'N/A')}</div>`; | |
| 951 | - html += `<div>index_languages: ${escapeHtml((debugInfo.query_analysis.index_languages || []).join(', ') || 'N/A')}</div>`; | |
| 952 | - html += `<div>query_tokens: ${escapeHtml((debugInfo.query_analysis.query_tokens || []).join(', ') || 'N/A')}</div>`; | |
| 953 | - | |
| 954 | - if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) { | |
| 955 | - html += '<div>translations: '; | |
| 956 | - for (const [lang, translation] of Object.entries(debugInfo.query_analysis.translations)) { | |
| 957 | - if (translation) { | |
| 958 | - html += `${lang}: ${escapeHtml(translation)}; `; | |
| 959 | - } | |
| 960 | - } | |
| 961 | - html += '</div>'; | |
| 962 | - } | |
| 963 | - | |
| 964 | - if (debugInfo.query_analysis.boolean_ast) { | |
| 965 | - html += `<div>boolean_ast: ${escapeHtml(debugInfo.query_analysis.boolean_ast)}</div>`; | |
| 966 | - } | |
| 967 | 1131 | |
| 968 | - const intentPayload = debugInfo.query_analysis.intent_detection ?? debugInfo.query_analysis.style_intent_profile; | |
| 969 | - html += formatIntentDetectionHtml(intentPayload); | |
| 970 | - | |
| 971 | - html += '</div>'; | |
| 972 | - } | |
| 973 | - | |
| 974 | - // Feature Flags | |
| 975 | - if (debugInfo.feature_flags) { | |
| 976 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Feature Flags:</strong>'; | |
| 977 | - html += `<div>translation_enabled: ${debugInfo.feature_flags.translation_enabled ? 'enabled' : 'disabled'}</div>`; | |
| 978 | - html += `<div>embedding_enabled: ${debugInfo.feature_flags.embedding_enabled ? 'enabled' : 'disabled'}</div>`; | |
| 979 | - html += `<div>rerank_enabled: ${debugInfo.feature_flags.rerank_enabled ? 'enabled' : 'disabled'}</div>`; | |
| 980 | - if (debugInfo.feature_flags.style_intent_enabled !== undefined) { | |
| 981 | - html += `<div>style_intent_enabled: ${debugInfo.feature_flags.style_intent_enabled ? 'enabled' : 'disabled'}</div>`; | |
| 982 | - } | |
| 983 | - if (debugInfo.feature_flags.style_intent_active !== undefined) { | |
| 984 | - html += `<div>style_intent_active: ${debugInfo.feature_flags.style_intent_active ? 'yes' : 'no'}</div>`; | |
| 985 | - } | |
| 986 | - html += '</div>'; | |
| 987 | - } | |
| 988 | - | |
| 989 | - // ES Response | |
| 990 | - if (debugInfo.es_response) { | |
| 991 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES Response:</strong>'; | |
| 992 | - html += `<div>took_ms: ${debugInfo.es_response.took_ms}ms</div>`; | |
| 993 | - html += `<div>total_hits: ${debugInfo.es_response.total_hits}</div>`; | |
| 994 | - html += `<div>max_score: ${debugInfo.es_response.max_score?.toFixed(3) || 0}</div>`; | |
| 995 | - html += `<div>es_score_normalization_factor: ${escapeHtml(String(debugInfo.es_response.es_score_normalization_factor ?? ''))}</div>`; | |
| 996 | - html += '</div>'; | |
| 997 | - } | |
| 998 | - | |
| 999 | - if (debugInfo.rerank) { | |
| 1000 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Rerank:</strong>'; | |
| 1001 | - html += `<div>query_template: ${escapeHtml(debugInfo.rerank.query_template || 'N/A')}</div>`; | |
| 1002 | - html += `<div>doc_template: ${escapeHtml(debugInfo.rerank.doc_template || 'N/A')}</div>`; | |
| 1003 | - html += `<div>query_text: ${escapeHtml(debugInfo.rerank.query_text || 'N/A')}</div>`; | |
| 1004 | - html += `<div>docs: ${escapeHtml(String(debugInfo.rerank.docs ?? ''))}</div>`; | |
| 1005 | - html += `<div>top_n: ${escapeHtml(String(debugInfo.rerank.top_n ?? ''))}</div>`; | |
| 1006 | - if (debugInfo.rerank.fusion) { | |
| 1007 | - html += '<div>fusion:</div>'; | |
| 1008 | - html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 160px;">${escapeHtml(customStringify(debugInfo.rerank.fusion))}</pre>`; | |
| 1009 | - } | |
| 1010 | - html += '</div>'; | |
| 1011 | - } | |
| 1012 | - | |
| 1013 | - // Stage Timings | |
| 1014 | - if (debugInfo.stage_timings) { | |
| 1015 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">Stage Timings:</strong>'; | |
| 1016 | - const bounds = debugInfo.stage_time_bounds_ms || {}; | |
| 1017 | - for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) { | |
| 1018 | - const b = bounds[stage]; | |
| 1019 | - if (b && b.start_unix_ms != null && b.end_unix_ms != null) { | |
| 1020 | - 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>`; | |
| 1021 | - } else { | |
| 1022 | - html += `<div>${stage}: ${Number(duration).toFixed(2)}ms</div>`; | |
| 1023 | - } | |
| 1024 | - } | |
| 1025 | - html += '</div>'; | |
| 1026 | - } | |
| 1027 | - | |
| 1028 | - // ES Query | |
| 1029 | - if (debugInfo.es_query) { | |
| 1030 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES Query DSL:</strong>'; | |
| 1031 | - html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 400px;">${escapeHtml(customStringify(debugInfo.es_query))}</pre>`; | |
| 1032 | - html += '</div>'; | |
| 1033 | - } | |
| 1034 | - | |
| 1035 | - if (debugInfo.es_query_context) { | |
| 1036 | - html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES Query Context:</strong>'; | |
| 1037 | - html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 240px;">${escapeHtml(customStringify(debugInfo.es_query_context))}</pre>`; | |
| 1038 | - html += '</div>'; | |
| 1039 | - } | |
| 1040 | - | |
| 1041 | - html += '</div>'; | |
| 1042 | - debugInfoDiv.innerHTML = html; | |
| 1132 | + debugInfoDiv.innerHTML = buildGlobalFunnelHtml(data, data.debug_info); | |
| 1043 | 1133 | } |
| 1044 | 1134 | |
| 1045 | 1135 | // Custom JSON stringify that compresses numeric arrays (like embeddings) to single line |
| ... | ... | @@ -1070,4 +1160,3 @@ function formatDate(dateStr) { |
| 1070 | 1160 | return dateStr; |
| 1071 | 1161 | } |
| 1072 | 1162 | } |
| 1073 | - | ... | ... |
search/searcher.py
| ... | ... | @@ -897,6 +897,16 @@ class Searcher: |
| 897 | 897 | if doc_id is None: |
| 898 | 898 | continue |
| 899 | 899 | rerank_debug_by_doc[str(doc_id)] = item |
| 900 | + coarse_debug_raw = context.get_intermediate_result('coarse_rank_scores', None) | |
| 901 | + coarse_debug_by_doc: Dict[str, Dict[str, Any]] = {} | |
| 902 | + if isinstance(coarse_debug_raw, list): | |
| 903 | + for item in coarse_debug_raw: | |
| 904 | + if not isinstance(item, dict): | |
| 905 | + continue | |
| 906 | + doc_id = item.get("doc_id") | |
| 907 | + if doc_id is None: | |
| 908 | + continue | |
| 909 | + coarse_debug_by_doc[str(doc_id)] = item | |
| 900 | 910 | fine_debug_raw = context.get_intermediate_result('fine_rank_scores', None) |
| 901 | 911 | fine_debug_by_doc: Dict[str, Dict[str, Any]] = {} |
| 902 | 912 | if isinstance(fine_debug_raw, list): |
| ... | ... | @@ -937,6 +947,9 @@ class Searcher: |
| 937 | 947 | rerank_debug = None |
| 938 | 948 | if doc_id is not None: |
| 939 | 949 | rerank_debug = rerank_debug_by_doc.get(str(doc_id)) |
| 950 | + coarse_debug = None | |
| 951 | + if doc_id is not None: | |
| 952 | + coarse_debug = coarse_debug_by_doc.get(str(doc_id)) | |
| 940 | 953 | fine_debug = None |
| 941 | 954 | if doc_id is not None: |
| 942 | 955 | fine_debug = fine_debug_by_doc.get(str(doc_id)) |
| ... | ... | @@ -974,6 +987,11 @@ class Searcher: |
| 974 | 987 | "vendor_multilingual": vendor_multilingual, |
| 975 | 988 | } |
| 976 | 989 | |
| 990 | + if coarse_debug: | |
| 991 | + debug_entry["coarse_score"] = coarse_debug.get("coarse_score") | |
| 992 | + debug_entry["coarse_text_factor"] = coarse_debug.get("coarse_text_factor") | |
| 993 | + debug_entry["coarse_knn_factor"] = coarse_debug.get("coarse_knn_factor") | |
| 994 | + | |
| 977 | 995 | # 若存在重排调试信息,则补充 doc 级别的融合分数信息 |
| 978 | 996 | if rerank_debug: |
| 979 | 997 | debug_entry["doc_id"] = rerank_debug.get("doc_id") | ... | ... |