From bcada818cdec318c60cea1cacdc1ea4cb44cd2e4 Mon Sep 17 00:00:00 2001 From: tangwang Date: Tue, 10 Mar 2026 16:17:18 +0800 Subject: [PATCH] last --- config/config_loader.py | 21 +++++++++------------ docs/DEVELOPER_GUIDE.md | 2 +- docs/QUICKSTART.md | 2 +- docs/相关性检索优化说明.md | 299 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/相关性检索优化说明_old1.md | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ search/es_query_builder.py | 37 +++++++++++++++++++++++++++++++++++++ search/searcher.py | 3 +++ 7 files changed, 398 insertions(+), 198 deletions(-) create mode 100644 docs/相关性检索优化说明_old1.md diff --git a/config/config_loader.py b/config/config_loader.py index fbd5aad..df34e32 100644 --- a/config/config_loader.py +++ b/config/config_loader.py @@ -47,11 +47,7 @@ class QueryConfig: # Embedding field names text_embedding_field: Optional[str] = "title_embedding" image_embedding_field: Optional[str] = None - - # Embedding disable thresholds (disable vector search for short queries) - embedding_disable_chinese_char_limit: int = 4 - embedding_disable_english_word_limit: int = 3 - + # Source fields configuration source_fields: Optional[List[str]] = None @@ -75,6 +71,7 @@ class QueryConfig: translation_boost: float = 0.4 translation_boost_when_source_missing: float = 1.0 source_boost_when_missing: float = 0.6 + original_query_fallback_boost_when_translation_missing: float = 0.2 keywords_boost: float = 0.1 enable_phrase_query: bool = True tie_breaker_base_query: float = 0.9 @@ -249,7 +246,6 @@ class ConfigLoader: query_config_data = config_data.get("query_config", {}) services_data = config_data.get("services", {}) if isinstance(config_data.get("services", {}), dict) else {} rewrite_dictionary = self._load_rewrite_dictionary() - embedding_thresholds = query_config_data.get("embedding_disable_thresholds", {}) search_fields_cfg = query_config_data.get("search_fields", {}) text_strategy_cfg = query_config_data.get("text_query_strategy", {}) @@ -266,8 +262,6 @@ class ConfigLoader: translation_prompts=query_config_data.get("translation_prompts", {}), text_embedding_field=query_config_data.get("text_embedding_field"), image_embedding_field=query_config_data.get("image_embedding_field"), - embedding_disable_chinese_char_limit=embedding_thresholds.get("chinese_char_limit", 4), - embedding_disable_english_word_limit=embedding_thresholds.get("english_word_limit", 3), source_fields=query_config_data.get("source_fields"), knn_boost=query_config_data.get("knn_boost", 0.25), multilingual_fields=search_fields_cfg.get( @@ -289,6 +283,9 @@ class ConfigLoader: text_strategy_cfg.get("translation_boost_when_source_missing", 1.0) ), source_boost_when_missing=float(text_strategy_cfg.get("source_boost_when_missing", 0.6)), + original_query_fallback_boost_when_translation_missing=float( + text_strategy_cfg.get("original_query_fallback_boost_when_translation_missing", 0.2) + ), keywords_boost=float(text_strategy_cfg.get("keywords_boost", 0.1)), enable_phrase_query=bool(text_strategy_cfg.get("enable_phrase_query", True)), tie_breaker_base_query=float(text_strategy_cfg.get("tie_breaker_base_query", 0.9)), @@ -433,6 +430,7 @@ class ConfigLoader: "translation_boost", "translation_boost_when_source_missing", "source_boost_when_missing", + "original_query_fallback_boost_when_translation_missing", "keywords_boost", "tie_breaker_base_query", "tie_breaker_keywords", @@ -482,10 +480,6 @@ class ConfigLoader: "translation_service": config.query_config.translation_service, "text_embedding_field": config.query_config.text_embedding_field, "image_embedding_field": config.query_config.image_embedding_field, - "embedding_disable_thresholds": { - "chinese_char_limit": config.query_config.embedding_disable_chinese_char_limit, - "english_word_limit": config.query_config.embedding_disable_english_word_limit - }, "source_fields": config.query_config.source_fields, "search_fields": { "multilingual_fields": config.query_config.multilingual_fields, @@ -498,6 +492,9 @@ class ConfigLoader: "translation_boost": config.query_config.translation_boost, "translation_boost_when_source_missing": config.query_config.translation_boost_when_source_missing, "source_boost_when_missing": config.query_config.source_boost_when_missing, + "original_query_fallback_boost_when_translation_missing": ( + config.query_config.original_query_fallback_boost_when_translation_missing + ), "keywords_boost": config.query_config.keywords_boost, "enable_phrase_query": config.query_config.enable_phrase_query, "tie_breaker_base_query": config.query_config.tie_breaker_base_query, diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 55df4e5..96fad44 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -241,7 +241,7 @@ docs/ # 文档(含本指南) ### 6.1 主配置文件 -- **config/config.yaml**:搜索行为(field_boosts、query_config.search_fields、query_config.text_query_strategy、ranking、function_score、rerank 融合参数)、SPU 配置、**services**(翻译/向量/重排的 provider 与 backends)、tenant_config 等。 +- **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 等。 - **.env**:敏感信息与部署态变量(DB、ES、Redis、API Key、端口等);不提交敏感值,可提供 `.env.example` 模板。 ### 6.2 services 块结构(能力统一约定) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index a3aa0d3..9d8c0b1 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -348,7 +348,7 @@ saas-search 以 MySQL 中的店匠标准表为权威数据源: - `field_boosts`:字段权重(统一按字段基名配置,运行时按 `.{lang}` 动态组装) - `query_config.search_fields`:动态多语言检索字段(multilingual/shared/core) -- `query_config.text_query_strategy`:文本召回策略参数(minimum_should_match、翻译boost等) +- `query_config.text_query_strategy`:文本召回策略参数(minimum_should_match、翻译boost、翻译失败原文兜底boost等) - `query_config`:语言、embedding 开关、source_fields、knn_boost、翻译提示词等 - `ranking.expression`:融合表达式(例如 `bm25() + 0.25*text_embedding_relevance()`) - `function_score`:ES 层加权函数 diff --git a/docs/相关性检索优化说明.md b/docs/相关性检索优化说明.md index 75c94b3..2369575 100644 --- a/docs/相关性检索优化说明.md +++ b/docs/相关性检索优化说明.md @@ -1,232 +1,163 @@ -# 相关性检索优化说明 +# 相关性检索优化说明(当前实现) -## 概述 +## 1. 文档目标 -本次优化将相关性检索从简单的 `must` 子句中的 `multi_match` 查询,改为使用 `should` 子句的多查询策略,参考了成熟的搜索实现,显著提升了检索效果。 +本文描述当前线上代码的文本检索策略,重点覆盖: -## 主要改进 +- 多语言检索路由(`detector` / `translator` / `indexed` 的关系) +- 统一文本召回表达式(无布尔 AST 分支) +- 翻译缺失时的兜底策略 +- 典型场景下实际生成的 ES 查询结构 -## 实现方式 +> 说明:向量召回(KNN)是另一维度,本篇仅简要提及,不展开。 -本次优化采用精简实现,直接在 `QueryParser` 中集成必要的分析功能,不新增独立模块。 +## 2. 核心流程 -### 1. 查询结构优化 +查询链路(文本相关): -**之前的结构**(效果差): -```json -{ - "bool": { - "must": [ - { - "multi_match": { - "query": "戏水动物", - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], - "minimum_should_match": "67%", - "tie_breaker": 0.9, - "boost": 1, - "_name": "base_query" - } - } - ] - } -} -``` - -**优化后的结构**(效果更好): -```json -{ - "bool": { - "should": [ - { - "multi_match": { - "_name": "base_query", - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], - "minimum_should_match": "75%", - "operator": "AND", - "query": "戏水动物", - "tie_breaker": 0.9 - } - }, - { - "multi_match": { - "_name": "base_query_trans_en", - "boost": 0.4, - "fields": ["title.en^3.0", ...], - "minimum_should_match": "75%", - "operator": "AND", - "query": "water sports", - "tie_breaker": 0.9 - } - }, - { - "multi_match": { - "query": "戏水动物", - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], - "type": "phrase", - "slop": 2, - "boost": 1.0, - "_name": "phrase_query" - } - }, - { - "multi_match": { - "query": "戏水 动物", - "fields": ["title.zh^3.0", "brief.zh^1.5", ...], - "operator": "AND", - "tie_breaker": 0.9, - "boost": 0.1, - "_name": "keywords_query" - } - } - ], - "minimum_should_match": 1 - } -} -``` +1. `QueryParser.parse()` + 输出 `detected_language`、`query_text_by_lang`、`search_langs`、`index_languages`、`source_in_index_languages`。 +2. `ESQueryBuilder._build_advanced_text_query()` + 按 `search_langs` 动态拼接 `title/brief/description/vendor/category_*` 的 `.{lang}` 字段,叠加 shared 字段(`tags`、`option*_values`)。 +3. `build_query()` + 统一走文本策略,不再有布尔 AST 枝路。 -### 2. 集成查询分析功能 +## 3. 能力矩阵(Detector / Translator / Indexed) -在 `QueryParser` 中直接集成必要的分析功能: +三类能力的职责边界: -- **关键词提取**:使用 HanLP 提取查询中的名词(长度>1),用于关键词查询(可选,HanLP 不可用时降级) -- **查询类型判断**:区分短查询和长查询 -- **Token 计数**:用于判断查询长度 +- **Detector**:识别 query 源语言(`detected_language`) +- **Indexed**:租户可检索语言集合(`tenant_config.*.index_languages`) +- **Translator**:源语言到目标语言的可翻译能力及实时成功率 -### 3. 多查询策略 +### 3.1 决策规则 -#### 3.1 基础查询(base_query) -- 使用 `operator: "AND"` 确保所有词都必须匹配 -- `minimum_should_match: "75%"` 提高匹配精度 -- 使用 `tie_breaker: 0.9` 进行分数融合 +1. 若 `detected_language in index_languages`: + 源语言字段做主召回;其他语言走翻译补召回(低权重)。 +2. 若 `detected_language not in index_languages`: + 翻译到 `index_languages` 是主路径;源语言字段仅作弱召回。 +3. 若第 2 步翻译部分失败或全部失败: + 对缺失翻译的 `index_languages` 字段,追加“原文低权重兜底”子句,避免完全丢失这些语种索引面的召回机会。 -#### 3.2 翻译查询(base_query_trans_zh/en) -- 当查询语言不是中文/英文时,添加翻译查询 -- 使用较低的 boost(0.4)避免过度影响 -- 支持跨语言检索 +### 3.2 翻译等待策略 -#### 3.3 短语查询(phrase_query) -- 针对短查询(token_count >= 2 且 is_short,由 query_tokens 推导) -- 使用 `type: "phrase"` 进行精确短语匹配 -- 支持 slop(允许词序调整) +`QueryParser.parse()` 中: -#### 3.4 关键词查询(keywords_query) -- 使用 HanLP 提取的名词进行查询 -- 仅在关键词长度合理时启用(避免关键词占查询比例过高) -- 使用较低的 boost(0.1)作为补充 +- 当源语种不在 `index_languages`:使用 `translate_multi_async(...)` 并等待 futures 收敛 +- 当源语种在 `index_languages`:使用 `translate_multi(..., async_mode=True)`,优先缓存命中,未命中可后台补齐 -#### 3.5 长查询优化(long_query) -- 当前已禁用(参考实现中也是 False) -- 未来可根据需要启用 +这保证了“必须翻译才能检索”的场景不会直接空跑。 -#### 3.6 KNN 向量召回自适应策略(query_tokens) +## 4. 统一文本召回表达式 -根据 `query_tokens`(HanLP 分词后的 token 数量)动态调整 KNN 的召回数量和权重: +每个语言子句的基础形态: -| 查询类型 | token_count | knn_k | num_candidates | boost 系数 | -|---------|-------------|-------|----------------|------------| -| 短查询 | ≤ 2 | 30 | 100 | 0.6× 默认 | -| 中等查询| 3~4 | 50 | 200 | 1.0× 默认 | -| 长查询 | ≥ 5 | 80 | 300 | 1.4× 默认 | +```json +{ + "multi_match": { + "_name": "base_query|base_query_trans_xx|fallback_original_query_xx", + "query": "", + "fields": ["title.xx^3.0", "brief.xx^1.5", "...", "tags", "option1_values^0.5", "..."], + "minimum_should_match": "75%", + "tie_breaker": 0.9, + "boost": "<按策略决定,可省略>" + } +} +``` -**策略说明**: -- **短查询**:BM25 对精确匹配更有效,降低 KNN 召回和权重,避免语义召回干扰 -- **长查询**:语义搜索更有利,提高 KNN 召回和权重,增强语义理解能力 -- 默认 boost 由 `config.query_config.knn_boost` 配置(通常 0.25) +最终按 `bool.should` 组合,`minimum_should_match: 1`。 -### 4. 字段映射优化 +## 5. 关键配置项(文本策略) -新增 `_get_match_fields()` 方法,支持: -- 根据语言动态获取匹配字段 -- 区分全部字段(all_fields)和核心字段(core_fields) -- 核心字段用于短语查询和关键词查询,提高精度 +位于 `config/config.yaml -> query_config.text_query_strategy`: -## 实现细节 +- `base_minimum_should_match` +- `translation_minimum_should_match` +- `translation_boost` +- `translation_boost_when_source_missing` +- `source_boost_when_missing` +- `original_query_fallback_boost_when_translation_missing`(新增) +- `keywords_boost` +- `enable_phrase_query` +- `tie_breaker_base_query` +- `tie_breaker_keywords` -### 文件修改清单 +新增项说明: -1. **修改文件**: - - `query/query_parser.py` - 添加关键词提取、查询类型判断等功能(HanLP 可选) - - `search/es_query_builder.py` - 实现 should 子句的多查询策略 - - `search/searcher.py` - 传递 parsed_query 给查询构建器 +- `original_query_fallback_boost_when_translation_missing`: + 当源语种不在索引语言且翻译缺失时,原文打到缺失目标语字段的低权重系数,默认 `0.2`。 -### 关键参数说明 +## 6. 典型场景与实际 DSL -- **minimum_should_match**: 从 "67%" 提升到 "75%",提高匹配精度 -- **operator**: 从默认改为 "AND",确保所有词都匹配 -- **tie_breaker**: 保持 0.9,用于分数融合 -- **boost 值**: - - base_query: 1.0(默认) - - translation queries: 0.4 - - phrase_query: 1.0 - - keywords_query: 0.1 +以下示例来自当前 `ESQueryBuilder` 生成结果(已按当前代码验证)。 -### 依赖要求 +### 场景 A:源语种已在索引语言中,且翻译成功 -- **HanLP**(可选):如果安装了 `hanlp` 包,会自动启用关键词提取功能 - ```bash - pip install hanlp - ``` - - 如果未安装,系统会自动降级到简单分析(基于空格分词),不影响基本功能。 +- `detected_language=de` +- `index_languages=[de,en]` +- `query_text_by_lang={de:"herren schuhe", en:"men shoes"}` -- **HanLP 模型**:首次运行时会自动下载 - - Tokenizer: `CTB9_TOK_ELECTRA_BASE_CRF` - - POS Tagger: `CTB9_POS_ELECTRA_SMALL` +策略结果: -### 配置说明 +- `base_query`:德语字段,正常权重 +- `base_query_trans_en`:英语字段,`boost=translation_boost`(默认 0.4) -- **忽略关键词**:在 `_extract_keywords()` 方法中配置 - - 默认忽略:`['玩具']` +### 场景 B:源语种不在索引语言中,部分翻译缺失 -## 使用示例 +- `detected_language=de` +- `index_languages=[en,zh]` +- 只翻译出 `en`,`zh` 失败 -### 基本使用 +策略结果: -查询会自动使用优化后的策略,无需额外配置: +- `base_query`(德语字段):`boost=source_boost_when_missing`(默认 0.6) +- `base_query_trans_en`(英文字段):`boost=translation_boost_when_source_missing`(默认 1.0) +- `fallback_original_query_zh`(中文字段):原文低权重兜底(默认 0.2) -```python -# 在 searcher.py 中,查询会自动使用优化策略 -result = searcher.search( - query="戏水动物", - tenant_id="162", - size=10 -) -``` +### 场景 C:源语种不在索引语言中,翻译全部失败 -### 查看分析结果 +- `detected_language=de` +- `index_languages=[en,zh]` +- `query_text_by_lang` 仅有 `de` -可以直接从 `parsed_query` 查看分析结果: +策略结果: -```python -parsed_query = query_parser.parse("戏水动物") -print(f"关键词: {parsed_query.keywords}") -print(f"Token数: {parsed_query.token_count}") -print(f"query_tokens: {parsed_query.query_tokens}") -``` +- `base_query`(德语字段,低权重) +- `fallback_original_query_en`(英文字段原文兜底) +- `fallback_original_query_zh`(中文字段原文兜底) -## 性能考虑 +这能避免“只有源语种字段查询,且该语种字段在商家索引中稀疏/为空”导致的弱召回问题。 -1. **HanLP 初始化**:采用懒加载,首次使用时才初始化 -2. **错误处理**:HanLP 初始化失败或未安装时,系统会降级到简单分析(基于空格分词),不影响服务 -3. **代码精简**:所有功能直接集成在 `QueryParser` 中,无额外模块依赖 +## 7. QueryParser 与 ESBuilder 的职责分工 -## 后续优化方向 +- `QueryParser` 负责“语言计划”与“可用文本”: + - `search_langs` + - `query_text_by_lang` + - `source_in_index_languages` + - `index_languages` +- `ESQueryBuilder` 负责“表达式展开”: + - 动态字段组装 + - 子句权重分配 + - 翻译缺失兜底子句拼接 -1. **长查询优化**:可以启用长查询的特殊处理 -2. **意图识别**:完善意图词典,提供更精准的意图识别 -3. **参数调优**:根据实际效果调整 boost 值和 minimum_should_match -4. **A/B 测试**:对比优化前后的检索效果 +这种分层让策略调优主要落在配置和 Builder,不破坏 Parser 的职责边界。 -## 注意事项 +## 8. 兼容与注意事项 -1. **HanLP 依赖**:HanLP 是可选的,如果未安装或初始化失败,系统会自动降级到简单分析,不会影响基本功能 -2. **性能影响**:HanLP 分析会增加一定的处理时间,但采用懒加载机制 -3. **字段匹配**:确保 ES 索引中存在对应的中英文字段 -4. **代码精简**:所有功能都集成在现有模块中,保持代码结构简洁 +1. 当前文本主链路已移除布尔 AST 分支。 +2. 文档中的旧描述(如 `operator: AND` 固定开启)不再适用,当前实现未强制设置该参数。 +3. `HanLP` 为可选依赖;不可用时退化到轻量分词,不影响主链路可用性。 +4. 若后续扩展到更多语种,请确保: + - mapping 中存在对应 `.` 字段 + - `index_languages` 配置在支持列表内 + - 翻译 provider 对目标语种可用 -## 参考 +## 9. 建议测试清单 -- 参考实现中的查询构建逻辑 -- HanLP 官方文档:https://hanlp.hankcs.com/ -- Elasticsearch multi_match 查询文档 +建议在 `tests/` 增加文本策略用例: +1. 源语种在索引语言,翻译命中缓存 +2. 源语种不在索引语言,翻译部分失败(验证 fallback 子句) +3. 源语种不在索引语言,翻译全部失败(验证多目标 fallback) +4. 自定义 `original_query_fallback_boost_when_translation_missing` 生效 +5. 非 `zh/en` 语种字段动态拼接(如 `de/fr/es`) diff --git a/docs/相关性检索优化说明_old1.md b/docs/相关性检索优化说明_old1.md new file mode 100644 index 0000000..75c94b3 --- /dev/null +++ b/docs/相关性检索优化说明_old1.md @@ -0,0 +1,232 @@ +# 相关性检索优化说明 + +## 概述 + +本次优化将相关性检索从简单的 `must` 子句中的 `multi_match` 查询,改为使用 `should` 子句的多查询策略,参考了成熟的搜索实现,显著提升了检索效果。 + +## 主要改进 + +## 实现方式 + +本次优化采用精简实现,直接在 `QueryParser` 中集成必要的分析功能,不新增独立模块。 + +### 1. 查询结构优化 + +**之前的结构**(效果差): +```json +{ + "bool": { + "must": [ + { + "multi_match": { + "query": "戏水动物", + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], + "minimum_should_match": "67%", + "tie_breaker": 0.9, + "boost": 1, + "_name": "base_query" + } + } + ] + } +} +``` + +**优化后的结构**(效果更好): +```json +{ + "bool": { + "should": [ + { + "multi_match": { + "_name": "base_query", + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], + "minimum_should_match": "75%", + "operator": "AND", + "query": "戏水动物", + "tie_breaker": 0.9 + } + }, + { + "multi_match": { + "_name": "base_query_trans_en", + "boost": 0.4, + "fields": ["title.en^3.0", ...], + "minimum_should_match": "75%", + "operator": "AND", + "query": "water sports", + "tie_breaker": 0.9 + } + }, + { + "multi_match": { + "query": "戏水动物", + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], + "type": "phrase", + "slop": 2, + "boost": 1.0, + "_name": "phrase_query" + } + }, + { + "multi_match": { + "query": "戏水 动物", + "fields": ["title.zh^3.0", "brief.zh^1.5", ...], + "operator": "AND", + "tie_breaker": 0.9, + "boost": 0.1, + "_name": "keywords_query" + } + } + ], + "minimum_should_match": 1 + } +} +``` + +### 2. 集成查询分析功能 + +在 `QueryParser` 中直接集成必要的分析功能: + +- **关键词提取**:使用 HanLP 提取查询中的名词(长度>1),用于关键词查询(可选,HanLP 不可用时降级) +- **查询类型判断**:区分短查询和长查询 +- **Token 计数**:用于判断查询长度 + +### 3. 多查询策略 + +#### 3.1 基础查询(base_query) +- 使用 `operator: "AND"` 确保所有词都必须匹配 +- `minimum_should_match: "75%"` 提高匹配精度 +- 使用 `tie_breaker: 0.9` 进行分数融合 + +#### 3.2 翻译查询(base_query_trans_zh/en) +- 当查询语言不是中文/英文时,添加翻译查询 +- 使用较低的 boost(0.4)避免过度影响 +- 支持跨语言检索 + +#### 3.3 短语查询(phrase_query) +- 针对短查询(token_count >= 2 且 is_short,由 query_tokens 推导) +- 使用 `type: "phrase"` 进行精确短语匹配 +- 支持 slop(允许词序调整) + +#### 3.4 关键词查询(keywords_query) +- 使用 HanLP 提取的名词进行查询 +- 仅在关键词长度合理时启用(避免关键词占查询比例过高) +- 使用较低的 boost(0.1)作为补充 + +#### 3.5 长查询优化(long_query) +- 当前已禁用(参考实现中也是 False) +- 未来可根据需要启用 + +#### 3.6 KNN 向量召回自适应策略(query_tokens) + +根据 `query_tokens`(HanLP 分词后的 token 数量)动态调整 KNN 的召回数量和权重: + +| 查询类型 | token_count | knn_k | num_candidates | boost 系数 | +|---------|-------------|-------|----------------|------------| +| 短查询 | ≤ 2 | 30 | 100 | 0.6× 默认 | +| 中等查询| 3~4 | 50 | 200 | 1.0× 默认 | +| 长查询 | ≥ 5 | 80 | 300 | 1.4× 默认 | + +**策略说明**: +- **短查询**:BM25 对精确匹配更有效,降低 KNN 召回和权重,避免语义召回干扰 +- **长查询**:语义搜索更有利,提高 KNN 召回和权重,增强语义理解能力 +- 默认 boost 由 `config.query_config.knn_boost` 配置(通常 0.25) + +### 4. 字段映射优化 + +新增 `_get_match_fields()` 方法,支持: +- 根据语言动态获取匹配字段 +- 区分全部字段(all_fields)和核心字段(core_fields) +- 核心字段用于短语查询和关键词查询,提高精度 + +## 实现细节 + +### 文件修改清单 + +1. **修改文件**: + - `query/query_parser.py` - 添加关键词提取、查询类型判断等功能(HanLP 可选) + - `search/es_query_builder.py` - 实现 should 子句的多查询策略 + - `search/searcher.py` - 传递 parsed_query 给查询构建器 + +### 关键参数说明 + +- **minimum_should_match**: 从 "67%" 提升到 "75%",提高匹配精度 +- **operator**: 从默认改为 "AND",确保所有词都匹配 +- **tie_breaker**: 保持 0.9,用于分数融合 +- **boost 值**: + - base_query: 1.0(默认) + - translation queries: 0.4 + - phrase_query: 1.0 + - keywords_query: 0.1 + +### 依赖要求 + +- **HanLP**(可选):如果安装了 `hanlp` 包,会自动启用关键词提取功能 + ```bash + pip install hanlp + ``` + + 如果未安装,系统会自动降级到简单分析(基于空格分词),不影响基本功能。 + +- **HanLP 模型**:首次运行时会自动下载 + - Tokenizer: `CTB9_TOK_ELECTRA_BASE_CRF` + - POS Tagger: `CTB9_POS_ELECTRA_SMALL` + +### 配置说明 + +- **忽略关键词**:在 `_extract_keywords()` 方法中配置 + - 默认忽略:`['玩具']` + +## 使用示例 + +### 基本使用 + +查询会自动使用优化后的策略,无需额外配置: + +```python +# 在 searcher.py 中,查询会自动使用优化策略 +result = searcher.search( + query="戏水动物", + tenant_id="162", + size=10 +) +``` + +### 查看分析结果 + +可以直接从 `parsed_query` 查看分析结果: + +```python +parsed_query = query_parser.parse("戏水动物") +print(f"关键词: {parsed_query.keywords}") +print(f"Token数: {parsed_query.token_count}") +print(f"query_tokens: {parsed_query.query_tokens}") +``` + +## 性能考虑 + +1. **HanLP 初始化**:采用懒加载,首次使用时才初始化 +2. **错误处理**:HanLP 初始化失败或未安装时,系统会降级到简单分析(基于空格分词),不影响服务 +3. **代码精简**:所有功能直接集成在 `QueryParser` 中,无额外模块依赖 + +## 后续优化方向 + +1. **长查询优化**:可以启用长查询的特殊处理 +2. **意图识别**:完善意图词典,提供更精准的意图识别 +3. **参数调优**:根据实际效果调整 boost 值和 minimum_should_match +4. **A/B 测试**:对比优化前后的检索效果 + +## 注意事项 + +1. **HanLP 依赖**:HanLP 是可选的,如果未安装或初始化失败,系统会自动降级到简单分析,不会影响基本功能 +2. **性能影响**:HanLP 分析会增加一定的处理时间,但采用懒加载机制 +3. **字段匹配**:确保 ES 索引中存在对应的中英文字段 +4. **代码精简**:所有功能都集成在现有模块中,保持代码结构简洁 + +## 参考 + +- 参考实现中的查询构建逻辑 +- HanLP 官方文档:https://hanlp.hankcs.com/ +- Elasticsearch multi_match 查询文档 + diff --git a/search/es_query_builder.py b/search/es_query_builder.py index 4e9f5cd..ae8d0ad 100644 --- a/search/es_query_builder.py +++ b/search/es_query_builder.py @@ -34,6 +34,7 @@ class ESQueryBuilder: translation_boost: float = 0.4, translation_boost_when_source_missing: float = 1.0, source_boost_when_missing: float = 0.6, + original_query_fallback_boost_when_translation_missing: float = 0.2, keywords_boost: float = 0.1, enable_phrase_query: bool = True, tie_breaker_base_query: float = 0.9, @@ -72,6 +73,9 @@ class ESQueryBuilder: self.translation_boost = float(translation_boost) self.translation_boost_when_source_missing = float(translation_boost_when_source_missing) self.source_boost_when_missing = float(source_boost_when_missing) + self.original_query_fallback_boost_when_translation_missing = float( + original_query_fallback_boost_when_translation_missing + ) self.keywords_boost = float(keywords_boost) self.enable_phrase_query = bool(enable_phrase_query) self.tie_breaker_base_query = float(tie_breaker_base_query) @@ -476,6 +480,7 @@ class ESQueryBuilder: search_langs: List[str] = [] source_lang = self.default_language source_in_index_languages = True + index_languages: List[str] = [] keywords = "" query_tokens = [] token_count = 0 @@ -488,6 +493,7 @@ class ESQueryBuilder: source_in_index_languages = bool( getattr(parsed_query, "source_in_index_languages", True) ) + index_languages = getattr(parsed_query, "index_languages", None) or [] keywords = getattr(parsed_query, 'keywords', '') or "" query_tokens = getattr(parsed_query, 'query_tokens', None) or [] token_count = len(query_tokens) or getattr(parsed_query, 'token_count', 0) or 0 @@ -543,6 +549,37 @@ class ESQueryBuilder: "multi_match": clause["multi_match"] }) + # Fallback: source language is not indexed and translation for some index languages is missing. + # Use original query text on missing index-language fields with a low boost. + if not source_in_index_languages and query_text and index_languages: + normalized_index_langs: List[str] = [] + seen_langs = set() + for lang in index_languages: + norm_lang = str(lang or "").strip().lower() + if not norm_lang or norm_lang in seen_langs: + continue + seen_langs.add(norm_lang) + normalized_index_langs.append(norm_lang) + + for lang in normalized_index_langs: + if lang == source_lang: + continue + if lang in query_text_by_lang: + continue + match_fields, _ = self._get_match_fields(lang) + if not match_fields: + continue + should_clauses.append({ + "multi_match": { + "_name": f"fallback_original_query_{lang}", + "query": query_text, + "fields": match_fields, + "minimum_should_match": self.translation_minimum_should_match, + "tie_breaker": self.tie_breaker_base_query, + "boost": self.original_query_fallback_boost_when_translation_missing, + } + }) + # 3. Short query - add phrase query (derived from query_tokens) # is_short: quoted or ((token_count <= 2 or len <= 4) and no space) source_query_text = query_text_by_lang.get(source_lang) or query_text diff --git a/search/searcher.py b/search/searcher.py index 1d974a4..718a42e 100644 --- a/search/searcher.py +++ b/search/searcher.py @@ -122,6 +122,9 @@ class Searcher: translation_boost=self.config.query_config.translation_boost, translation_boost_when_source_missing=self.config.query_config.translation_boost_when_source_missing, source_boost_when_missing=self.config.query_config.source_boost_when_missing, + original_query_fallback_boost_when_translation_missing=( + self.config.query_config.original_query_fallback_boost_when_translation_missing + ), keywords_boost=self.config.query_config.keywords_boost, enable_phrase_query=self.config.query_config.enable_phrase_query, tie_breaker_base_query=self.config.query_config.tie_breaker_base_query, -- libgit2 0.21.2