Commit bcada818cdec318c60cea1cacdc1ea4cb44cd2e4

Authored by tangwang
1 parent bd96cead

last

config/config_loader.py
... ... @@ -47,11 +47,7 @@ class QueryConfig:
47 47 # Embedding field names
48 48 text_embedding_field: Optional[str] = "title_embedding"
49 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 51 # Source fields configuration
56 52 source_fields: Optional[List[str]] = None
57 53  
... ... @@ -75,6 +71,7 @@ class QueryConfig:
75 71 translation_boost: float = 0.4
76 72 translation_boost_when_source_missing: float = 1.0
77 73 source_boost_when_missing: float = 0.6
  74 + original_query_fallback_boost_when_translation_missing: float = 0.2
78 75 keywords_boost: float = 0.1
79 76 enable_phrase_query: bool = True
80 77 tie_breaker_base_query: float = 0.9
... ... @@ -249,7 +246,6 @@ class ConfigLoader:
249 246 query_config_data = config_data.get("query_config", {})
250 247 services_data = config_data.get("services", {}) if isinstance(config_data.get("services", {}), dict) else {}
251 248 rewrite_dictionary = self._load_rewrite_dictionary()
252   - embedding_thresholds = query_config_data.get("embedding_disable_thresholds", {})
253 249 search_fields_cfg = query_config_data.get("search_fields", {})
254 250 text_strategy_cfg = query_config_data.get("text_query_strategy", {})
255 251  
... ... @@ -266,8 +262,6 @@ class ConfigLoader:
266 262 translation_prompts=query_config_data.get("translation_prompts", {}),
267 263 text_embedding_field=query_config_data.get("text_embedding_field"),
268 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 265 source_fields=query_config_data.get("source_fields"),
272 266 knn_boost=query_config_data.get("knn_boost", 0.25),
273 267 multilingual_fields=search_fields_cfg.get(
... ... @@ -289,6 +283,9 @@ class ConfigLoader:
289 283 text_strategy_cfg.get("translation_boost_when_source_missing", 1.0)
290 284 ),
291 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 289 keywords_boost=float(text_strategy_cfg.get("keywords_boost", 0.1)),
293 290 enable_phrase_query=bool(text_strategy_cfg.get("enable_phrase_query", True)),
294 291 tie_breaker_base_query=float(text_strategy_cfg.get("tie_breaker_base_query", 0.9)),
... ... @@ -433,6 +430,7 @@ class ConfigLoader:
433 430 "translation_boost",
434 431 "translation_boost_when_source_missing",
435 432 "source_boost_when_missing",
  433 + "original_query_fallback_boost_when_translation_missing",
436 434 "keywords_boost",
437 435 "tie_breaker_base_query",
438 436 "tie_breaker_keywords",
... ... @@ -482,10 +480,6 @@ class ConfigLoader:
482 480 "translation_service": config.query_config.translation_service,
483 481 "text_embedding_field": config.query_config.text_embedding_field,
484 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 483 "source_fields": config.query_config.source_fields,
490 484 "search_fields": {
491 485 "multilingual_fields": config.query_config.multilingual_fields,
... ... @@ -498,6 +492,9 @@ class ConfigLoader:
498 492 "translation_boost": config.query_config.translation_boost,
499 493 "translation_boost_when_source_missing": config.query_config.translation_boost_when_source_missing,
500 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 498 "keywords_boost": config.query_config.keywords_boost,
502 499 "enable_phrase_query": config.query_config.enable_phrase_query,
503 500 "tie_breaker_base_query": config.query_config.tie_breaker_base_query,
... ...
docs/DEVELOPER_GUIDE.md
... ... @@ -241,7 +241,7 @@ docs/ # 文档(含本指南)
241 241  
242 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 245 - **.env**:敏感信息与部署态变量(DB、ES、Redis、API Key、端口等);不提交敏感值,可提供 `.env.example` 模板。
246 246  
247 247 ### 6.2 services 块结构(能力统一约定)
... ...
docs/QUICKSTART.md
... ... @@ -348,7 +348,7 @@ saas-search 以 MySQL 中的店匠标准表为权威数据源:
348 348  
349 349 - `field_boosts`:字段权重(统一按字段基名配置,运行时按 `.{lang}` 动态组装)
350 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 352 - `query_config`:语言、embedding 开关、source_fields、knn_boost、翻译提示词等
353 353 - `ranking.expression`:融合表达式(例如 `bm25() + 0.25*text_embedding_relevance()`)
354 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`)
... ...
docs/相关性检索优化说明_old1.md 0 → 100644
... ... @@ -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 34 translation_boost: float = 0.4,
35 35 translation_boost_when_source_missing: float = 1.0,
36 36 source_boost_when_missing: float = 0.6,
  37 + original_query_fallback_boost_when_translation_missing: float = 0.2,
37 38 keywords_boost: float = 0.1,
38 39 enable_phrase_query: bool = True,
39 40 tie_breaker_base_query: float = 0.9,
... ... @@ -72,6 +73,9 @@ class ESQueryBuilder:
72 73 self.translation_boost = float(translation_boost)
73 74 self.translation_boost_when_source_missing = float(translation_boost_when_source_missing)
74 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 79 self.keywords_boost = float(keywords_boost)
76 80 self.enable_phrase_query = bool(enable_phrase_query)
77 81 self.tie_breaker_base_query = float(tie_breaker_base_query)
... ... @@ -476,6 +480,7 @@ class ESQueryBuilder:
476 480 search_langs: List[str] = []
477 481 source_lang = self.default_language
478 482 source_in_index_languages = True
  483 + index_languages: List[str] = []
479 484 keywords = ""
480 485 query_tokens = []
481 486 token_count = 0
... ... @@ -488,6 +493,7 @@ class ESQueryBuilder:
488 493 source_in_index_languages = bool(
489 494 getattr(parsed_query, "source_in_index_languages", True)
490 495 )
  496 + index_languages = getattr(parsed_query, "index_languages", None) or []
491 497 keywords = getattr(parsed_query, 'keywords', '') or ""
492 498 query_tokens = getattr(parsed_query, 'query_tokens', None) or []
493 499 token_count = len(query_tokens) or getattr(parsed_query, 'token_count', 0) or 0
... ... @@ -543,6 +549,37 @@ class ESQueryBuilder:
543 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 583 # 3. Short query - add phrase query (derived from query_tokens)
547 584 # is_short: quoted or ((token_count <= 2 or len <= 4) and no space)
548 585 source_query_text = query_text_by_lang.get(source_lang) or query_text
... ...
search/searcher.py
... ... @@ -122,6 +122,9 @@ class Searcher:
122 122 translation_boost=self.config.query_config.translation_boost,
123 123 translation_boost_when_source_missing=self.config.query_config.translation_boost_when_source_missing,
124 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 128 keywords_boost=self.config.query_config.keywords_boost,
126 129 enable_phrase_query=self.config.query_config.enable_phrase_query,
127 130 tie_breaker_base_query=self.config.query_config.tie_breaker_base_query,
... ...