Commit 9d0214bbab2cde5b64ed5011ffe10fc825aeae5c
1 parent
45b39796
qp性能优化
Showing
5 changed files
with
30 additions
and
48 deletions
Show diff stats
context/request_context.py
| @@ -41,6 +41,7 @@ class QueryAnalysisResult: | @@ -41,6 +41,7 @@ class QueryAnalysisResult: | ||
| 41 | rewritten_query: Optional[str] = None | 41 | rewritten_query: Optional[str] = None |
| 42 | detected_language: Optional[str] = None | 42 | detected_language: Optional[str] = None |
| 43 | translations: Dict[str, str] = field(default_factory=dict) | 43 | translations: Dict[str, str] = field(default_factory=dict) |
| 44 | + keywords_queries: Dict[str, str] = field(default_factory=dict) | ||
| 44 | query_vector: Optional[List[float]] = None | 45 | query_vector: Optional[List[float]] = None |
| 45 | boolean_ast: Optional[str] = None | 46 | boolean_ast: Optional[str] = None |
| 46 | 47 |
frontend/static/js/app.js
| @@ -1062,6 +1062,7 @@ function buildGlobalFunnelHtml(data, debugInfo) { | @@ -1062,6 +1062,7 @@ function buildGlobalFunnelHtml(data, debugInfo) { | ||
| 1062 | const fineInfo = rankingFunnel.fine_rank || debugInfo.fine_rank || {}; | 1062 | const fineInfo = rankingFunnel.fine_rank || debugInfo.fine_rank || {}; |
| 1063 | const rerankInfo = rankingFunnel.rerank || debugInfo.rerank || {}; | 1063 | const rerankInfo = rankingFunnel.rerank || debugInfo.rerank || {}; |
| 1064 | const translations = queryAnalysis.translations || {}; | 1064 | const translations = queryAnalysis.translations || {}; |
| 1065 | + const keywordsQueries = queryAnalysis.keywords_queries || {}; | ||
| 1065 | 1066 | ||
| 1066 | const summaryHtml = ` | 1067 | const summaryHtml = ` |
| 1067 | <div class="debug-section-block"> | 1068 | <div class="debug-section-block"> |
| @@ -1072,11 +1073,13 @@ function buildGlobalFunnelHtml(data, debugInfo) { | @@ -1072,11 +1073,13 @@ function buildGlobalFunnelHtml(data, debugInfo) { | ||
| 1072 | { label: 'detected_language', value: queryAnalysis.detected_language || 'N/A' }, | 1073 | { label: 'detected_language', value: queryAnalysis.detected_language || 'N/A' }, |
| 1073 | { label: 'index_languages', value: (queryAnalysis.index_languages || []).join(', ') || 'N/A' }, | 1074 | { label: 'index_languages', value: (queryAnalysis.index_languages || []).join(', ') || 'N/A' }, |
| 1074 | { label: 'query_tokens', value: (queryAnalysis.query_tokens || []).join(', ') || 'N/A' }, | 1075 | { label: 'query_tokens', value: (queryAnalysis.query_tokens || []).join(', ') || 'N/A' }, |
| 1076 | + { label: 'base_keywords', value: keywordsQueries.base || 'N/A' }, | ||
| 1075 | { label: 'translation_enabled', value: featureFlags.translation_enabled ? 'enabled' : 'disabled' }, | 1077 | { label: 'translation_enabled', value: featureFlags.translation_enabled ? 'enabled' : 'disabled' }, |
| 1076 | { label: 'embedding_enabled', value: featureFlags.embedding_enabled ? 'enabled' : 'disabled' }, | 1078 | { label: 'embedding_enabled', value: featureFlags.embedding_enabled ? 'enabled' : 'disabled' }, |
| 1077 | { label: 'style_intent_active', value: featureFlags.style_intent_active ? 'yes' : 'no' }, | 1079 | { label: 'style_intent_active', value: featureFlags.style_intent_active ? 'yes' : 'no' }, |
| 1078 | ])} | 1080 | ])} |
| 1079 | ${Object.keys(translations).length ? renderJsonDetails('Translations', translations, true) : ''} | 1081 | ${Object.keys(translations).length ? renderJsonDetails('Translations', translations, true) : ''} |
| 1082 | + ${Object.keys(keywordsQueries).length ? renderJsonDetails('Keywords Queries', keywordsQueries, true) : ''} | ||
| 1080 | ${formatIntentDetectionHtml(queryAnalysis.intent_detection ?? queryAnalysis.style_intent_profile)} | 1083 | ${formatIntentDetectionHtml(queryAnalysis.intent_detection ?? queryAnalysis.style_intent_profile)} |
| 1081 | </div> | 1084 | </div> |
| 1082 | `; | 1085 | `; |
query/query_parser.py
| @@ -359,16 +359,15 @@ class QueryParser: | @@ -359,16 +359,15 @@ class QueryParser: | ||
| 359 | else: | 359 | else: |
| 360 | active_logger.debug(msg) | 360 | active_logger.debug(msg) |
| 361 | 361 | ||
| 362 | + before_wait_t0 = time.perf_counter() | ||
| 363 | + | ||
| 362 | # Stage 1: Normalize | 364 | # Stage 1: Normalize |
| 363 | - normalize_t0 = time.perf_counter() | ||
| 364 | normalized = self.normalizer.normalize(query) | 365 | normalized = self.normalizer.normalize(query) |
| 365 | - normalize_ms = (time.perf_counter() - normalize_t0) * 1000.0 | ||
| 366 | log_debug(f"Normalization completed | '{query}' -> '{normalized}'") | 366 | log_debug(f"Normalization completed | '{query}' -> '{normalized}'") |
| 367 | if context: | 367 | if context: |
| 368 | context.store_intermediate_result('query_normalized', normalized) | 368 | context.store_intermediate_result('query_normalized', normalized) |
| 369 | 369 | ||
| 370 | # Stage 2: Query rewriting | 370 | # Stage 2: Query rewriting |
| 371 | - rewrite_t0 = time.perf_counter() | ||
| 372 | query_text = normalized | 371 | query_text = normalized |
| 373 | rewritten = normalized | 372 | rewritten = normalized |
| 374 | if self.config.query_config.rewrite_dictionary: # Enable rewrite if dictionary exists | 373 | if self.config.query_config.rewrite_dictionary: # Enable rewrite if dictionary exists |
| @@ -379,12 +378,10 @@ class QueryParser: | @@ -379,12 +378,10 @@ class QueryParser: | ||
| 379 | if context: | 378 | if context: |
| 380 | context.store_intermediate_result('rewritten_query', rewritten) | 379 | context.store_intermediate_result('rewritten_query', rewritten) |
| 381 | context.add_warning(f"Query was rewritten: {query_text}") | 380 | context.add_warning(f"Query was rewritten: {query_text}") |
| 382 | - rewrite_ms = (time.perf_counter() - rewrite_t0) * 1000.0 | ||
| 383 | 381 | ||
| 384 | normalized_targets = self._normalize_language_codes(target_languages) | 382 | normalized_targets = self._normalize_language_codes(target_languages) |
| 385 | 383 | ||
| 386 | # Stage 3: Language detection | 384 | # Stage 3: Language detection |
| 387 | - language_detect_t0 = time.perf_counter() | ||
| 388 | detected_lang = self._detect_query_language( | 385 | detected_lang = self._detect_query_language( |
| 389 | query_text, | 386 | query_text, |
| 390 | target_languages=normalized_targets, | 387 | target_languages=normalized_targets, |
| @@ -392,7 +389,6 @@ class QueryParser: | @@ -392,7 +389,6 @@ class QueryParser: | ||
| 392 | # Use default language if detection failed (None or "unknown") | 389 | # Use default language if detection failed (None or "unknown") |
| 393 | if not detected_lang or detected_lang == "unknown": | 390 | if not detected_lang or detected_lang == "unknown": |
| 394 | detected_lang = self.config.query_config.default_language | 391 | detected_lang = self.config.query_config.default_language |
| 395 | - language_detect_ms = (time.perf_counter() - language_detect_t0) * 1000.0 | ||
| 396 | log_info(f"Language detection | Detected language: {detected_lang}") | 392 | log_info(f"Language detection | Detected language: {detected_lang}") |
| 397 | if context: | 393 | if context: |
| 398 | context.store_intermediate_result('detected_language', detected_lang) | 394 | context.store_intermediate_result('detected_language', detected_lang) |
| @@ -433,9 +429,7 @@ class QueryParser: | @@ -433,9 +429,7 @@ class QueryParser: | ||
| 433 | thread_name_prefix="query-enrichment", | 429 | thread_name_prefix="query-enrichment", |
| 434 | ) | 430 | ) |
| 435 | 431 | ||
| 436 | - async_submit_ms = 0.0 | ||
| 437 | try: | 432 | try: |
| 438 | - async_submit_t0 = time.perf_counter() | ||
| 439 | if async_executor is not None: | 433 | if async_executor is not None: |
| 440 | for lang in translation_targets: | 434 | for lang in translation_targets: |
| 441 | model_name = self._pick_query_translation_model( | 435 | model_name = self._pick_query_translation_model( |
| @@ -503,7 +497,6 @@ class QueryParser: | @@ -503,7 +497,6 @@ class QueryParser: | ||
| 503 | future = async_executor.submit(_encode_image_query_vector) | 497 | future = async_executor.submit(_encode_image_query_vector) |
| 504 | future_to_task[future] = ("image_embedding", None) | 498 | future_to_task[future] = ("image_embedding", None) |
| 505 | future_submit_at[future] = time.perf_counter() | 499 | future_submit_at[future] = time.perf_counter() |
| 506 | - async_submit_ms = (time.perf_counter() - async_submit_t0) * 1000.0 | ||
| 507 | except Exception as e: | 500 | except Exception as e: |
| 508 | error_msg = f"Async query enrichment submission failed | Error: {str(e)}" | 501 | error_msg = f"Async query enrichment submission failed | Error: {str(e)}" |
| 509 | log_info(error_msg) | 502 | log_info(error_msg) |
| @@ -516,14 +509,8 @@ class QueryParser: | @@ -516,14 +509,8 @@ class QueryParser: | ||
| 516 | future_submit_at.clear() | 509 | future_submit_at.clear() |
| 517 | 510 | ||
| 518 | # Stage 4: Query analysis (tokenization) now overlaps with async enrichment work. | 511 | # Stage 4: Query analysis (tokenization) now overlaps with async enrichment work. |
| 519 | - query_analysis_t0 = time.perf_counter() | ||
| 520 | - query_tokenizer_t0 = time.perf_counter() | ||
| 521 | query_tokenizer_result = text_analysis_cache.get_tokenizer_result(query_text) | 512 | query_tokenizer_result = text_analysis_cache.get_tokenizer_result(query_text) |
| 522 | - query_tokenizer_ms = (time.perf_counter() - query_tokenizer_t0) * 1000.0 | ||
| 523 | - query_token_extract_t0 = time.perf_counter() | ||
| 524 | query_tokens = self._extract_tokens(query_tokenizer_result) | 513 | query_tokens = self._extract_tokens(query_tokenizer_result) |
| 525 | - query_token_extract_ms = (time.perf_counter() - query_token_extract_t0) * 1000.0 | ||
| 526 | - query_analysis_ms = (time.perf_counter() - query_analysis_t0) * 1000.0 | ||
| 527 | 514 | ||
| 528 | log_debug(f"Query analysis | Query tokens: {query_tokens}") | 515 | log_debug(f"Query analysis | Query tokens: {query_tokens}") |
| 529 | if context: | 516 | if context: |
| @@ -541,6 +528,7 @@ class QueryParser: | @@ -541,6 +528,7 @@ class QueryParser: | ||
| 541 | keywords_base_ms = (time.perf_counter() - keywords_base_t0) * 1000.0 | 528 | keywords_base_ms = (time.perf_counter() - keywords_base_t0) * 1000.0 |
| 542 | except Exception as e: | 529 | except Exception as e: |
| 543 | log_info(f"Base keyword extraction failed | Error: {e}") | 530 | log_info(f"Base keyword extraction failed | Error: {e}") |
| 531 | + before_wait_ms = (time.perf_counter() - before_wait_t0) * 1000.0 | ||
| 544 | 532 | ||
| 545 | # Wait for translation + embedding concurrently; shared budget depends on whether | 533 | # Wait for translation + embedding concurrently; shared budget depends on whether |
| 546 | # the detected language belongs to caller-provided target_languages. | 534 | # the detected language belongs to caller-provided target_languages. |
| @@ -569,7 +557,6 @@ class QueryParser: | @@ -569,7 +557,6 @@ class QueryParser: | ||
| 569 | async_wait_t0 = time.perf_counter() | 557 | async_wait_t0 = time.perf_counter() |
| 570 | done, not_done = wait(list(future_to_task.keys()), timeout=budget_sec) | 558 | done, not_done = wait(list(future_to_task.keys()), timeout=budget_sec) |
| 571 | async_wait_ms = (time.perf_counter() - async_wait_t0) * 1000.0 | 559 | async_wait_ms = (time.perf_counter() - async_wait_t0) * 1000.0 |
| 572 | - async_collect_t0 = time.perf_counter() | ||
| 573 | for future in done: | 560 | for future in done: |
| 574 | task_type, lang = future_to_task[future] | 561 | task_type, lang = future_to_task[future] |
| 575 | t0 = future_submit_at.pop(future, None) | 562 | t0 = future_submit_at.pop(future, None) |
| @@ -630,7 +617,6 @@ class QueryParser: | @@ -630,7 +617,6 @@ class QueryParser: | ||
| 630 | log_info(timeout_msg) | 617 | log_info(timeout_msg) |
| 631 | if context: | 618 | if context: |
| 632 | context.add_warning(timeout_msg) | 619 | context.add_warning(timeout_msg) |
| 633 | - async_collect_ms = (time.perf_counter() - async_collect_t0) * 1000.0 | ||
| 634 | 620 | ||
| 635 | if async_executor: | 621 | if async_executor: |
| 636 | async_executor.shutdown(wait=False) | 622 | async_executor.shutdown(wait=False) |
| @@ -639,7 +625,6 @@ class QueryParser: | @@ -639,7 +625,6 @@ class QueryParser: | ||
| 639 | context.store_intermediate_result("translations", translations) | 625 | context.store_intermediate_result("translations", translations) |
| 640 | else: | 626 | else: |
| 641 | async_wait_ms = 0.0 | 627 | async_wait_ms = 0.0 |
| 642 | - async_collect_ms = 0.0 | ||
| 643 | 628 | ||
| 644 | tail_sync_t0 = time.perf_counter() | 629 | tail_sync_t0 = time.perf_counter() |
| 645 | keywords_queries: Dict[str, str] = {} | 630 | keywords_queries: Dict[str, str] = {} |
| @@ -655,6 +640,9 @@ class QueryParser: | @@ -655,6 +640,9 @@ class QueryParser: | ||
| 655 | base_keywords_query=keywords_base_query, | 640 | base_keywords_query=keywords_base_query, |
| 656 | ) | 641 | ) |
| 657 | keyword_tail_ms = (time.perf_counter() - keywords_t0) * 1000.0 | 642 | keyword_tail_ms = (time.perf_counter() - keywords_t0) * 1000.0 |
| 643 | + if context: | ||
| 644 | + context.store_intermediate_result("keywords_queries", keywords_queries) | ||
| 645 | + log_info(f"Keyword extraction completed | keywords_queries={keywords_queries}") | ||
| 658 | except Exception as e: | 646 | except Exception as e: |
| 659 | log_info(f"Keyword extraction failed | Error: {e}") | 647 | log_info(f"Keyword extraction failed | Error: {e}") |
| 660 | 648 | ||
| @@ -671,39 +659,15 @@ class QueryParser: | @@ -671,39 +659,15 @@ class QueryParser: | ||
| 671 | keywords_queries=keywords_queries, | 659 | keywords_queries=keywords_queries, |
| 672 | _text_analysis_cache=text_analysis_cache, | 660 | _text_analysis_cache=text_analysis_cache, |
| 673 | ) | 661 | ) |
| 674 | - style_intent_t0 = time.perf_counter() | ||
| 675 | style_intent_profile = self.style_intent_detector.detect(base_result) | 662 | style_intent_profile = self.style_intent_detector.detect(base_result) |
| 676 | - style_intent_ms = (time.perf_counter() - style_intent_t0) * 1000.0 | ||
| 677 | - product_title_exclusion_t0 = time.perf_counter() | ||
| 678 | product_title_exclusion_profile = self.product_title_exclusion_detector.detect(base_result) | 663 | product_title_exclusion_profile = self.product_title_exclusion_detector.detect(base_result) |
| 679 | - product_title_exclusion_ms = ( | ||
| 680 | - (time.perf_counter() - product_title_exclusion_t0) * 1000.0 | ||
| 681 | - ) | ||
| 682 | tail_sync_ms = (time.perf_counter() - tail_sync_t0) * 1000.0 | 664 | tail_sync_ms = (time.perf_counter() - tail_sync_t0) * 1000.0 |
| 683 | - before_wait_ms = ( | ||
| 684 | - normalize_ms | ||
| 685 | - + rewrite_ms | ||
| 686 | - + language_detect_ms | ||
| 687 | - + async_submit_ms | ||
| 688 | - + query_analysis_ms | ||
| 689 | - + keywords_base_ms | ||
| 690 | - ) | ||
| 691 | log_info( | 665 | log_info( |
| 692 | "Query parse stage timings | " | 666 | "Query parse stage timings | " |
| 693 | - f"normalize_ms={normalize_ms:.1f} | " | ||
| 694 | - f"rewrite_ms={rewrite_ms:.1f} | " | ||
| 695 | - f"language_detect_ms={language_detect_ms:.1f} | " | ||
| 696 | - f"query_tokenizer_ms={query_tokenizer_ms:.1f} | " | ||
| 697 | - f"query_token_extract_ms={query_token_extract_ms:.1f} | " | ||
| 698 | - f"query_analysis_ms={query_analysis_ms:.1f} | " | ||
| 699 | - f"async_submit_ms={async_submit_ms:.1f} | " | ||
| 700 | f"before_wait_ms={before_wait_ms:.1f} | " | 667 | f"before_wait_ms={before_wait_ms:.1f} | " |
| 701 | f"async_wait_ms={async_wait_ms:.1f} | " | 668 | f"async_wait_ms={async_wait_ms:.1f} | " |
| 702 | - f"async_collect_ms={async_collect_ms:.1f} | " | ||
| 703 | f"base_keywords_ms={keywords_base_ms:.1f} | " | 669 | f"base_keywords_ms={keywords_base_ms:.1f} | " |
| 704 | f"keyword_tail_ms={keyword_tail_ms:.1f} | " | 670 | f"keyword_tail_ms={keyword_tail_ms:.1f} | " |
| 705 | - f"style_intent_ms={style_intent_ms:.1f} | " | ||
| 706 | - f"product_title_exclusion_ms={product_title_exclusion_ms:.1f} | " | ||
| 707 | f"tail_sync_ms={tail_sync_ms:.1f}" | 671 | f"tail_sync_ms={tail_sync_ms:.1f}" |
| 708 | ) | 672 | ) |
| 709 | if context: | 673 | if context: |
query/tokenization.py
| @@ -89,11 +89,18 @@ def _build_phrase_candidates(tokens: Sequence[str], max_ngram: int) -> List[str] | @@ -89,11 +89,18 @@ def _build_phrase_candidates(tokens: Sequence[str], max_ngram: int) -> List[str] | ||
| 89 | return phrases | 89 | return phrases |
| 90 | 90 | ||
| 91 | 91 | ||
| 92 | -def _build_coarse_tokens(text: str, fine_tokens: Sequence[str]) -> List[str]: | ||
| 93 | - coarse_tokens = _dedupe_preserve_order(simple_tokenize_query(text)) | ||
| 94 | - if contains_han_text(text) and fine_tokens: | ||
| 95 | - return list(_dedupe_preserve_order(fine_tokens)) | ||
| 96 | - return coarse_tokens | 92 | +def _build_coarse_tokens( |
| 93 | + text: str, | ||
| 94 | + *, | ||
| 95 | + language_hint: Optional[str], | ||
| 96 | + tokenizer_tokens: Sequence[str], | ||
| 97 | +) -> List[str]: | ||
| 98 | + normalized_language = normalize_query_text(language_hint) | ||
| 99 | + if normalized_language == "zh" or (contains_han_text(text) and tokenizer_tokens): | ||
| 100 | + # Chinese coarse tokenization should follow the model tokenizer rather than a | ||
| 101 | + # regex that collapses the whole sentence into one CJK span. | ||
| 102 | + return list(_dedupe_preserve_order(tokenizer_tokens)) | ||
| 103 | + return _dedupe_preserve_order(simple_tokenize_query(text)) | ||
| 97 | 104 | ||
| 98 | 105 | ||
| 99 | @dataclass(frozen=True) | 106 | @dataclass(frozen=True) |
| @@ -159,7 +166,11 @@ class QueryTextAnalysisCache: | @@ -159,7 +166,11 @@ class QueryTextAnalysisCache: | ||
| 159 | normalized_text = normalize_query_text(normalized_input) | 166 | normalized_text = normalize_query_text(normalized_input) |
| 160 | fine_raw = extract_token_strings(self.get_tokenizer_result(normalized_input)) | 167 | fine_raw = extract_token_strings(self.get_tokenizer_result(normalized_input)) |
| 161 | fine_tokens = _dedupe_preserve_order(fine_raw) | 168 | fine_tokens = _dedupe_preserve_order(fine_raw) |
| 162 | - coarse_tokens = _build_coarse_tokens(normalized_input, fine_tokens) | 169 | + coarse_tokens = _build_coarse_tokens( |
| 170 | + normalized_input, | ||
| 171 | + language_hint=self.get_language_hint(normalized_input), | ||
| 172 | + tokenizer_tokens=fine_tokens, | ||
| 173 | + ) | ||
| 163 | 174 | ||
| 164 | bundle = TokenizedText( | 175 | bundle = TokenizedText( |
| 165 | text=normalized_input, | 176 | text=normalized_input, |
search/searcher.py
| @@ -446,6 +446,7 @@ class Searcher: | @@ -446,6 +446,7 @@ class Searcher: | ||
| 446 | rewritten_query=parsed_query.rewritten_query, | 446 | rewritten_query=parsed_query.rewritten_query, |
| 447 | detected_language=parsed_query.detected_language, | 447 | detected_language=parsed_query.detected_language, |
| 448 | translations=parsed_query.translations, | 448 | translations=parsed_query.translations, |
| 449 | + keywords_queries=parsed_query.keywords_queries, | ||
| 449 | query_vector=parsed_query.query_vector.tolist() if parsed_query.query_vector is not None else None, | 450 | query_vector=parsed_query.query_vector.tolist() if parsed_query.query_vector is not None else None, |
| 450 | ) | 451 | ) |
| 451 | context.metadata["feature_flags"]["style_intent_active"] = self._has_style_intent(parsed_query) | 452 | context.metadata["feature_flags"]["style_intent_active"] = self._has_style_intent(parsed_query) |
| @@ -454,6 +455,7 @@ class Searcher: | @@ -454,6 +455,7 @@ class Searcher: | ||
| 454 | f"查询解析完成 | 原查询: '{parsed_query.original_query}' | " | 455 | f"查询解析完成 | 原查询: '{parsed_query.original_query}' | " |
| 455 | f"重写后: '{parsed_query.rewritten_query}' | " | 456 | f"重写后: '{parsed_query.rewritten_query}' | " |
| 456 | f"语言: {parsed_query.detected_language} | " | 457 | f"语言: {parsed_query.detected_language} | " |
| 458 | + f"关键词: {parsed_query.keywords_queries} | " | ||
| 457 | f"文本向量: {'是' if parsed_query.query_vector is not None else '否'} | " | 459 | f"文本向量: {'是' if parsed_query.query_vector is not None else '否'} | " |
| 458 | f"图片向量: {'是' if getattr(parsed_query, 'image_query_vector', None) is not None else '否'}", | 460 | f"图片向量: {'是' if getattr(parsed_query, 'image_query_vector', None) is not None else '否'}", |
| 459 | extra={'reqid': context.reqid, 'uid': context.uid} | 461 | extra={'reqid': context.reqid, 'uid': context.uid} |
| @@ -1172,6 +1174,7 @@ class Searcher: | @@ -1172,6 +1174,7 @@ class Searcher: | ||
| 1172 | "detected_language": context.query_analysis.detected_language, | 1174 | "detected_language": context.query_analysis.detected_language, |
| 1173 | "index_languages": index_langs, | 1175 | "index_languages": index_langs, |
| 1174 | "translations": context.query_analysis.translations, | 1176 | "translations": context.query_analysis.translations, |
| 1177 | + "keywords_queries": context.query_analysis.keywords_queries, | ||
| 1175 | "has_vector": context.query_analysis.query_vector is not None, | 1178 | "has_vector": context.query_analysis.query_vector is not None, |
| 1176 | "has_image_vector": getattr(parsed_query, "image_query_vector", None) is not None, | 1179 | "has_image_vector": getattr(parsed_query, "image_query_vector", None) is not None, |
| 1177 | "query_tokens": getattr(parsed_query, "query_tokens", []), | 1180 | "query_tokens": getattr(parsed_query, "query_tokens", []), |