Commit bcada818cdec318c60cea1cacdc1ea4cb44cd2e4
1 parent
bd96cead
last
Showing
7 changed files
with
398 additions
and
198 deletions
Show diff stats
config/config_loader.py
| @@ -47,11 +47,7 @@ class QueryConfig: | @@ -47,11 +47,7 @@ class QueryConfig: | ||
| 47 | # Embedding field names | 47 | # Embedding field names |
| 48 | text_embedding_field: Optional[str] = "title_embedding" | 48 | text_embedding_field: Optional[str] = "title_embedding" |
| 49 | image_embedding_field: Optional[str] = None | 49 | image_embedding_field: Optional[str] = None |
| 50 | - | ||
| 51 | - # Embedding disable thresholds (disable vector search for short queries) | ||
| 52 | - embedding_disable_chinese_char_limit: int = 4 | ||
| 53 | - embedding_disable_english_word_limit: int = 3 | ||
| 54 | - | 50 | + |
| 55 | # Source fields configuration | 51 | # Source fields configuration |
| 56 | source_fields: Optional[List[str]] = None | 52 | source_fields: Optional[List[str]] = None |
| 57 | 53 | ||
| @@ -75,6 +71,7 @@ class QueryConfig: | @@ -75,6 +71,7 @@ class QueryConfig: | ||
| 75 | translation_boost: float = 0.4 | 71 | translation_boost: float = 0.4 |
| 76 | translation_boost_when_source_missing: float = 1.0 | 72 | translation_boost_when_source_missing: float = 1.0 |
| 77 | source_boost_when_missing: float = 0.6 | 73 | source_boost_when_missing: float = 0.6 |
| 74 | + original_query_fallback_boost_when_translation_missing: float = 0.2 | ||
| 78 | keywords_boost: float = 0.1 | 75 | keywords_boost: float = 0.1 |
| 79 | enable_phrase_query: bool = True | 76 | enable_phrase_query: bool = True |
| 80 | tie_breaker_base_query: float = 0.9 | 77 | tie_breaker_base_query: float = 0.9 |
| @@ -249,7 +246,6 @@ class ConfigLoader: | @@ -249,7 +246,6 @@ class ConfigLoader: | ||
| 249 | query_config_data = config_data.get("query_config", {}) | 246 | query_config_data = config_data.get("query_config", {}) |
| 250 | services_data = config_data.get("services", {}) if isinstance(config_data.get("services", {}), dict) else {} | 247 | services_data = config_data.get("services", {}) if isinstance(config_data.get("services", {}), dict) else {} |
| 251 | rewrite_dictionary = self._load_rewrite_dictionary() | 248 | rewrite_dictionary = self._load_rewrite_dictionary() |
| 252 | - embedding_thresholds = query_config_data.get("embedding_disable_thresholds", {}) | ||
| 253 | search_fields_cfg = query_config_data.get("search_fields", {}) | 249 | search_fields_cfg = query_config_data.get("search_fields", {}) |
| 254 | text_strategy_cfg = query_config_data.get("text_query_strategy", {}) | 250 | text_strategy_cfg = query_config_data.get("text_query_strategy", {}) |
| 255 | 251 | ||
| @@ -266,8 +262,6 @@ class ConfigLoader: | @@ -266,8 +262,6 @@ class ConfigLoader: | ||
| 266 | translation_prompts=query_config_data.get("translation_prompts", {}), | 262 | translation_prompts=query_config_data.get("translation_prompts", {}), |
| 267 | text_embedding_field=query_config_data.get("text_embedding_field"), | 263 | text_embedding_field=query_config_data.get("text_embedding_field"), |
| 268 | image_embedding_field=query_config_data.get("image_embedding_field"), | 264 | image_embedding_field=query_config_data.get("image_embedding_field"), |
| 269 | - embedding_disable_chinese_char_limit=embedding_thresholds.get("chinese_char_limit", 4), | ||
| 270 | - embedding_disable_english_word_limit=embedding_thresholds.get("english_word_limit", 3), | ||
| 271 | source_fields=query_config_data.get("source_fields"), | 265 | source_fields=query_config_data.get("source_fields"), |
| 272 | knn_boost=query_config_data.get("knn_boost", 0.25), | 266 | knn_boost=query_config_data.get("knn_boost", 0.25), |
| 273 | multilingual_fields=search_fields_cfg.get( | 267 | multilingual_fields=search_fields_cfg.get( |
| @@ -289,6 +283,9 @@ class ConfigLoader: | @@ -289,6 +283,9 @@ class ConfigLoader: | ||
| 289 | text_strategy_cfg.get("translation_boost_when_source_missing", 1.0) | 283 | text_strategy_cfg.get("translation_boost_when_source_missing", 1.0) |
| 290 | ), | 284 | ), |
| 291 | source_boost_when_missing=float(text_strategy_cfg.get("source_boost_when_missing", 0.6)), | 285 | source_boost_when_missing=float(text_strategy_cfg.get("source_boost_when_missing", 0.6)), |
| 286 | + original_query_fallback_boost_when_translation_missing=float( | ||
| 287 | + text_strategy_cfg.get("original_query_fallback_boost_when_translation_missing", 0.2) | ||
| 288 | + ), | ||
| 292 | keywords_boost=float(text_strategy_cfg.get("keywords_boost", 0.1)), | 289 | keywords_boost=float(text_strategy_cfg.get("keywords_boost", 0.1)), |
| 293 | enable_phrase_query=bool(text_strategy_cfg.get("enable_phrase_query", True)), | 290 | enable_phrase_query=bool(text_strategy_cfg.get("enable_phrase_query", True)), |
| 294 | tie_breaker_base_query=float(text_strategy_cfg.get("tie_breaker_base_query", 0.9)), | 291 | tie_breaker_base_query=float(text_strategy_cfg.get("tie_breaker_base_query", 0.9)), |
| @@ -433,6 +430,7 @@ class ConfigLoader: | @@ -433,6 +430,7 @@ class ConfigLoader: | ||
| 433 | "translation_boost", | 430 | "translation_boost", |
| 434 | "translation_boost_when_source_missing", | 431 | "translation_boost_when_source_missing", |
| 435 | "source_boost_when_missing", | 432 | "source_boost_when_missing", |
| 433 | + "original_query_fallback_boost_when_translation_missing", | ||
| 436 | "keywords_boost", | 434 | "keywords_boost", |
| 437 | "tie_breaker_base_query", | 435 | "tie_breaker_base_query", |
| 438 | "tie_breaker_keywords", | 436 | "tie_breaker_keywords", |
| @@ -482,10 +480,6 @@ class ConfigLoader: | @@ -482,10 +480,6 @@ class ConfigLoader: | ||
| 482 | "translation_service": config.query_config.translation_service, | 480 | "translation_service": config.query_config.translation_service, |
| 483 | "text_embedding_field": config.query_config.text_embedding_field, | 481 | "text_embedding_field": config.query_config.text_embedding_field, |
| 484 | "image_embedding_field": config.query_config.image_embedding_field, | 482 | "image_embedding_field": config.query_config.image_embedding_field, |
| 485 | - "embedding_disable_thresholds": { | ||
| 486 | - "chinese_char_limit": config.query_config.embedding_disable_chinese_char_limit, | ||
| 487 | - "english_word_limit": config.query_config.embedding_disable_english_word_limit | ||
| 488 | - }, | ||
| 489 | "source_fields": config.query_config.source_fields, | 483 | "source_fields": config.query_config.source_fields, |
| 490 | "search_fields": { | 484 | "search_fields": { |
| 491 | "multilingual_fields": config.query_config.multilingual_fields, | 485 | "multilingual_fields": config.query_config.multilingual_fields, |
| @@ -498,6 +492,9 @@ class ConfigLoader: | @@ -498,6 +492,9 @@ class ConfigLoader: | ||
| 498 | "translation_boost": config.query_config.translation_boost, | 492 | "translation_boost": config.query_config.translation_boost, |
| 499 | "translation_boost_when_source_missing": config.query_config.translation_boost_when_source_missing, | 493 | "translation_boost_when_source_missing": config.query_config.translation_boost_when_source_missing, |
| 500 | "source_boost_when_missing": config.query_config.source_boost_when_missing, | 494 | "source_boost_when_missing": config.query_config.source_boost_when_missing, |
| 495 | + "original_query_fallback_boost_when_translation_missing": ( | ||
| 496 | + config.query_config.original_query_fallback_boost_when_translation_missing | ||
| 497 | + ), | ||
| 501 | "keywords_boost": config.query_config.keywords_boost, | 498 | "keywords_boost": config.query_config.keywords_boost, |
| 502 | "enable_phrase_query": config.query_config.enable_phrase_query, | 499 | "enable_phrase_query": config.query_config.enable_phrase_query, |
| 503 | "tie_breaker_base_query": config.query_config.tie_breaker_base_query, | 500 | "tie_breaker_base_query": config.query_config.tie_breaker_base_query, |
docs/DEVELOPER_GUIDE.md
| @@ -241,7 +241,7 @@ docs/ # 文档(含本指南) | @@ -241,7 +241,7 @@ docs/ # 文档(含本指南) | ||
| 241 | 241 | ||
| 242 | ### 6.1 主配置文件 | 242 | ### 6.1 主配置文件 |
| 243 | 243 | ||
| 244 | -- **config/config.yaml**:搜索行为(field_boosts、query_config.search_fields、query_config.text_query_strategy、ranking、function_score、rerank 融合参数)、SPU 配置、**services**(翻译/向量/重排的 provider 与 backends)、tenant_config 等。 | 244 | +- **config/config.yaml**:搜索行为(field_boosts、query_config.search_fields、query_config.text_query_strategy,含翻译失败时的原文兜底 boost、ranking、function_score、rerank 融合参数)、SPU 配置、**services**(翻译/向量/重排的 provider 与 backends)、tenant_config 等。 |
| 245 | - **.env**:敏感信息与部署态变量(DB、ES、Redis、API Key、端口等);不提交敏感值,可提供 `.env.example` 模板。 | 245 | - **.env**:敏感信息与部署态变量(DB、ES、Redis、API Key、端口等);不提交敏感值,可提供 `.env.example` 模板。 |
| 246 | 246 | ||
| 247 | ### 6.2 services 块结构(能力统一约定) | 247 | ### 6.2 services 块结构(能力统一约定) |
docs/QUICKSTART.md
| @@ -348,7 +348,7 @@ saas-search 以 MySQL 中的店匠标准表为权威数据源: | @@ -348,7 +348,7 @@ saas-search 以 MySQL 中的店匠标准表为权威数据源: | ||
| 348 | 348 | ||
| 349 | - `field_boosts`:字段权重(统一按字段基名配置,运行时按 `.{lang}` 动态组装) | 349 | - `field_boosts`:字段权重(统一按字段基名配置,运行时按 `.{lang}` 动态组装) |
| 350 | - `query_config.search_fields`:动态多语言检索字段(multilingual/shared/core) | 350 | - `query_config.search_fields`:动态多语言检索字段(multilingual/shared/core) |
| 351 | -- `query_config.text_query_strategy`:文本召回策略参数(minimum_should_match、翻译boost等) | 351 | +- `query_config.text_query_strategy`:文本召回策略参数(minimum_should_match、翻译boost、翻译失败原文兜底boost等) |
| 352 | - `query_config`:语言、embedding 开关、source_fields、knn_boost、翻译提示词等 | 352 | - `query_config`:语言、embedding 开关、source_fields、knn_boost、翻译提示词等 |
| 353 | - `ranking.expression`:融合表达式(例如 `bm25() + 0.25*text_embedding_relevance()`) | 353 | - `ranking.expression`:融合表达式(例如 `bm25() + 0.25*text_embedding_relevance()`) |
| 354 | - `function_score`:ES 层加权函数 | 354 | - `function_score`:ES 层加权函数 |
docs/相关性检索优化说明.md
| 1 | -# 相关性检索优化说明 | 1 | +# 相关性检索优化说明(当前实现) |
| 2 | 2 | ||
| 3 | -## 概述 | 3 | +## 1. 文档目标 |
| 4 | 4 | ||
| 5 | -本次优化将相关性检索从简单的 `must` 子句中的 `multi_match` 查询,改为使用 `should` 子句的多查询策略,参考了成熟的搜索实现,显著提升了检索效果。 | 5 | +本文描述当前线上代码的文本检索策略,重点覆盖: |
| 6 | 6 | ||
| 7 | -## 主要改进 | 7 | +- 多语言检索路由(`detector` / `translator` / `indexed` 的关系) |
| 8 | +- 统一文本召回表达式(无布尔 AST 分支) | ||
| 9 | +- 翻译缺失时的兜底策略 | ||
| 10 | +- 典型场景下实际生成的 ES 查询结构 | ||
| 8 | 11 | ||
| 9 | -## 实现方式 | 12 | +> 说明:向量召回(KNN)是另一维度,本篇仅简要提及,不展开。 |
| 10 | 13 | ||
| 11 | -本次优化采用精简实现,直接在 `QueryParser` 中集成必要的分析功能,不新增独立模块。 | 14 | +## 2. 核心流程 |
| 12 | 15 | ||
| 13 | -### 1. 查询结构优化 | 16 | +查询链路(文本相关): |
| 14 | 17 | ||
| 15 | -**之前的结构**(效果差): | ||
| 16 | -```json | ||
| 17 | -{ | ||
| 18 | - "bool": { | ||
| 19 | - "must": [ | ||
| 20 | - { | ||
| 21 | - "multi_match": { | ||
| 22 | - "query": "戏水动物", | ||
| 23 | - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 24 | - "minimum_should_match": "67%", | ||
| 25 | - "tie_breaker": 0.9, | ||
| 26 | - "boost": 1, | ||
| 27 | - "_name": "base_query" | ||
| 28 | - } | ||
| 29 | - } | ||
| 30 | - ] | ||
| 31 | - } | ||
| 32 | -} | ||
| 33 | -``` | ||
| 34 | - | ||
| 35 | -**优化后的结构**(效果更好): | ||
| 36 | -```json | ||
| 37 | -{ | ||
| 38 | - "bool": { | ||
| 39 | - "should": [ | ||
| 40 | - { | ||
| 41 | - "multi_match": { | ||
| 42 | - "_name": "base_query", | ||
| 43 | - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 44 | - "minimum_should_match": "75%", | ||
| 45 | - "operator": "AND", | ||
| 46 | - "query": "戏水动物", | ||
| 47 | - "tie_breaker": 0.9 | ||
| 48 | - } | ||
| 49 | - }, | ||
| 50 | - { | ||
| 51 | - "multi_match": { | ||
| 52 | - "_name": "base_query_trans_en", | ||
| 53 | - "boost": 0.4, | ||
| 54 | - "fields": ["title.en^3.0", ...], | ||
| 55 | - "minimum_should_match": "75%", | ||
| 56 | - "operator": "AND", | ||
| 57 | - "query": "water sports", | ||
| 58 | - "tie_breaker": 0.9 | ||
| 59 | - } | ||
| 60 | - }, | ||
| 61 | - { | ||
| 62 | - "multi_match": { | ||
| 63 | - "query": "戏水动物", | ||
| 64 | - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 65 | - "type": "phrase", | ||
| 66 | - "slop": 2, | ||
| 67 | - "boost": 1.0, | ||
| 68 | - "_name": "phrase_query" | ||
| 69 | - } | ||
| 70 | - }, | ||
| 71 | - { | ||
| 72 | - "multi_match": { | ||
| 73 | - "query": "戏水 动物", | ||
| 74 | - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 75 | - "operator": "AND", | ||
| 76 | - "tie_breaker": 0.9, | ||
| 77 | - "boost": 0.1, | ||
| 78 | - "_name": "keywords_query" | ||
| 79 | - } | ||
| 80 | - } | ||
| 81 | - ], | ||
| 82 | - "minimum_should_match": 1 | ||
| 83 | - } | ||
| 84 | -} | ||
| 85 | -``` | 18 | +1. `QueryParser.parse()` |
| 19 | + 输出 `detected_language`、`query_text_by_lang`、`search_langs`、`index_languages`、`source_in_index_languages`。 | ||
| 20 | +2. `ESQueryBuilder._build_advanced_text_query()` | ||
| 21 | + 按 `search_langs` 动态拼接 `title/brief/description/vendor/category_*` 的 `.{lang}` 字段,叠加 shared 字段(`tags`、`option*_values`)。 | ||
| 22 | +3. `build_query()` | ||
| 23 | + 统一走文本策略,不再有布尔 AST 枝路。 | ||
| 86 | 24 | ||
| 87 | -### 2. 集成查询分析功能 | 25 | +## 3. 能力矩阵(Detector / Translator / Indexed) |
| 88 | 26 | ||
| 89 | -在 `QueryParser` 中直接集成必要的分析功能: | 27 | +三类能力的职责边界: |
| 90 | 28 | ||
| 91 | -- **关键词提取**:使用 HanLP 提取查询中的名词(长度>1),用于关键词查询(可选,HanLP 不可用时降级) | ||
| 92 | -- **查询类型判断**:区分短查询和长查询 | ||
| 93 | -- **Token 计数**:用于判断查询长度 | 29 | +- **Detector**:识别 query 源语言(`detected_language`) |
| 30 | +- **Indexed**:租户可检索语言集合(`tenant_config.*.index_languages`) | ||
| 31 | +- **Translator**:源语言到目标语言的可翻译能力及实时成功率 | ||
| 94 | 32 | ||
| 95 | -### 3. 多查询策略 | 33 | +### 3.1 决策规则 |
| 96 | 34 | ||
| 97 | -#### 3.1 基础查询(base_query) | ||
| 98 | -- 使用 `operator: "AND"` 确保所有词都必须匹配 | ||
| 99 | -- `minimum_should_match: "75%"` 提高匹配精度 | ||
| 100 | -- 使用 `tie_breaker: 0.9` 进行分数融合 | 35 | +1. 若 `detected_language in index_languages`: |
| 36 | + 源语言字段做主召回;其他语言走翻译补召回(低权重)。 | ||
| 37 | +2. 若 `detected_language not in index_languages`: | ||
| 38 | + 翻译到 `index_languages` 是主路径;源语言字段仅作弱召回。 | ||
| 39 | +3. 若第 2 步翻译部分失败或全部失败: | ||
| 40 | + 对缺失翻译的 `index_languages` 字段,追加“原文低权重兜底”子句,避免完全丢失这些语种索引面的召回机会。 | ||
| 101 | 41 | ||
| 102 | -#### 3.2 翻译查询(base_query_trans_zh/en) | ||
| 103 | -- 当查询语言不是中文/英文时,添加翻译查询 | ||
| 104 | -- 使用较低的 boost(0.4)避免过度影响 | ||
| 105 | -- 支持跨语言检索 | 42 | +### 3.2 翻译等待策略 |
| 106 | 43 | ||
| 107 | -#### 3.3 短语查询(phrase_query) | ||
| 108 | -- 针对短查询(token_count >= 2 且 is_short,由 query_tokens 推导) | ||
| 109 | -- 使用 `type: "phrase"` 进行精确短语匹配 | ||
| 110 | -- 支持 slop(允许词序调整) | 44 | +`QueryParser.parse()` 中: |
| 111 | 45 | ||
| 112 | -#### 3.4 关键词查询(keywords_query) | ||
| 113 | -- 使用 HanLP 提取的名词进行查询 | ||
| 114 | -- 仅在关键词长度合理时启用(避免关键词占查询比例过高) | ||
| 115 | -- 使用较低的 boost(0.1)作为补充 | 46 | +- 当源语种不在 `index_languages`:使用 `translate_multi_async(...)` 并等待 futures 收敛 |
| 47 | +- 当源语种在 `index_languages`:使用 `translate_multi(..., async_mode=True)`,优先缓存命中,未命中可后台补齐 | ||
| 116 | 48 | ||
| 117 | -#### 3.5 长查询优化(long_query) | ||
| 118 | -- 当前已禁用(参考实现中也是 False) | ||
| 119 | -- 未来可根据需要启用 | 49 | +这保证了“必须翻译才能检索”的场景不会直接空跑。 |
| 120 | 50 | ||
| 121 | -#### 3.6 KNN 向量召回自适应策略(query_tokens) | 51 | +## 4. 统一文本召回表达式 |
| 122 | 52 | ||
| 123 | -根据 `query_tokens`(HanLP 分词后的 token 数量)动态调整 KNN 的召回数量和权重: | 53 | +每个语言子句的基础形态: |
| 124 | 54 | ||
| 125 | -| 查询类型 | token_count | knn_k | num_candidates | boost 系数 | | ||
| 126 | -|---------|-------------|-------|----------------|------------| | ||
| 127 | -| 短查询 | ≤ 2 | 30 | 100 | 0.6× 默认 | | ||
| 128 | -| 中等查询| 3~4 | 50 | 200 | 1.0× 默认 | | ||
| 129 | -| 长查询 | ≥ 5 | 80 | 300 | 1.4× 默认 | | 55 | +```json |
| 56 | +{ | ||
| 57 | + "multi_match": { | ||
| 58 | + "_name": "base_query|base_query_trans_xx|fallback_original_query_xx", | ||
| 59 | + "query": "<text>", | ||
| 60 | + "fields": ["title.xx^3.0", "brief.xx^1.5", "...", "tags", "option1_values^0.5", "..."], | ||
| 61 | + "minimum_should_match": "75%", | ||
| 62 | + "tie_breaker": 0.9, | ||
| 63 | + "boost": "<按策略决定,可省略>" | ||
| 64 | + } | ||
| 65 | +} | ||
| 66 | +``` | ||
| 130 | 67 | ||
| 131 | -**策略说明**: | ||
| 132 | -- **短查询**:BM25 对精确匹配更有效,降低 KNN 召回和权重,避免语义召回干扰 | ||
| 133 | -- **长查询**:语义搜索更有利,提高 KNN 召回和权重,增强语义理解能力 | ||
| 134 | -- 默认 boost 由 `config.query_config.knn_boost` 配置(通常 0.25) | 68 | +最终按 `bool.should` 组合,`minimum_should_match: 1`。 |
| 135 | 69 | ||
| 136 | -### 4. 字段映射优化 | 70 | +## 5. 关键配置项(文本策略) |
| 137 | 71 | ||
| 138 | -新增 `_get_match_fields()` 方法,支持: | ||
| 139 | -- 根据语言动态获取匹配字段 | ||
| 140 | -- 区分全部字段(all_fields)和核心字段(core_fields) | ||
| 141 | -- 核心字段用于短语查询和关键词查询,提高精度 | 72 | +位于 `config/config.yaml -> query_config.text_query_strategy`: |
| 142 | 73 | ||
| 143 | -## 实现细节 | 74 | +- `base_minimum_should_match` |
| 75 | +- `translation_minimum_should_match` | ||
| 76 | +- `translation_boost` | ||
| 77 | +- `translation_boost_when_source_missing` | ||
| 78 | +- `source_boost_when_missing` | ||
| 79 | +- `original_query_fallback_boost_when_translation_missing`(新增) | ||
| 80 | +- `keywords_boost` | ||
| 81 | +- `enable_phrase_query` | ||
| 82 | +- `tie_breaker_base_query` | ||
| 83 | +- `tie_breaker_keywords` | ||
| 144 | 84 | ||
| 145 | -### 文件修改清单 | 85 | +新增项说明: |
| 146 | 86 | ||
| 147 | -1. **修改文件**: | ||
| 148 | - - `query/query_parser.py` - 添加关键词提取、查询类型判断等功能(HanLP 可选) | ||
| 149 | - - `search/es_query_builder.py` - 实现 should 子句的多查询策略 | ||
| 150 | - - `search/searcher.py` - 传递 parsed_query 给查询构建器 | 87 | +- `original_query_fallback_boost_when_translation_missing`: |
| 88 | + 当源语种不在索引语言且翻译缺失时,原文打到缺失目标语字段的低权重系数,默认 `0.2`。 | ||
| 151 | 89 | ||
| 152 | -### 关键参数说明 | 90 | +## 6. 典型场景与实际 DSL |
| 153 | 91 | ||
| 154 | -- **minimum_should_match**: 从 "67%" 提升到 "75%",提高匹配精度 | ||
| 155 | -- **operator**: 从默认改为 "AND",确保所有词都匹配 | ||
| 156 | -- **tie_breaker**: 保持 0.9,用于分数融合 | ||
| 157 | -- **boost 值**: | ||
| 158 | - - base_query: 1.0(默认) | ||
| 159 | - - translation queries: 0.4 | ||
| 160 | - - phrase_query: 1.0 | ||
| 161 | - - keywords_query: 0.1 | 92 | +以下示例来自当前 `ESQueryBuilder` 生成结果(已按当前代码验证)。 |
| 162 | 93 | ||
| 163 | -### 依赖要求 | 94 | +### 场景 A:源语种已在索引语言中,且翻译成功 |
| 164 | 95 | ||
| 165 | -- **HanLP**(可选):如果安装了 `hanlp` 包,会自动启用关键词提取功能 | ||
| 166 | - ```bash | ||
| 167 | - pip install hanlp | ||
| 168 | - ``` | ||
| 169 | - | ||
| 170 | - 如果未安装,系统会自动降级到简单分析(基于空格分词),不影响基本功能。 | 96 | +- `detected_language=de` |
| 97 | +- `index_languages=[de,en]` | ||
| 98 | +- `query_text_by_lang={de:"herren schuhe", en:"men shoes"}` | ||
| 171 | 99 | ||
| 172 | -- **HanLP 模型**:首次运行时会自动下载 | ||
| 173 | - - Tokenizer: `CTB9_TOK_ELECTRA_BASE_CRF` | ||
| 174 | - - POS Tagger: `CTB9_POS_ELECTRA_SMALL` | 100 | +策略结果: |
| 175 | 101 | ||
| 176 | -### 配置说明 | 102 | +- `base_query`:德语字段,正常权重 |
| 103 | +- `base_query_trans_en`:英语字段,`boost=translation_boost`(默认 0.4) | ||
| 177 | 104 | ||
| 178 | -- **忽略关键词**:在 `_extract_keywords()` 方法中配置 | ||
| 179 | - - 默认忽略:`['玩具']` | 105 | +### 场景 B:源语种不在索引语言中,部分翻译缺失 |
| 180 | 106 | ||
| 181 | -## 使用示例 | 107 | +- `detected_language=de` |
| 108 | +- `index_languages=[en,zh]` | ||
| 109 | +- 只翻译出 `en`,`zh` 失败 | ||
| 182 | 110 | ||
| 183 | -### 基本使用 | 111 | +策略结果: |
| 184 | 112 | ||
| 185 | -查询会自动使用优化后的策略,无需额外配置: | 113 | +- `base_query`(德语字段):`boost=source_boost_when_missing`(默认 0.6) |
| 114 | +- `base_query_trans_en`(英文字段):`boost=translation_boost_when_source_missing`(默认 1.0) | ||
| 115 | +- `fallback_original_query_zh`(中文字段):原文低权重兜底(默认 0.2) | ||
| 186 | 116 | ||
| 187 | -```python | ||
| 188 | -# 在 searcher.py 中,查询会自动使用优化策略 | ||
| 189 | -result = searcher.search( | ||
| 190 | - query="戏水动物", | ||
| 191 | - tenant_id="162", | ||
| 192 | - size=10 | ||
| 193 | -) | ||
| 194 | -``` | 117 | +### 场景 C:源语种不在索引语言中,翻译全部失败 |
| 195 | 118 | ||
| 196 | -### 查看分析结果 | 119 | +- `detected_language=de` |
| 120 | +- `index_languages=[en,zh]` | ||
| 121 | +- `query_text_by_lang` 仅有 `de` | ||
| 197 | 122 | ||
| 198 | -可以直接从 `parsed_query` 查看分析结果: | 123 | +策略结果: |
| 199 | 124 | ||
| 200 | -```python | ||
| 201 | -parsed_query = query_parser.parse("戏水动物") | ||
| 202 | -print(f"关键词: {parsed_query.keywords}") | ||
| 203 | -print(f"Token数: {parsed_query.token_count}") | ||
| 204 | -print(f"query_tokens: {parsed_query.query_tokens}") | ||
| 205 | -``` | 125 | +- `base_query`(德语字段,低权重) |
| 126 | +- `fallback_original_query_en`(英文字段原文兜底) | ||
| 127 | +- `fallback_original_query_zh`(中文字段原文兜底) | ||
| 206 | 128 | ||
| 207 | -## 性能考虑 | 129 | +这能避免“只有源语种字段查询,且该语种字段在商家索引中稀疏/为空”导致的弱召回问题。 |
| 208 | 130 | ||
| 209 | -1. **HanLP 初始化**:采用懒加载,首次使用时才初始化 | ||
| 210 | -2. **错误处理**:HanLP 初始化失败或未安装时,系统会降级到简单分析(基于空格分词),不影响服务 | ||
| 211 | -3. **代码精简**:所有功能直接集成在 `QueryParser` 中,无额外模块依赖 | 131 | +## 7. QueryParser 与 ESBuilder 的职责分工 |
| 212 | 132 | ||
| 213 | -## 后续优化方向 | 133 | +- `QueryParser` 负责“语言计划”与“可用文本”: |
| 134 | + - `search_langs` | ||
| 135 | + - `query_text_by_lang` | ||
| 136 | + - `source_in_index_languages` | ||
| 137 | + - `index_languages` | ||
| 138 | +- `ESQueryBuilder` 负责“表达式展开”: | ||
| 139 | + - 动态字段组装 | ||
| 140 | + - 子句权重分配 | ||
| 141 | + - 翻译缺失兜底子句拼接 | ||
| 214 | 142 | ||
| 215 | -1. **长查询优化**:可以启用长查询的特殊处理 | ||
| 216 | -2. **意图识别**:完善意图词典,提供更精准的意图识别 | ||
| 217 | -3. **参数调优**:根据实际效果调整 boost 值和 minimum_should_match | ||
| 218 | -4. **A/B 测试**:对比优化前后的检索效果 | 143 | +这种分层让策略调优主要落在配置和 Builder,不破坏 Parser 的职责边界。 |
| 219 | 144 | ||
| 220 | -## 注意事项 | 145 | +## 8. 兼容与注意事项 |
| 221 | 146 | ||
| 222 | -1. **HanLP 依赖**:HanLP 是可选的,如果未安装或初始化失败,系统会自动降级到简单分析,不会影响基本功能 | ||
| 223 | -2. **性能影响**:HanLP 分析会增加一定的处理时间,但采用懒加载机制 | ||
| 224 | -3. **字段匹配**:确保 ES 索引中存在对应的中英文字段 | ||
| 225 | -4. **代码精简**:所有功能都集成在现有模块中,保持代码结构简洁 | 147 | +1. 当前文本主链路已移除布尔 AST 分支。 |
| 148 | +2. 文档中的旧描述(如 `operator: AND` 固定开启)不再适用,当前实现未强制设置该参数。 | ||
| 149 | +3. `HanLP` 为可选依赖;不可用时退化到轻量分词,不影响主链路可用性。 | ||
| 150 | +4. 若后续扩展到更多语种,请确保: | ||
| 151 | + - mapping 中存在对应 `.<lang>` 字段 | ||
| 152 | + - `index_languages` 配置在支持列表内 | ||
| 153 | + - 翻译 provider 对目标语种可用 | ||
| 226 | 154 | ||
| 227 | -## 参考 | 155 | +## 9. 建议测试清单 |
| 228 | 156 | ||
| 229 | -- 参考实现中的查询构建逻辑 | ||
| 230 | -- HanLP 官方文档:https://hanlp.hankcs.com/ | ||
| 231 | -- Elasticsearch multi_match 查询文档 | 157 | +建议在 `tests/` 增加文本策略用例: |
| 232 | 158 | ||
| 159 | +1. 源语种在索引语言,翻译命中缓存 | ||
| 160 | +2. 源语种不在索引语言,翻译部分失败(验证 fallback 子句) | ||
| 161 | +3. 源语种不在索引语言,翻译全部失败(验证多目标 fallback) | ||
| 162 | +4. 自定义 `original_query_fallback_boost_when_translation_missing` 生效 | ||
| 163 | +5. 非 `zh/en` 语种字段动态拼接(如 `de/fr/es`) |
| @@ -0,0 +1,232 @@ | @@ -0,0 +1,232 @@ | ||
| 1 | +# 相关性检索优化说明 | ||
| 2 | + | ||
| 3 | +## 概述 | ||
| 4 | + | ||
| 5 | +本次优化将相关性检索从简单的 `must` 子句中的 `multi_match` 查询,改为使用 `should` 子句的多查询策略,参考了成熟的搜索实现,显著提升了检索效果。 | ||
| 6 | + | ||
| 7 | +## 主要改进 | ||
| 8 | + | ||
| 9 | +## 实现方式 | ||
| 10 | + | ||
| 11 | +本次优化采用精简实现,直接在 `QueryParser` 中集成必要的分析功能,不新增独立模块。 | ||
| 12 | + | ||
| 13 | +### 1. 查询结构优化 | ||
| 14 | + | ||
| 15 | +**之前的结构**(效果差): | ||
| 16 | +```json | ||
| 17 | +{ | ||
| 18 | + "bool": { | ||
| 19 | + "must": [ | ||
| 20 | + { | ||
| 21 | + "multi_match": { | ||
| 22 | + "query": "戏水动物", | ||
| 23 | + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 24 | + "minimum_should_match": "67%", | ||
| 25 | + "tie_breaker": 0.9, | ||
| 26 | + "boost": 1, | ||
| 27 | + "_name": "base_query" | ||
| 28 | + } | ||
| 29 | + } | ||
| 30 | + ] | ||
| 31 | + } | ||
| 32 | +} | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +**优化后的结构**(效果更好): | ||
| 36 | +```json | ||
| 37 | +{ | ||
| 38 | + "bool": { | ||
| 39 | + "should": [ | ||
| 40 | + { | ||
| 41 | + "multi_match": { | ||
| 42 | + "_name": "base_query", | ||
| 43 | + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 44 | + "minimum_should_match": "75%", | ||
| 45 | + "operator": "AND", | ||
| 46 | + "query": "戏水动物", | ||
| 47 | + "tie_breaker": 0.9 | ||
| 48 | + } | ||
| 49 | + }, | ||
| 50 | + { | ||
| 51 | + "multi_match": { | ||
| 52 | + "_name": "base_query_trans_en", | ||
| 53 | + "boost": 0.4, | ||
| 54 | + "fields": ["title.en^3.0", ...], | ||
| 55 | + "minimum_should_match": "75%", | ||
| 56 | + "operator": "AND", | ||
| 57 | + "query": "water sports", | ||
| 58 | + "tie_breaker": 0.9 | ||
| 59 | + } | ||
| 60 | + }, | ||
| 61 | + { | ||
| 62 | + "multi_match": { | ||
| 63 | + "query": "戏水动物", | ||
| 64 | + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 65 | + "type": "phrase", | ||
| 66 | + "slop": 2, | ||
| 67 | + "boost": 1.0, | ||
| 68 | + "_name": "phrase_query" | ||
| 69 | + } | ||
| 70 | + }, | ||
| 71 | + { | ||
| 72 | + "multi_match": { | ||
| 73 | + "query": "戏水 动物", | ||
| 74 | + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], | ||
| 75 | + "operator": "AND", | ||
| 76 | + "tie_breaker": 0.9, | ||
| 77 | + "boost": 0.1, | ||
| 78 | + "_name": "keywords_query" | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + ], | ||
| 82 | + "minimum_should_match": 1 | ||
| 83 | + } | ||
| 84 | +} | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +### 2. 集成查询分析功能 | ||
| 88 | + | ||
| 89 | +在 `QueryParser` 中直接集成必要的分析功能: | ||
| 90 | + | ||
| 91 | +- **关键词提取**:使用 HanLP 提取查询中的名词(长度>1),用于关键词查询(可选,HanLP 不可用时降级) | ||
| 92 | +- **查询类型判断**:区分短查询和长查询 | ||
| 93 | +- **Token 计数**:用于判断查询长度 | ||
| 94 | + | ||
| 95 | +### 3. 多查询策略 | ||
| 96 | + | ||
| 97 | +#### 3.1 基础查询(base_query) | ||
| 98 | +- 使用 `operator: "AND"` 确保所有词都必须匹配 | ||
| 99 | +- `minimum_should_match: "75%"` 提高匹配精度 | ||
| 100 | +- 使用 `tie_breaker: 0.9` 进行分数融合 | ||
| 101 | + | ||
| 102 | +#### 3.2 翻译查询(base_query_trans_zh/en) | ||
| 103 | +- 当查询语言不是中文/英文时,添加翻译查询 | ||
| 104 | +- 使用较低的 boost(0.4)避免过度影响 | ||
| 105 | +- 支持跨语言检索 | ||
| 106 | + | ||
| 107 | +#### 3.3 短语查询(phrase_query) | ||
| 108 | +- 针对短查询(token_count >= 2 且 is_short,由 query_tokens 推导) | ||
| 109 | +- 使用 `type: "phrase"` 进行精确短语匹配 | ||
| 110 | +- 支持 slop(允许词序调整) | ||
| 111 | + | ||
| 112 | +#### 3.4 关键词查询(keywords_query) | ||
| 113 | +- 使用 HanLP 提取的名词进行查询 | ||
| 114 | +- 仅在关键词长度合理时启用(避免关键词占查询比例过高) | ||
| 115 | +- 使用较低的 boost(0.1)作为补充 | ||
| 116 | + | ||
| 117 | +#### 3.5 长查询优化(long_query) | ||
| 118 | +- 当前已禁用(参考实现中也是 False) | ||
| 119 | +- 未来可根据需要启用 | ||
| 120 | + | ||
| 121 | +#### 3.6 KNN 向量召回自适应策略(query_tokens) | ||
| 122 | + | ||
| 123 | +根据 `query_tokens`(HanLP 分词后的 token 数量)动态调整 KNN 的召回数量和权重: | ||
| 124 | + | ||
| 125 | +| 查询类型 | token_count | knn_k | num_candidates | boost 系数 | | ||
| 126 | +|---------|-------------|-------|----------------|------------| | ||
| 127 | +| 短查询 | ≤ 2 | 30 | 100 | 0.6× 默认 | | ||
| 128 | +| 中等查询| 3~4 | 50 | 200 | 1.0× 默认 | | ||
| 129 | +| 长查询 | ≥ 5 | 80 | 300 | 1.4× 默认 | | ||
| 130 | + | ||
| 131 | +**策略说明**: | ||
| 132 | +- **短查询**:BM25 对精确匹配更有效,降低 KNN 召回和权重,避免语义召回干扰 | ||
| 133 | +- **长查询**:语义搜索更有利,提高 KNN 召回和权重,增强语义理解能力 | ||
| 134 | +- 默认 boost 由 `config.query_config.knn_boost` 配置(通常 0.25) | ||
| 135 | + | ||
| 136 | +### 4. 字段映射优化 | ||
| 137 | + | ||
| 138 | +新增 `_get_match_fields()` 方法,支持: | ||
| 139 | +- 根据语言动态获取匹配字段 | ||
| 140 | +- 区分全部字段(all_fields)和核心字段(core_fields) | ||
| 141 | +- 核心字段用于短语查询和关键词查询,提高精度 | ||
| 142 | + | ||
| 143 | +## 实现细节 | ||
| 144 | + | ||
| 145 | +### 文件修改清单 | ||
| 146 | + | ||
| 147 | +1. **修改文件**: | ||
| 148 | + - `query/query_parser.py` - 添加关键词提取、查询类型判断等功能(HanLP 可选) | ||
| 149 | + - `search/es_query_builder.py` - 实现 should 子句的多查询策略 | ||
| 150 | + - `search/searcher.py` - 传递 parsed_query 给查询构建器 | ||
| 151 | + | ||
| 152 | +### 关键参数说明 | ||
| 153 | + | ||
| 154 | +- **minimum_should_match**: 从 "67%" 提升到 "75%",提高匹配精度 | ||
| 155 | +- **operator**: 从默认改为 "AND",确保所有词都匹配 | ||
| 156 | +- **tie_breaker**: 保持 0.9,用于分数融合 | ||
| 157 | +- **boost 值**: | ||
| 158 | + - base_query: 1.0(默认) | ||
| 159 | + - translation queries: 0.4 | ||
| 160 | + - phrase_query: 1.0 | ||
| 161 | + - keywords_query: 0.1 | ||
| 162 | + | ||
| 163 | +### 依赖要求 | ||
| 164 | + | ||
| 165 | +- **HanLP**(可选):如果安装了 `hanlp` 包,会自动启用关键词提取功能 | ||
| 166 | + ```bash | ||
| 167 | + pip install hanlp | ||
| 168 | + ``` | ||
| 169 | + | ||
| 170 | + 如果未安装,系统会自动降级到简单分析(基于空格分词),不影响基本功能。 | ||
| 171 | + | ||
| 172 | +- **HanLP 模型**:首次运行时会自动下载 | ||
| 173 | + - Tokenizer: `CTB9_TOK_ELECTRA_BASE_CRF` | ||
| 174 | + - POS Tagger: `CTB9_POS_ELECTRA_SMALL` | ||
| 175 | + | ||
| 176 | +### 配置说明 | ||
| 177 | + | ||
| 178 | +- **忽略关键词**:在 `_extract_keywords()` 方法中配置 | ||
| 179 | + - 默认忽略:`['玩具']` | ||
| 180 | + | ||
| 181 | +## 使用示例 | ||
| 182 | + | ||
| 183 | +### 基本使用 | ||
| 184 | + | ||
| 185 | +查询会自动使用优化后的策略,无需额外配置: | ||
| 186 | + | ||
| 187 | +```python | ||
| 188 | +# 在 searcher.py 中,查询会自动使用优化策略 | ||
| 189 | +result = searcher.search( | ||
| 190 | + query="戏水动物", | ||
| 191 | + tenant_id="162", | ||
| 192 | + size=10 | ||
| 193 | +) | ||
| 194 | +``` | ||
| 195 | + | ||
| 196 | +### 查看分析结果 | ||
| 197 | + | ||
| 198 | +可以直接从 `parsed_query` 查看分析结果: | ||
| 199 | + | ||
| 200 | +```python | ||
| 201 | +parsed_query = query_parser.parse("戏水动物") | ||
| 202 | +print(f"关键词: {parsed_query.keywords}") | ||
| 203 | +print(f"Token数: {parsed_query.token_count}") | ||
| 204 | +print(f"query_tokens: {parsed_query.query_tokens}") | ||
| 205 | +``` | ||
| 206 | + | ||
| 207 | +## 性能考虑 | ||
| 208 | + | ||
| 209 | +1. **HanLP 初始化**:采用懒加载,首次使用时才初始化 | ||
| 210 | +2. **错误处理**:HanLP 初始化失败或未安装时,系统会降级到简单分析(基于空格分词),不影响服务 | ||
| 211 | +3. **代码精简**:所有功能直接集成在 `QueryParser` 中,无额外模块依赖 | ||
| 212 | + | ||
| 213 | +## 后续优化方向 | ||
| 214 | + | ||
| 215 | +1. **长查询优化**:可以启用长查询的特殊处理 | ||
| 216 | +2. **意图识别**:完善意图词典,提供更精准的意图识别 | ||
| 217 | +3. **参数调优**:根据实际效果调整 boost 值和 minimum_should_match | ||
| 218 | +4. **A/B 测试**:对比优化前后的检索效果 | ||
| 219 | + | ||
| 220 | +## 注意事项 | ||
| 221 | + | ||
| 222 | +1. **HanLP 依赖**:HanLP 是可选的,如果未安装或初始化失败,系统会自动降级到简单分析,不会影响基本功能 | ||
| 223 | +2. **性能影响**:HanLP 分析会增加一定的处理时间,但采用懒加载机制 | ||
| 224 | +3. **字段匹配**:确保 ES 索引中存在对应的中英文字段 | ||
| 225 | +4. **代码精简**:所有功能都集成在现有模块中,保持代码结构简洁 | ||
| 226 | + | ||
| 227 | +## 参考 | ||
| 228 | + | ||
| 229 | +- 参考实现中的查询构建逻辑 | ||
| 230 | +- HanLP 官方文档:https://hanlp.hankcs.com/ | ||
| 231 | +- Elasticsearch multi_match 查询文档 | ||
| 232 | + |
search/es_query_builder.py
| @@ -34,6 +34,7 @@ class ESQueryBuilder: | @@ -34,6 +34,7 @@ class ESQueryBuilder: | ||
| 34 | translation_boost: float = 0.4, | 34 | translation_boost: float = 0.4, |
| 35 | translation_boost_when_source_missing: float = 1.0, | 35 | translation_boost_when_source_missing: float = 1.0, |
| 36 | source_boost_when_missing: float = 0.6, | 36 | source_boost_when_missing: float = 0.6, |
| 37 | + original_query_fallback_boost_when_translation_missing: float = 0.2, | ||
| 37 | keywords_boost: float = 0.1, | 38 | keywords_boost: float = 0.1, |
| 38 | enable_phrase_query: bool = True, | 39 | enable_phrase_query: bool = True, |
| 39 | tie_breaker_base_query: float = 0.9, | 40 | tie_breaker_base_query: float = 0.9, |
| @@ -72,6 +73,9 @@ class ESQueryBuilder: | @@ -72,6 +73,9 @@ class ESQueryBuilder: | ||
| 72 | self.translation_boost = float(translation_boost) | 73 | self.translation_boost = float(translation_boost) |
| 73 | self.translation_boost_when_source_missing = float(translation_boost_when_source_missing) | 74 | self.translation_boost_when_source_missing = float(translation_boost_when_source_missing) |
| 74 | self.source_boost_when_missing = float(source_boost_when_missing) | 75 | self.source_boost_when_missing = float(source_boost_when_missing) |
| 76 | + self.original_query_fallback_boost_when_translation_missing = float( | ||
| 77 | + original_query_fallback_boost_when_translation_missing | ||
| 78 | + ) | ||
| 75 | self.keywords_boost = float(keywords_boost) | 79 | self.keywords_boost = float(keywords_boost) |
| 76 | self.enable_phrase_query = bool(enable_phrase_query) | 80 | self.enable_phrase_query = bool(enable_phrase_query) |
| 77 | self.tie_breaker_base_query = float(tie_breaker_base_query) | 81 | self.tie_breaker_base_query = float(tie_breaker_base_query) |
| @@ -476,6 +480,7 @@ class ESQueryBuilder: | @@ -476,6 +480,7 @@ class ESQueryBuilder: | ||
| 476 | search_langs: List[str] = [] | 480 | search_langs: List[str] = [] |
| 477 | source_lang = self.default_language | 481 | source_lang = self.default_language |
| 478 | source_in_index_languages = True | 482 | source_in_index_languages = True |
| 483 | + index_languages: List[str] = [] | ||
| 479 | keywords = "" | 484 | keywords = "" |
| 480 | query_tokens = [] | 485 | query_tokens = [] |
| 481 | token_count = 0 | 486 | token_count = 0 |
| @@ -488,6 +493,7 @@ class ESQueryBuilder: | @@ -488,6 +493,7 @@ class ESQueryBuilder: | ||
| 488 | source_in_index_languages = bool( | 493 | source_in_index_languages = bool( |
| 489 | getattr(parsed_query, "source_in_index_languages", True) | 494 | getattr(parsed_query, "source_in_index_languages", True) |
| 490 | ) | 495 | ) |
| 496 | + index_languages = getattr(parsed_query, "index_languages", None) or [] | ||
| 491 | keywords = getattr(parsed_query, 'keywords', '') or "" | 497 | keywords = getattr(parsed_query, 'keywords', '') or "" |
| 492 | query_tokens = getattr(parsed_query, 'query_tokens', None) or [] | 498 | query_tokens = getattr(parsed_query, 'query_tokens', None) or [] |
| 493 | token_count = len(query_tokens) or getattr(parsed_query, 'token_count', 0) or 0 | 499 | token_count = len(query_tokens) or getattr(parsed_query, 'token_count', 0) or 0 |
| @@ -543,6 +549,37 @@ class ESQueryBuilder: | @@ -543,6 +549,37 @@ class ESQueryBuilder: | ||
| 543 | "multi_match": clause["multi_match"] | 549 | "multi_match": clause["multi_match"] |
| 544 | }) | 550 | }) |
| 545 | 551 | ||
| 552 | + # Fallback: source language is not indexed and translation for some index languages is missing. | ||
| 553 | + # Use original query text on missing index-language fields with a low boost. | ||
| 554 | + if not source_in_index_languages and query_text and index_languages: | ||
| 555 | + normalized_index_langs: List[str] = [] | ||
| 556 | + seen_langs = set() | ||
| 557 | + for lang in index_languages: | ||
| 558 | + norm_lang = str(lang or "").strip().lower() | ||
| 559 | + if not norm_lang or norm_lang in seen_langs: | ||
| 560 | + continue | ||
| 561 | + seen_langs.add(norm_lang) | ||
| 562 | + normalized_index_langs.append(norm_lang) | ||
| 563 | + | ||
| 564 | + for lang in normalized_index_langs: | ||
| 565 | + if lang == source_lang: | ||
| 566 | + continue | ||
| 567 | + if lang in query_text_by_lang: | ||
| 568 | + continue | ||
| 569 | + match_fields, _ = self._get_match_fields(lang) | ||
| 570 | + if not match_fields: | ||
| 571 | + continue | ||
| 572 | + should_clauses.append({ | ||
| 573 | + "multi_match": { | ||
| 574 | + "_name": f"fallback_original_query_{lang}", | ||
| 575 | + "query": query_text, | ||
| 576 | + "fields": match_fields, | ||
| 577 | + "minimum_should_match": self.translation_minimum_should_match, | ||
| 578 | + "tie_breaker": self.tie_breaker_base_query, | ||
| 579 | + "boost": self.original_query_fallback_boost_when_translation_missing, | ||
| 580 | + } | ||
| 581 | + }) | ||
| 582 | + | ||
| 546 | # 3. Short query - add phrase query (derived from query_tokens) | 583 | # 3. Short query - add phrase query (derived from query_tokens) |
| 547 | # is_short: quoted or ((token_count <= 2 or len <= 4) and no space) | 584 | # is_short: quoted or ((token_count <= 2 or len <= 4) and no space) |
| 548 | source_query_text = query_text_by_lang.get(source_lang) or query_text | 585 | source_query_text = query_text_by_lang.get(source_lang) or query_text |
search/searcher.py
| @@ -122,6 +122,9 @@ class Searcher: | @@ -122,6 +122,9 @@ class Searcher: | ||
| 122 | translation_boost=self.config.query_config.translation_boost, | 122 | translation_boost=self.config.query_config.translation_boost, |
| 123 | translation_boost_when_source_missing=self.config.query_config.translation_boost_when_source_missing, | 123 | translation_boost_when_source_missing=self.config.query_config.translation_boost_when_source_missing, |
| 124 | source_boost_when_missing=self.config.query_config.source_boost_when_missing, | 124 | source_boost_when_missing=self.config.query_config.source_boost_when_missing, |
| 125 | + original_query_fallback_boost_when_translation_missing=( | ||
| 126 | + self.config.query_config.original_query_fallback_boost_when_translation_missing | ||
| 127 | + ), | ||
| 125 | keywords_boost=self.config.query_config.keywords_boost, | 128 | keywords_boost=self.config.query_config.keywords_boost, |
| 126 | enable_phrase_query=self.config.query_config.enable_phrase_query, | 129 | enable_phrase_query=self.config.query_config.enable_phrase_query, |
| 127 | tie_breaker_base_query=self.config.query_config.tie_breaker_base_query, | 130 | tie_breaker_base_query=self.config.query_config.tie_breaker_base_query, |