Commit bcada818cdec318c60cea1cacdc1ea4cb44cd2e4

Authored by tangwang
1 parent bd96cead

last

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`)
docs/相关性检索优化说明_old1.md 0 → 100644
@@ -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,