Commit 16d28bf873926a1801d39fb6cffbaacab52d3c27

Authored by tangwang
1 parent 8c8b9d84

漏斗信息呈现,便于调整参数

frontend/static/css/style.css
@@ -357,16 +357,22 @@ body { @@ -357,16 +357,22 @@ body {
357 color: #555; 357 color: #555;
358 border-left: 1px dashed #eee; 358 border-left: 1px dashed #eee;
359 padding-left: 12px; 359 padding-left: 12px;
360 - max-height: 260px; 360 + max-height: 540px;
361 overflow: auto; 361 overflow: auto;
362 } 362 }
363 363
364 .product-debug-title { 364 .product-debug-title {
365 font-weight: 600; 365 font-weight: 600;
366 - margin-bottom: 6px; 366 + margin-bottom: 8px;
367 color: #333; 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 .product-debug-line { 376 .product-debug-line {
371 margin-bottom: 2px; 377 margin-bottom: 2px;
372 } 378 }
@@ -418,6 +424,191 @@ body { @@ -418,6 +424,191 @@ body {
418 word-break: break-word; 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 .product-debug-link { 612 .product-debug-link {
422 display: inline-block; 613 display: inline-block;
423 margin-top: 0; 614 margin-top: 0;
@@ -687,10 +878,41 @@ footer span { @@ -687,10 +878,41 @@ footer span {
687 } 878 }
688 879
689 .product-grid { 880 .product-grid {
690 - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));  
691 - gap: 15px;  
692 padding: 15px; 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 .pagination { 917 .pagination {
696 padding: 20px 15px; 918 padding: 20px 15px;
@@ -699,10 +921,6 @@ footer span { @@ -699,10 +921,6 @@ footer span {
699 } 921 }
700 922
701 @media (max-width: 480px) { 923 @media (max-width: 480px) {
702 - .product-grid {  
703 - grid-template-columns: repeat(2, 1fr);  
704 - }  
705 -  
706 .header-left { 924 .header-left {
707 gap: 15px; 925 gap: 15px;
708 } 926 }
frontend/static/js/app.js
@@ -407,89 +407,12 @@ function displayResults(data) { @@ -407,89 +407,12 @@ function displayResults(data) {
407 407
408 let debugHtml = ''; 408 let debugHtml = '';
409 if (debug) { 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 html += ` 418 html += `
@@ -527,6 +450,126 @@ function displayResults(data) { @@ -527,6 +450,126 @@ function displayResults(data) {
527 grid.innerHTML = html; 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 // Display facets as filter tags (一级分类 + 三个属性分面) 573 // Display facets as filter tags (一级分类 + 三个属性分面)
531 function displayFacets(facets) { 574 function displayFacets(facets) {
532 if (!facets || !Array.isArray(facets)) { 575 if (!facets || !Array.isArray(facets)) {
@@ -919,127 +962,174 @@ function formatIntentDetectionHtml(intent) { @@ -919,127 +962,174 @@ function formatIntentDetectionHtml(intent) {
919 return block; 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 // Display debug info 1109 // Display debug info
923 function displayDebugInfo(data) { 1110 function displayDebugInfo(data) {
924 const debugInfoDiv = document.getElementById('debugInfo'); 1111 const debugInfoDiv = document.getElementById('debugInfo');
925 - 1112 +
926 if (!state.debug || !data.debug_info) { 1113 if (!state.debug || !data.debug_info) {
927 - // If debug mode is off or no debug info, show basic query info  
928 if (data.query_info) { 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 } else { 1126 } else {
935 debugInfoDiv.innerHTML = ''; 1127 debugInfoDiv.innerHTML = '';
936 } 1128 }
937 return; 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 // Custom JSON stringify that compresses numeric arrays (like embeddings) to single line 1135 // Custom JSON stringify that compresses numeric arrays (like embeddings) to single line
@@ -1070,4 +1160,3 @@ function formatDate(dateStr) { @@ -1070,4 +1160,3 @@ function formatDate(dateStr) {
1070 return dateStr; 1160 return dateStr;
1071 } 1161 }
1072 } 1162 }
1073 -  
search/searcher.py
@@ -897,6 +897,16 @@ class Searcher: @@ -897,6 +897,16 @@ class Searcher:
897 if doc_id is None: 897 if doc_id is None:
898 continue 898 continue
899 rerank_debug_by_doc[str(doc_id)] = item 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 fine_debug_raw = context.get_intermediate_result('fine_rank_scores', None) 910 fine_debug_raw = context.get_intermediate_result('fine_rank_scores', None)
901 fine_debug_by_doc: Dict[str, Dict[str, Any]] = {} 911 fine_debug_by_doc: Dict[str, Dict[str, Any]] = {}
902 if isinstance(fine_debug_raw, list): 912 if isinstance(fine_debug_raw, list):
@@ -937,6 +947,9 @@ class Searcher: @@ -937,6 +947,9 @@ class Searcher:
937 rerank_debug = None 947 rerank_debug = None
938 if doc_id is not None: 948 if doc_id is not None:
939 rerank_debug = rerank_debug_by_doc.get(str(doc_id)) 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 fine_debug = None 953 fine_debug = None
941 if doc_id is not None: 954 if doc_id is not None:
942 fine_debug = fine_debug_by_doc.get(str(doc_id)) 955 fine_debug = fine_debug_by_doc.get(str(doc_id))
@@ -974,6 +987,11 @@ class Searcher: @@ -974,6 +987,11 @@ class Searcher:
974 "vendor_multilingual": vendor_multilingual, 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 # 若存在重排调试信息,则补充 doc 级别的融合分数信息 995 # 若存在重排调试信息,则补充 doc 级别的融合分数信息
978 if rerank_debug: 996 if rerank_debug:
979 debug_entry["doc_id"] = rerank_debug.get("doc_id") 997 debug_entry["doc_id"] = rerank_debug.get("doc_id")