From a866b68809b1de63342da87e95e62c717a039ccd Mon Sep 17 00:00:00 2001 From: tangwang Date: Mon, 26 Jan 2026 14:58:06 +0800 Subject: [PATCH] 翻译接口 --- docs/temporary/sku_image_src问题诊断报告.md | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/翻译功能测试说明.md | 197 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- query/translator.py | 26 +++++++++++++++++++++----- 3 files changed, 138 insertions(+), 202 deletions(-) create mode 100644 docs/temporary/sku_image_src问题诊断报告.md delete mode 100644 docs/翻译功能测试说明.md diff --git a/docs/temporary/sku_image_src问题诊断报告.md b/docs/temporary/sku_image_src问题诊断报告.md new file mode 100644 index 0000000..9a98b98 --- /dev/null +++ b/docs/temporary/sku_image_src问题诊断报告.md @@ -0,0 +1,117 @@ +# SKU image_src 字段为空问题诊断报告 + +## 问题描述 + +返回结果的每条结果中,多款式字段 `skus` 下面每个 SKU 的 `image_src` 为空。 + +## 问题分析 + +### 1. ES 数据检查 + +通过查询 ES 数据,发现: +- ES 中确实有 `skus` 数据(不是空数组) +- 但是 `skus` 数组中的每个 SKU 对象**都没有 `image_src` 字段** + +示例 ES 文档: +```json +{ + "spu_id": "68238", + "skus": [ + { + "sku_id": "3568395", + "price": 329.61, + "compare_at_price": 485.65, + "sku_code": "3468269", + "stock": 57, + "weight": 0.26, + "weight_unit": "kg", + "option1_value": "", + "option2_value": "", + "option3_value": "" + // 注意:这里没有 image_src 字段 + } + ] +} +``` + +### 2. 代码逻辑检查 + +在 `indexer/document_transformer.py` 的 `_transform_sku_row` 方法中(第558-560行),原有逻辑为: + +```python +# Image src +if pd.notna(sku_row.get('image_src')): + sku_data['image_src'] = str(sku_row['image_src']) +``` + +**问题根源**: +- 只有当 MySQL 中的 `image_src` 字段**非空**时,才会将其添加到 `sku_data` 字典中 +- 如果 MySQL 中的 `image_src` 是 `NULL` 或空字符串,这个字段就**不会出现在返回的字典中** +- 导致 ES 文档中缺少 `image_src` 字段 +- API 返回时,`sku_entry.get('image_src')` 返回 `None`,前端看到的就是空值 + +### 3. MySQL 数据情况 + +根据代码逻辑推断: +- MySQL 的 `shoplazza_product_sku` 表中,`image_src` 字段可能为 `NULL` 或空字符串 +- 这导致索引时该字段没有被写入 ES + +## 解决方案 + +### 修复方案 + +修改 `indexer/document_transformer.py` 中的 `_transform_sku_row` 方法,**始终包含 `image_src` 字段**,即使值为空也设置为 `None`: + +```python +# Image src - always include this field, even if empty +# This ensures the field is present in ES documents and API responses +image_src = sku_row.get('image_src') +if pd.notna(image_src) and str(image_src).strip(): + sku_data['image_src'] = str(image_src).strip() +else: + # Set to None (will be serialized as null in JSON) instead of omitting the field + sku_data['image_src'] = None +``` + +### 修复效果 + +修复后: +1. **即使 MySQL 中 `image_src` 为 NULL 或空字符串**,ES 文档中也会包含该字段(值为 `null`) +2. API 返回时,前端可以明确知道该字段存在但值为空 +3. 符合 API 模型定义:`image_src: Optional[str] = Field(None, ...)` + +## 问题分类 + +**问题类型**:**本项目填充的问题** + +- ✅ **不是 MySQL 原始数据的问题**:MySQL 中 `image_src` 字段可能确实为 NULL,但这是正常的业务数据 +- ✅ **不是 ES 数据的问题**:ES mapping 中 `image_src` 字段定义正确 +- ❌ **是本项目填充的问题**:代码逻辑导致当 MySQL 中 `image_src` 为空时,该字段没有被写入 ES 文档 + +## 后续操作 + +1. **重新索引数据**:修复代码后,需要重新索引数据才能生效 + ```bash + # 重新索引指定租户的数据 + ./scripts/ingest.sh true + ``` + +2. **验证修复**:重新索引后,查询 ES 验证 `image_src` 字段是否已包含: + ```bash + curl -u 'essa:4hOaLaf41y2VuI8y' -X GET 'http://localhost:9200/search_products/_search?pretty' \ + -H 'Content-Type: application/json' \ + -d '{ + "size": 1, + "query": {"nested": {"path": "skus", "query": {"exists": {"field": "skus"}}}}, + "_source": ["spu_id", "skus"] + }' + ``` + +3. **可选优化**:如果业务需要,可以考虑当 SKU 的 `image_src` 为空时,使用 SPU 的主图(`image_url`)作为默认值 + +## 相关文件 + +- `indexer/document_transformer.py` - 已修复 +- `api/models.py` - `SkuResult.image_src: Optional[str]` - 模型定义正确 +- `api/result_formatter.py` - `image_src=sku_entry.get('image_src')` - 读取逻辑正确 +- `mappings/search_products.json` - `skus.image_src` mapping 定义正确 diff --git a/docs/翻译功能测试说明.md b/docs/翻译功能测试说明.md deleted file mode 100644 index 76bb4a9..0000000 --- a/docs/翻译功能测试说明.md +++ /dev/null @@ -1,197 +0,0 @@ -# 翻译功能测试说明 - -## 功能概述 - -本次更新实现了以下功能: - -1. **翻译提示词配置**:支持中英文提示词,用于提高翻译质量 -2. **DeepL Context参数**:提示词作为DeepL API的`context`参数传递(不参与翻译,仅提供上下文) -3. **同步/异步翻译**: - - 索引场景:同步翻译,等待结果返回 - - 查询场景:异步翻译,立即返回缓存结果 -4. **缓存机制**:翻译结果自动缓存,避免重复翻译 - -## 配置说明 - -### 配置文件位置 - -`config/config.yaml` - -### 翻译提示词配置 - -```yaml -translation_prompts: - # 商品标题翻译提示词 - product_title.zh: "请将原文翻译成中文商品SKU名称,要求:确保精确、完整地传达原文信息的基础上,语言简洁清晰、地道、专业。" - product_title.en: "Translate the original text into an English product SKU name. Requirements: Ensure accurate and complete transmission of the original information, with concise, clear, authentic, and professional language." - # query翻译提示词 - query_zh: "电商领域" - query_en: "e-commerce domain" - # 默认翻译用词 - default_zh: "电商领域" - default_en: "e-commerce domain" -``` - -### 提示词使用规则 - -1. **商品标题翻译**: - - 中文→英文:使用 `product_title.en` - - 英文→中文:使用 `product_title.zh` - -2. **其他字段翻译**(brief, description, vendor): - - 根据目标语言选择 `default_zh` 或 `default_en` - -3. **查询翻译**: - - 根据目标语言选择 `query_zh` 或 `query_en` - -## 测试方法 - -### 1. 测试配置加载 - -```python -from config import ConfigLoader - -config_loader = ConfigLoader() -config = config_loader.load_config() - -# 检查翻译提示词配置 -print(config.query_config.translation_prompts) -``` - -### 2. 测试同步翻译(索引场景) - -```python -from query.translator import Translator -from config import ConfigLoader - -config = ConfigLoader().load_config() -translator = Translator( - api_key=config.query_config.translation_api_key, - use_cache=True -) - -# 测试商品标题翻译 -text = "蓝牙耳机" -prompt = config.query_config.translation_prompts.get('product_title.en') -result = translator.translate( - text, - target_lang='en', - source_lang='zh', - prompt=prompt -) -print(f"翻译结果: {result}") -``` - -### 3. 测试异步翻译(查询场景) - -```python -# 异步模式(立即返回,后台翻译) -results = translator.translate_multi( - "手机", - target_langs=['en'], - source_lang='zh', - async_mode=True, - prompt=config.query_config.translation_prompts.get('query_zh') -) -print(f"异步结果: {results}") # 可能包含None(后台翻译中) - -# 同步模式(等待完成) -results_sync = translator.translate_multi( - "手机", - target_langs=['en'], - source_lang='zh', - async_mode=False, - prompt=config.query_config.translation_prompts.get('query_zh') -) -print(f"同步结果: {results_sync}") -``` - -### 4. 测试文档转换器集成 - -```python -from indexer.document_transformer import SPUDocumentTransformer -import pandas as pd - -# 创建模拟数据 -spu_row = pd.Series({ - 'id': 123, - 'tenant_id': '1', - 'title': '蓝牙耳机', - 'brief': '高品质无线蓝牙耳机', - 'description': '这是一款高品质的无线蓝牙耳机。', - 'vendor': '品牌A', - # ... 其他字段 -}) - -# 初始化转换器(带翻译器) -transformer = SPUDocumentTransformer( - category_id_to_name={}, - searchable_option_dimensions=['option1', 'option2', 'option3'], - tenant_config={'primary_language': 'zh', 'translate_to_en': True}, - translator=translator, - translation_prompts=config.query_config.translation_prompts -) - -# 转换文档 -doc = transformer.transform_spu_to_doc( - tenant_id='1', - spu_row=spu_row, - skus=pd.DataFrame(), - options=pd.DataFrame() -) - -print(f"title.zh: {doc.get('title.zh')}") -print(f"title.en: {doc.get('title.en')}") # 应该包含翻译结果 -``` - -### 5. 测试缓存功能 - -```python -# 第一次翻译(调用API) -result1 = translator.translate("测试文本", "en", "zh", prompt="电商领域") - -# 第二次翻译(使用缓存) -result2 = translator.translate("测试文本", "en", "zh", prompt="电商领域") - -assert result1 == result2 # 应该相同 -``` - -## DeepL API Context参数说明 - -根据 [DeepL API文档](https://developers.deepl.com/api-reference/translate/request-translation): - -- `context` 参数:Additional context that can influence a translation but is not translated itself -- Context中的字符不计入计费 -- Context用于提供翻译上下文,帮助提高翻译质量 - -我们的实现: -- 将提示词作为 `context` 参数传递给DeepL API -- Context不参与翻译,仅提供上下文信息 -- 不同场景使用不同的提示词(商品标题、查询、默认) - -## 运行完整测试 - -```bash -# 激活环境 -source /home/tw/miniconda3/etc/profile.d/conda.sh -conda activate searchengine - -# 运行测试脚本 -python scripts/test_translation.py -``` - -## 验证要点 - -1. **配置加载**:确认所有提示词配置正确加载 -2. **同步翻译**:索引时翻译结果正确填充到文档 -3. **异步翻译**:查询时缓存命中立即返回,未命中后台翻译 -4. **提示词使用**:不同场景使用正确的提示词 -5. **缓存机制**:相同文本和提示词的翻译结果被缓存 - -## 注意事项 - -1. 需要配置 `DEEPL_AUTH_KEY` 环境变量或 `translation_api_key` -2. 如果没有API key,翻译器会返回原文(mock模式) -3. 缓存文件存储在 `.cache/translations.json` -4. Context参数中的字符不计入DeepL计费 - diff --git a/query/translator.py b/query/translator.py index 92d7af2..80ed401 100644 --- a/query/translator.py +++ b/query/translator.py @@ -141,7 +141,9 @@ class Translator: ) # Test connection self.redis_client.ping() - self.expire_time = timedelta(days=REDIS_CONFIG.get('translation_cache_expire_days', 360)) + expire_days = REDIS_CONFIG.get('translation_cache_expire_days', 360) + self.expire_time = timedelta(days=expire_days) + self.expire_seconds = int(self.expire_time.total_seconds()) # Redis 需要秒数 self.cache_prefix = REDIS_CONFIG.get('translation_cache_prefix', 'trans') logger.info("Redis cache initialized for translations") except Exception as e: @@ -622,7 +624,13 @@ class Translator: context: Optional[str] = None, prompt: Optional[str] = None ) -> Optional[str]: - """Get translation from Redis cache with sliding expiration.""" + """ + Get translation from Redis cache with sliding expiration. + + 滑动过期机制:每次访问缓存时,重置过期时间为配置的过期时间(默认720天)。 + 这样缓存会在最后一次访问后的720天才过期,而不是写入后的720天。 + 这确保了常用的翻译缓存不会被过早删除。 + """ if not self.redis_client: return None @@ -634,10 +642,18 @@ class Translator: value = self.redis_client.get(cache_key) if value: # Sliding expiration: reset expiration time on access - self.redis_client.expire(cache_key, self.expire_time) + # 每次读取缓存时,重置过期时间为配置的过期时间(最后一次访问后的N天才过期) + try: + self.redis_client.expire(cache_key, self.expire_seconds) + except Exception as expire_error: + # 即使 expire 失败,也返回缓存值(不影响功能) + logger.warning( + f"[Translator] Failed to update cache expiration for key {cache_key}: {expire_error}" + ) + logger.debug( f"[Translator] Redis cache hit | Original text: '{text}' | Target language: {target_lang} | " - f"Cache key: {cache_key} | Translation result: '{value}'" + f"Cache key: {cache_key} | Translation result: '{value}' | TTL reset to {self.expire_seconds}s" ) return value logger.debug( @@ -664,7 +680,7 @@ class Translator: try: cache_key = f"{self.cache_prefix}:{target_lang.upper()}:{text}" - self.redis_client.setex(cache_key, self.expire_time, translation) + self.redis_client.setex(cache_key, self.expire_seconds, translation) logger.debug( f"[Translator] Redis cache write | Original text: '{text}' | Target language: {target_lang} | " f"Cache key: {cache_key} | Translation result: '{translation}'" -- libgit2 0.21.2