From 768ad710662047243eb18877c9a43cc6ec54c60b Mon Sep 17 00:00:00 2001 From: tangwang Date: Wed, 31 Dec 2025 16:58:16 +0800 Subject: [PATCH] MySQL到ES字段映射说明-业务版.md --- README.md | 27 +++++++++++++++++++++++++++ api/translator_app.py | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/CNCLIP_SERVICE.md | 187 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/CNCLIP_SERVICE说明文档.md | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/MySQL到ES字段映射说明-业务版.md | 570 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/MySQL到ES文档映射说明.md | 961 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/blog/img_seo_analysis.md | 34 ++++++++++++++++++++++++++++++++++ docs/blog/seo_optimizer_analysis.md | 38 ++++++++++++++++++++++++++++++++++++++ docs/blog/smart_search_analysis.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/blog/website_seo_analysis.md | 33 +++++++++++++++++++++++++++++++++ docs/blog/店匠(Shoplazza)插件功能分析报告:搜索与推荐方向的市场需求与技术方案提炼.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/亚马逊到店匠格式转换分析.md | 1 + scripts/compare_index_mappings.py | 189 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/test_cnclip_service.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/test_cnclip_service.sh | 131 ----------------------------------------------------------------------------------------------------------------------------------- search/searcher.py | 1 + 16 files changed, 2624 insertions(+), 318 deletions(-) create mode 100644 api/translator_app.py delete mode 100644 docs/CNCLIP_SERVICE.md create mode 100644 docs/CNCLIP_SERVICE说明文档.md create mode 100644 docs/MySQL到ES字段映射说明-业务版.md create mode 100644 docs/MySQL到ES文档映射说明.md create mode 100644 docs/blog/img_seo_analysis.md create mode 100644 docs/blog/seo_optimizer_analysis.md create mode 100644 docs/blog/smart_search_analysis.md create mode 100644 docs/blog/website_seo_analysis.md create mode 100644 docs/blog/店匠(Shoplazza)插件功能分析报告:搜索与推荐方向的市场需求与技术方案提炼.md create mode 100644 scripts/compare_index_mappings.py create mode 100755 scripts/test_cnclip_service.py delete mode 100755 scripts/test_cnclip_service.sh diff --git a/README.md b/README.md index 781a6e6..59b7577 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,33 @@ query anchor + +对外: +embedding服务: + curl -X POST http://120.76.41.98:6005/embed/text \ + -H "Content-Type: application/json" \ + -d '["衣服", "Bohemian Maxi Dress"]' + + +翻译服务: +# 方式1:直接运行 +python api/translator_app.py +# 方式2:使用 uvicorn +uvicorn api.translator_app:app --host 0.0.0.0 --port 6006 --reload + +curl -X POST http://localhost:6006/translate -H "Content-Type: application/json" -d '{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh" + }' + +localhost替换为 +服务器内网地址: +10.0.163.168 +公网地址: +120.76.41.98 + + # 电商搜索引擎 SaaS 一个针对跨境独立站(店匠 Shoplazza 等)的多租户可配置搜索平台。README 作为项目导航入口,帮助你在不同阶段定位到更详细的文档。 diff --git a/api/translator_app.py b/api/translator_app.py new file mode 100644 index 0000000..7f41913 --- /dev/null +++ b/api/translator_app.py @@ -0,0 +1,283 @@ + +""" + +# 方式1:直接运行 +python api/translator_app.py + +# 方式2:使用 uvicorn +uvicorn api.translator_app:app --host 0.0.0.0 --port 6006 --reload + + +使用说明: +Translation HTTP Service + +This service provides a RESTful API for text translation using DeepL API. +The service runs on port 6006 and provides a simple translation endpoint. + +API Endpoint: + POST /translate + +Request Body (JSON): + { + "text": "要翻译的文本", + "target_lang": "en", # Required: target language code (zh, en, ru, etc.) + "source_lang": "zh" # Optional: source language code (auto-detect if not provided) + } + +Response (JSON): + { + "text": "要翻译的文本", + "target_lang": "en", + "source_lang": "zh", + "translated_text": "Text to translate", + "status": "success" + } + +Usage Examples: + +1. Translate Chinese to English: + curl -X POST http://localhost:6006/translate \ + -H "Content-Type: application/json" \ + -d '{ + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh" + }' + +2. Translate with auto-detection: + curl -X POST http://localhost:6006/translate \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Product name", + "target_lang": "zh" + }' + +3. Translate Russian to English: + curl -X POST http://localhost:6006/translate \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Название товара", + "target_lang": "en", + "source_lang": "ru" + }' + +Health Check: + GET /health + + curl http://localhost:6006/health + +Start the service: + python api/translator_app.py + # or + uvicorn api.translator_app:app --host 0.0.0.0 --port 6006 --reload +""" + +import os +import sys +import logging +import argparse +import uvicorn +from typing import Optional +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from query.translator import Translator +from config.env_config import DEEPL_AUTH_KEY, REDIS_CONFIG + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Fixed translation prompt +TRANSLATION_PROMPT = "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." + +# Global translator instance +_translator: Optional[Translator] = None + + +def init_translator(): + """Initialize translator instance.""" + global _translator + if _translator is None: + logger.info("Initializing translator...") + _translator = Translator( + api_key=DEEPL_AUTH_KEY, + use_cache=True, + timeout=10 + ) + logger.info("Translator initialized") + return _translator + + +# Request/Response models +class TranslationRequest(BaseModel): + """Translation request model.""" + text: str = Field(..., description="Text to translate") + target_lang: str = Field(..., description="Target language code (zh, en, ru, etc.)") + source_lang: Optional[str] = Field(None, description="Source language code (optional, auto-detect if not provided)") + + class Config: + json_schema_extra = { + "example": { + "text": "商品名称", + "target_lang": "en", + "source_lang": "zh" + } + } + + +class TranslationResponse(BaseModel): + """Translation response model.""" + text: str = Field(..., description="Original text") + target_lang: str = Field(..., description="Target language code") + source_lang: Optional[str] = Field(None, description="Source language code (detected or provided)") + translated_text: str = Field(..., description="Translated text") + status: str = Field(..., description="Translation status") + + +# Create FastAPI app +app = FastAPI( + title="Translation Service API", + description="RESTful API for text translation using DeepL", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc" +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +async def startup_event(): + """Initialize translator on startup.""" + logger.info("Starting Translation Service API on port 6006") + try: + init_translator() + logger.info("Translation service ready") + except Exception as e: + logger.error(f"Failed to initialize translator: {e}", exc_info=True) + logger.warning("Service will start but translation may not work correctly") + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + try: + translator = init_translator() + return { + "status": "healthy", + "service": "translation", + "translator_initialized": translator is not None, + "cache_enabled": translator.use_cache if translator else False + } + except Exception as e: + logger.error(f"Health check failed: {e}") + return JSONResponse( + status_code=503, + content={ + "status": "unhealthy", + "error": str(e) + } + ) + + +@app.post("/translate", response_model=TranslationResponse) +async def translate(request: TranslationRequest): + """ + Translate text to target language. + + Uses a fixed prompt optimized for product SKU name translation. + The translation is cached in Redis for performance. + """ + if not request.text or not request.text.strip(): + raise HTTPException( + status_code=400, + detail="Text cannot be empty" + ) + + if not request.target_lang: + raise HTTPException( + status_code=400, + detail="target_lang is required" + ) + + try: + translator = init_translator() + + # Translate using the fixed prompt + translated_text = translator.translate( + text=request.text, + target_lang=request.target_lang, + source_lang=request.source_lang, + prompt=TRANSLATION_PROMPT + ) + + if translated_text is None: + raise HTTPException( + status_code=500, + detail="Translation failed" + ) + + return TranslationResponse( + text=request.text, + target_lang=request.target_lang, + source_lang=request.source_lang, + translated_text=translated_text, + status="success" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Translation error: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Translation error: {str(e)}" + ) + + +@app.get("/") +async def root(): + """Root endpoint with API information.""" + return { + "service": "Translation Service API", + "version": "1.0.0", + "status": "running", + "endpoints": { + "translate": "POST /translate", + "health": "GET /health", + "docs": "GET /docs" + } + } + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Start translation API service') + parser.add_argument('--host', default='0.0.0.0', help='Host to bind to') + parser.add_argument('--port', type=int, default=6006, help='Port to bind to') + parser.add_argument('--reload', action='store_true', help='Enable auto-reload') + args = parser.parse_args() + + # Run server + uvicorn.run( + "api.translator_app:app", + host=args.host, + port=args.port, + reload=args.reload + ) + diff --git a/docs/CNCLIP_SERVICE.md b/docs/CNCLIP_SERVICE.md deleted file mode 100644 index 0e4d919..0000000 --- a/docs/CNCLIP_SERVICE.md +++ /dev/null @@ -1,187 +0,0 @@ -# CN-CLIP 编码服务 - -## 模块说明 - -CN-CLIP 编码服务基于 [clip-as-service](https://github.com/jina-ai/clip-as-service) 提供中文 CLIP 模型的文本和图像编码功能。服务使用 gRPC 协议,支持批量编码,返回固定维度的向量表示。 - -### 功能特性 - -- 文本编码:将中文文本编码为向量 -- 图像编码:将图像(本地文件或远程 URL)编码为向量 -- 混合编码:同时编码文本和图像 -- 批量处理:支持批量编码,提高效率 - -### 技术架构 - -- **框架**: clip-as-service (基于 Jina) -- **模型**: CN-CLIP/ViT-L-14-336(默认) -- **协议**: gRPC(默认,官方推荐) -- **运行时**: PyTorch - -## 启动服务 - -### 基本用法 - -```bash -./scripts/start_cnclip_service.sh -``` - -### 启动参数 - -| 参数 | 说明 | 默认值 | -|------|------|--------| -| `--port PORT` | 服务端口 | 51000 | -| `--device DEVICE` | 设备类型:cuda 或 cpu | 自动检测 | -| `--batch-size SIZE` | 批处理大小 | 32 | -| `--num-workers NUM` | 预处理线程数 | 4 | -| `--dtype TYPE` | 数据类型:float16 或 float32 | float16 | -| `--model-name NAME` | 模型名称 | CN-CLIP/ViT-L-14-336 | -| `--replicas NUM` | 副本数 | 1 | - -### 示例 - -```bash -# 使用默认配置启动 -./scripts/start_cnclip_service.sh - -# 指定端口和设备 -./scripts/start_cnclip_service.sh --port 52000 --device cpu - -# 使用其他模型 -./scripts/start_cnclip_service.sh --model-name CN-CLIP/ViT-H-14 -``` - -### 停止服务 - -```bash -./scripts/stop_cnclip_service.sh -``` - -## API 接口说明 - -### Python 客户端 - -服务使用 gRPC 协议,必须使用 Python 客户端: - -```python -from clip_client import Client - -# 创建客户端(使用 grpc:// 协议) -c = Client('grpc://localhost:51000') -``` - -### 编码接口 - -#### 1. 文本编码 - -```python -from clip_client import Client - -c = Client('grpc://localhost:51000') - -# 编码单个文本 -result = c.encode(['这是测试文本']) -print(result.shape) # (1, 1024) - -# 编码多个文本 -result = c.encode(['文本1', '文本2', '文本3']) -print(result.shape) # (3, 1024) -``` - -#### 2. 图像编码 - -```python -# 编码远程图像 URL -result = c.encode(['https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg']) -print(result.shape) # (1, 1024) - -# 编码本地图像文件 -result = c.encode(['/path/to/image.jpg']) -print(result.shape) # (1, 1024) -``` - -#### 3. 混合编码 - -```python -# 同时编码文本和图像 -result = c.encode([ - '这是文本', - 'https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg', - '另一个文本' -]) -print(result.shape) # (3, 1024) -``` - -### 返回格式 - -- **类型**: `numpy.ndarray` -- **形状**: `(N, 1024)`,其中 N 是输入数量 -- **数据类型**: `float32` -- **维度**: 1024(CN-CLIP 模型的 embedding 维度) - -### 支持的模型 - -| 模型名称 | 说明 | 推荐场景 | -|---------|------|---------| -| `CN-CLIP/ViT-B-16` | 基础版本,速度快 | 对速度要求高的场景 | -| `CN-CLIP/ViT-L-14` | 平衡版本 | 通用场景 | -| `CN-CLIP/ViT-L-14-336` | 高分辨率版本(默认) | 需要处理高分辨率图像 | -| `CN-CLIP/ViT-H-14` | 大型版本,精度高 | 对精度要求高的场景 | -| `CN-CLIP/RN50` | ResNet-50 版本 | 兼容性场景 | - -## 测试 - -运行测试脚本: - -```bash -./scripts/test_cnclip_service.sh -``` - -测试脚本会验证: -- 文本编码功能 -- 图像编码功能(远程 URL) -- 混合编码功能 - -每个测试会显示 embedding 的维度和前 20 个数字。 - -## 查看日志 - -```bash -tail -f /data/tw/SearchEngine/logs/cnclip_service.log -``` - -## 常见问题 - -### 1. 服务启动失败 - -- 检查端口是否被占用:`lsof -i :51000` -- 检查 conda 环境是否正确激活 -- 查看日志文件获取详细错误信息 - -### 2. 客户端连接失败 - -确保使用正确的协议: - -```python -# 正确:使用 grpc:// -c = Client('grpc://localhost:51000') - -# 错误:不要使用 http:// -# c = Client('http://localhost:51000') # 会失败 -``` - -### 3. 编码失败 - -- 检查服务是否正常运行 -- 检查输入格式是否正确 -- 查看服务日志排查错误 - -### 4. 依赖安装 - -确保已安装必要的依赖: - -```bash -pip install clip-client -``` - -服务端依赖会在启动脚本中自动检查。 diff --git a/docs/CNCLIP_SERVICE说明文档.md b/docs/CNCLIP_SERVICE说明文档.md new file mode 100644 index 0000000..d2725af --- /dev/null +++ b/docs/CNCLIP_SERVICE说明文档.md @@ -0,0 +1,210 @@ + +# TODO + +现在,跟自己 cn_clip 预估的结果,有差别: +这个比较接近: 可能是预处理逻辑有些不一样。 +https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg +normlize后的结果: +0.046295166015625,0.012847900390625,-0.0299530029296875,-0.01629638671875,0.01708984375,0.00487518310546875,0.01284027099609375,0.01348876953125,0.04617632180452347, 0.012860896065831184, -0.030133124440908432, -0.0162516962736845, +0.04617632180452347, 0.012860896065831184, -0.030133124440908432, -0.0162516962736845, 0.01708567887544632, 0.005110889207571745 + +以下两个,差别非常大,感觉不是一个模型: +https://aisearch.cdn.bcebos.com/fileManager/GtB5doGAr1skTx38P7fb7Q/182.jpg?authorization=bce-auth-v1%2F7e22d8caf5af46cc9310f1e3021709f3%2F2025-12-30T04%3A45%3A38Z%2F86400%2Fhost%2Ffe222039926cb7ff593021af40268c782b8892598114e24773d0c1bfc976a8df +https://oss.essa.cn/2e353867-7496-4d4e-a7c8-0af50f49f6eb.jpg?x-oss-process=image/resize,m_lfit,w_2048,h_2048 + +curl -X POST "http://120.76.41.98:5000/embedding/generate_image_embeddings" -H "Content-Type: application/json" -d '[ + { + "id": "test_1", + "pic_url": "https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg" + } + ]' + + + +# CN-CLIP 编码服务 + +## 模块说明 + +CN-CLIP 编码服务基于 [clip-as-service](https://github.com/jina-ai/clip-as-service) 提供中文 CLIP 模型的文本和图像编码功能。服务使用 gRPC 协议,支持批量编码,返回固定维度的向量表示。 + +### 功能特性 + +- 文本编码:将中文文本编码为向量 +- 图像编码:将图像(本地文件或远程 URL)编码为向量 +- 混合编码:同时编码文本和图像 +- 批量处理:支持批量编码,提高效率 + +### 技术架构 + +- **框架**: clip-as-service (基于 Jina) +- **模型**: CN-CLIP/ViT-L-14-336(默认) +- **协议**: gRPC(默认,官方推荐) +- **运行时**: PyTorch + +## 启动服务 + +### 基本用法 + +```bash +./scripts/start_cnclip_service.sh +``` + +### 启动参数 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `--port PORT` | 服务端口 | 51000 | +| `--device DEVICE` | 设备类型:cuda 或 cpu | 自动检测 | +| `--batch-size SIZE` | 批处理大小 | 32 | +| `--num-workers NUM` | 预处理线程数 | 4 | +| `--dtype TYPE` | 数据类型:float16 或 float32 | float16 | +| `--model-name NAME` | 模型名称 | CN-CLIP/ViT-L-14-336 | +| `--replicas NUM` | 副本数 | 1 | + +### 示例 + +```bash +# 使用默认配置启动 +./scripts/start_cnclip_service.sh + +# 指定端口和设备 +./scripts/start_cnclip_service.sh --port 52000 --device cpu + +# 使用其他模型 +./scripts/start_cnclip_service.sh --model-name CN-CLIP/ViT-H-14 +``` + +### 停止服务 + +```bash +./scripts/stop_cnclip_service.sh +``` + +## API 接口说明 + +### Python 客户端 + +服务使用 gRPC 协议,必须使用 Python 客户端: + +```python +from clip_client import Client + +# 创建客户端(使用 grpc:// 协议) +c = Client('grpc://localhost:51000') +``` + +### 编码接口 + +#### 1. 文本编码 + +```python +from clip_client import Client + +c = Client('grpc://localhost:51000') + +# 编码单个文本 +result = c.encode(['这是测试文本']) +print(result.shape) # (1, 1024) + +# 编码多个文本 +result = c.encode(['文本1', '文本2', '文本3']) +print(result.shape) # (3, 1024) +``` + +#### 2. 图像编码 + +```python +# 编码远程图像 URL +result = c.encode(['https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg']) +print(result.shape) # (1, 1024) + +# 编码本地图像文件 +result = c.encode(['/path/to/image.jpg']) +print(result.shape) # (1, 1024) +``` + +#### 3. 混合编码 + +```python +# 同时编码文本和图像 +result = c.encode([ + '这是文本', + 'https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg', + '另一个文本' +]) +print(result.shape) # (3, 1024) +``` + +### 返回格式 + +- **类型**: `numpy.ndarray` +- **形状**: `(N, 1024)`,其中 N 是输入数量 +- **数据类型**: `float32` +- **维度**: 1024(CN-CLIP 模型的 embedding 维度) + +### 支持的模型 + +| 模型名称 | 说明 | 推荐场景 | +|---------|------|---------| +| `CN-CLIP/ViT-B-16` | 基础版本,速度快 | 对速度要求高的场景 | +| `CN-CLIP/ViT-L-14` | 平衡版本 | 通用场景 | +| `CN-CLIP/ViT-L-14-336` | 高分辨率版本(默认) | 需要处理高分辨率图像 | +| `CN-CLIP/ViT-H-14` | 大型版本,精度高 | 对精度要求高的场景 | +| `CN-CLIP/RN50` | ResNet-50 版本 | 兼容性场景 | + +## 测试 + +运行测试脚本: + +```bash +./scripts/test_cnclip_service.sh +``` + +测试脚本会验证: +- 文本编码功能 +- 图像编码功能(远程 URL) +- 混合编码功能 + +每个测试会显示 embedding 的维度和前 20 个数字。 + +## 查看日志 + +```bash +tail -f /data/tw/SearchEngine/logs/cnclip_service.log +``` + +## 常见问题 + +### 1. 服务启动失败 + +- 检查端口是否被占用:`lsof -i :51000` +- 检查 conda 环境是否正确激活 +- 查看日志文件获取详细错误信息 + +### 2. 客户端连接失败 + +确保使用正确的协议: + +```python +# 正确:使用 grpc:// +c = Client('grpc://localhost:51000') + +# 错误:不要使用 http:// +# c = Client('http://localhost:51000') # 会失败 +``` + +### 3. 编码失败 + +- 检查服务是否正常运行 +- 检查输入格式是否正确 +- 查看服务日志排查错误 + +### 4. 依赖安装 + +确保已安装必要的依赖: + +```bash +pip install clip-client +``` + +服务端依赖会在启动脚本中自动检查。 diff --git a/docs/MySQL到ES字段映射说明-业务版.md b/docs/MySQL到ES字段映射说明-业务版.md new file mode 100644 index 0000000..22b882a --- /dev/null +++ b/docs/MySQL到ES字段映射说明-业务版.md @@ -0,0 +1,570 @@ +# MySQL 到 Elasticsearch 字段映射说明(业务版) + +## 1. 概述 + +本文档说明 Elasticsearch 中的商品文档(Document)的各个字段是如何从 MySQL 数据库中的相关表获取和转换的。 + +### 1.1 数据源表 + +- **SPU 表** (`shoplazza_product_spu`): 商品基本信息 +- **SKU 表** (`shoplazza_product_sku`): 商品变体信息(多款式) +- **Option 表** (`shoplazza_product_option`): 选项名称定义(如:颜色、尺寸) +- **类目表** (`product_category`): 类目信息(通过 SPU 表的 category_id 关联) + +### 1.2 数据关系 + +``` +SPU (1) ──→ (N) SKU # 一个商品有多个变体 +SPU (1) ──→ (N) Option # 一个商品有多个选项维度 +SPU.category_id ──→ 类目表 # 商品关联类目 +SKU.option1/2/3 ──→ Option # SKU 的选项值对应 Option 的 position +``` + +--- + +## 2. 基础字段映射 + +### 2.1 标识字段 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `tenant_id` | SPU 表 | `tenant_id` | 直接使用,转为字符串 | +| `spu_id` | SPU 表 | `id` | 直接使用,转为字符串 | + +### 2.2 文本字段(多语言) + +文本字段支持中英文双语,根据租户配置自动翻译。 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `title_zh` | SPU 表 | `title` | 如果主语言是中文,直接使用;否则翻译为中文 | +| `title_en` | SPU 表 | `title` | 如果主语言是英文,直接使用;否则翻译为英文 | +| `brief_zh` | SPU 表 | `brief` | 同上 | +| `brief_en` | SPU 表 | `brief` | 同上 | +| `description_zh` | SPU 表 | `description` | 同上 | +| `description_en` | SPU 表 | `description` | 同上 | +| `vendor_zh` | SPU 表 | `vendor` | 同上 | +| `vendor_en` | SPU 表 | `vendor` | 同上 | + +**翻译规则:** +- 根据租户配置的 `primary_language` 确定主语言 +- 如果配置了 `translate_to_en=true`,且主语言不是英文,则翻译为英文 +- 如果配置了 `translate_to_zh=true`,且主语言不是中文,则翻译为中文 + +### 2.3 标签字段 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `tags` | SPU 表 | `tags` | 逗号分隔的字符串 → 数组
示例:`"标签1,标签2"` → `["标签1", "标签2"]` | + +### 2.4 图片字段 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `image_url` | SPU 表 | `image_src` | 直接使用,如果 URL 不以 `http` 开头则添加 `//` 前缀 | + +### 2.5 销量字段 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `sales` | SPU 表 | `fake_sales` | 转为整数,如果为空则为 0 | + +### 2.6 时间字段 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `create_time` | SPU 表 | `create_time` | 转为 ISO 格式字符串(如:`2024-01-01T00:00:00`) | +| `update_time` | SPU 表 | `update_time` | 转为 ISO 格式字符串 | + +--- + +## 3. 类别(Category)字段映射 + +### 3.1 类别数据来源 + +类别信息主要来自 SPU 表的以下字段: +- `category_path`: 类目路径,逗号分隔的类目ID列表(如:`"1,2,3"`) +- `category`: 类目名称(备用字段) +- `category_id`: 当前类目ID +- `category_level`: 类目层级 + +### 3.2 类别映射流程 + +**步骤 1:解析 category_path** + +从 SPU 表的 `category_path` 字段解析出类目ID列表: + +``` +category_path = "1,2,3" +→ category_ids = ["1", "2", "3"] +``` + +**步骤 2:ID 转名称** + +通过预加载的类目映射表(从 SPU 表查询 `category_id` 和 `category` 字段构建),将ID转换为名称: + +``` +映射表:{"1": "电子产品", "2": "手机", "3": "iPhone"} +category_ids = ["1", "2", "3"] +→ category_names = ["电子产品", "手机", "iPhone"] +``` + +**步骤 3:构建 ES 字段** + +| ES 字段 | 数据来源 | 转换说明 | +|---------|----------|----------| +| `category_path_zh` | 类目名称列表 | 用 `/` 连接:`"电子产品/手机/iPhone"` | +| `category1_name` | 类目名称列表[0] | 一级类目:`"电子产品"` | +| `category2_name` | 类目名称列表[1] | 二级类目:`"手机"` | +| `category3_name` | 类目名称列表[2] | 三级类目:`"iPhone"` | +| `category_id` | SPU 表 | `category_id` 转为字符串 | +| `category_level` | SPU 表 | `category_level` 转为整数 | + +**备用处理:** + +如果 `category_path` 为空,使用 `category` 字段作为备选: +- 如果 `category` 包含 `/`,按 `/` 分割为多级类目 +- 否则,直接作为 `category1_name` + +**数据质量检查:** + +如果 `category_path` 中的类目ID在映射表中不存在,则不写入任何类目字段(视为没有类目)。 + +--- + +## 4. 多款式(SKU/Options)字段映射 + +### 4.1 SKU 嵌套结构 + +一个 SPU 可以有多个 SKU,每个 SKU 代表一个商品变体(如:红色-L码、蓝色-M码)。 + +**ES 中的 `skus` 字段结构:** + +```json +{ + "skus": [ + { + "sku_id": "123", + "price": 99.99, + "compare_at_price": 129.99, + "sku_code": "SKU001", + "stock": 100, + "weight": 0.5, + "weight_unit": "kg", + "option1_value": "红色", + "option2_value": "L", + "option3_value": "棉", + "image_src": "https://..." + } + ] +} +``` + +### 4.2 SKU 字段映射 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `skus[].sku_id` | SKU 表 | `id` | 转为字符串 | +| `skus[].price` | SKU 表 | `price` | 转为浮点数 | +| `skus[].compare_at_price` | SKU 表 | `compare_at_price` | 转为浮点数 | +| `skus[].sku_code` | SKU 表 | `sku` | 转为字符串 | +| `skus[].stock` | SKU 表 | `inventory_quantity` | 转为整数 | +| `skus[].weight` | SKU 表 | `weight` | 转为浮点数 | +| `skus[].weight_unit` | SKU 表 | `weight_unit` | 转为字符串 | +| `skus[].image_src` | SKU 表 | `image_src` | 转为字符串 | +| `skus[].option1_value` | SKU 表 | `option1` | 转为字符串 | +| `skus[].option2_value` | SKU 表 | `option2` | 转为字符串 | +| `skus[].option3_value` | SKU 表 | `option3` | 转为字符串 | + +### 4.3 选项名称字段 + +选项名称来自 Option 表,按 `position` 字段排序(1、2、3 对应 option1、option2、option3)。 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `option1_name` | Option 表 | `name` (where position=1) | 第一个选项的名称(如:"颜色") | +| `option2_name` | Option 表 | `name` (where position=2) | 第二个选项的名称(如:"尺寸") | +| `option3_name` | Option 表 | `name` (where position=3) | 第三个选项的名称(如:"材质") | + +**查询逻辑:** + +```sql +SELECT position, name +FROM shoplazza_product_option +WHERE spu_id = ? AND deleted = 0 +ORDER BY position +``` + +### 4.4 选项值字段 + +选项值来自所有 SKU 的 `option1`、`option2`、`option3` 字段,去重后形成列表。 + +| ES 字段 | 数据来源表 | 表中字段 | 转换说明 | +|---------|-----------|----------|----------| +| `option1_values` | SKU 表 | `option1` | 收集所有 SKU 的 option1 值,去重
示例:`["红色", "蓝色", "绿色"]` | +| `option2_values` | SKU 表 | `option2` | 收集所有 SKU 的 option2 值,去重
示例:`["S", "M", "L"]` | +| `option3_values` | SKU 表 | `option3` | 收集所有 SKU 的 option3 值,去重
示例:`["棉", "涤纶"]` | + +**转换逻辑:** + +``` +遍历所有 SKU: + - 收集 option1 值 → option1_values 列表 + - 收集 option2 值 → option2_values 列表 + - 收集 option3 值 → option3_values 列表 +去重后写入 ES +``` + +**注意:** 只有配置在 `searchable_option_dimensions` 中的选项才会被索引。 + +### 4.5 规格(Specifications)字段 + +规格字段用于支持按规格过滤和分面搜索,将 SKU 的选项值结构化存储。 + +**ES 中的 `specifications` 字段结构:** + +```json +{ + "specifications": [ + {"sku_id": "123", "name": "颜色", "value": "红色"}, + {"sku_id": "123", "name": "尺寸", "value": "L"}, + {"sku_id": "124", "name": "颜色", "value": "蓝色"}, + {"sku_id": "124", "name": "尺寸", "value": "M"} + ] +} +``` + +**构建逻辑:** + +1. 从 Option 表获取选项名称映射: + ``` + position=1 → name="颜色" + position=2 → name="尺寸" + position=3 → name="材质" + ``` + +2. 遍历所有 SKU,为每个 SKU 的每个选项值构建规格记录: + ``` + SKU.id=123, option1="红色", option2="L" + → {"sku_id": "123", "name": "颜色", "value": "红色"} + → {"sku_id": "123", "name": "尺寸", "value": "L"} + ``` + +| ES 字段 | 数据来源 | 转换说明 | +|---------|----------|----------| +| `specifications[].sku_id` | SKU 表 | `id` 转为字符串 | +| `specifications[].name` | Option 表 | `name`(根据 position 匹配) | +| `specifications[].value` | SKU 表 | `option1/2/3` 转为字符串 | + +### 4.6 价格聚合字段 + +从所有 SKU 的价格中计算聚合值。 + +| ES 字段 | 数据来源 | 转换说明 | +|---------|----------|----------| +| `min_price` | SKU 表 | 所有 SKU 的 `price` 最小值 | +| `max_price` | SKU 表 | 所有 SKU 的 `price` 最大值 | +| `compare_at_price` | SKU 表 | 所有 SKU 的 `compare_at_price` 最大值 | +| `sku_prices` | SKU 表 | 所有 SKU 的 `price` 数组
示例:`[99.99, 99.99, 129.99]` | + +**计算逻辑:** + +``` +遍历所有 SKU: + - 收集 price → prices 列表 + - 收集 compare_at_price → compare_prices 列表 + +min_price = min(prices) +max_price = max(prices) +compare_at_price = max(compare_prices) +sku_prices = prices 列表 +``` + +### 4.7 库存和重量聚合字段 + +| ES 字段 | 数据来源 | 转换说明 | +|---------|----------|----------| +| `total_inventory` | SKU 表 | 所有 SKU 的 `inventory_quantity` 求和 | +| `sku_weights` | SKU 表 | 所有 SKU 的 `weight` 数组
示例:`[500, 500, 600]` | +| `sku_weight_units` | SKU 表 | 所有 SKU 的 `weight_unit` 去重后的列表
示例:`["kg"]` | + +**计算逻辑:** + +``` +遍历所有 SKU: + - 累加 inventory_quantity → total_inventory + - 收集 weight → sku_weights 列表 + - 收集 weight_unit → sku_weight_units 列表 + +total_inventory = 所有 SKU 库存之和 +sku_weights = 所有 SKU 重量数组 +sku_weight_units = 去重后的重量单位列表 +``` + +--- + +## 5. 向量字段映射 + +### 5.1 标题向量(title_embedding) + +| ES 字段 | 数据来源 | 转换说明 | +|---------|----------|----------| +| `title_embedding` | SPU 表 `title` | 使用文本编码器(BGE)将标题转换为 1024 维向量
优先使用 `title_en`,如果没有则使用 `title_zh` | + +**生成逻辑:** + +``` +如果启用向量搜索: + 文本 = title_en 或 title_zh + 向量 = 文本编码器.encode(文本) + title_embedding = 向量(1024 维浮点数组) +``` + +--- + +## 6. 完整字段映射表 + +### 6.1 字段来源汇总 + +| ES 字段 | 数据来源表 | 表中字段 | 说明 | +|---------|-----------|----------|------| +| **基础字段** | +| `tenant_id` | SPU | `tenant_id` | 租户ID | +| `spu_id` | SPU | `id` | 商品ID | +| `title_zh/en` | SPU | `title` | 标题(多语言) | +| `brief_zh/en` | SPU | `brief` | 简介(多语言) | +| `description_zh/en` | SPU | `description` | 描述(多语言) | +| `vendor_zh/en` | SPU | `vendor` | 品牌(多语言) | +| `tags` | SPU | `tags` | 标签数组 | +| `image_url` | SPU | `image_src` | 主图URL | +| `sales` | SPU | `fake_sales` | 销量 | +| `create_time` | SPU | `create_time` | 创建时间 | +| `update_time` | SPU | `update_time` | 更新时间 | +| **类别字段** | +| `category_path_zh` | SPU + 类目映射 | `category_path` → 类目名称 | 类目路径 | +| `category1_name` | SPU + 类目映射 | `category_path` → 类目名称[0] | 一级类目 | +| `category2_name` | SPU + 类目映射 | `category_path` → 类目名称[1] | 二级类目 | +| `category3_name` | SPU + 类目映射 | `category_path` → 类目名称[2] | 三级类目 | +| `category_id` | SPU | `category_id` | 类目ID | +| `category_level` | SPU | `category_level` | 类目层级 | +| **SKU 嵌套字段** | +| `skus[]` | SKU | 多个字段 | SKU 数组(见 4.2 节) | +| **选项字段** | +| `option1_name` | Option | `name` (position=1) | 选项1名称 | +| `option2_name` | Option | `name` (position=2) | 选项2名称 | +| `option3_name` | Option | `name` (position=3) | 选项3名称 | +| `option1_values` | SKU | `option1` | 选项1值列表 | +| `option2_values` | SKU | `option2` | 选项2值列表 | +| `option3_values` | SKU | `option3` | 选项3值列表 | +| **规格字段** | +| `specifications[]` | SKU + Option | `option1/2/3` + `name` | 规格数组 | +| **聚合字段** | +| `min_price` | SKU | `price` | 最低价格 | +| `max_price` | SKU | `price` | 最高价格 | +| `compare_at_price` | SKU | `compare_at_price` | 最高原价 | +| `sku_prices` | SKU | `price` | 所有价格数组 | +| `total_inventory` | SKU | `inventory_quantity` | 总库存 | +| `sku_weights` | SKU | `weight` | 所有重量数组 | +| `sku_weight_units` | SKU | `weight_unit` | 重量单位列表 | +| **向量字段** | +| `title_embedding` | SPU | `title` | 标题向量(1024维) | + +--- + +## 7. 数据查询示例 + +### 7.1 查询 SPU 数据 + +```sql +SELECT + id, tenant_id, title, brief, description, vendor, + category, category_id, category_path, category_level, + tags, image_src, fake_sales, create_time, update_time +FROM shoplazza_product_spu +WHERE tenant_id = ? AND id = ? AND deleted = 0 +``` + +### 7.2 查询 SKU 数据 + +```sql +SELECT + id, spu_id, price, compare_at_price, sku, + inventory_quantity, weight, weight_unit, + option1, option2, option3, image_src +FROM shoplazza_product_sku +WHERE tenant_id = ? AND spu_id = ? AND deleted = 0 +``` + +### 7.3 查询 Option 数据 + +```sql +SELECT + position, name +FROM shoplazza_product_option +WHERE tenant_id = ? AND spu_id = ? AND deleted = 0 +ORDER BY position +``` + +### 7.4 查询类目映射 + +```sql +SELECT DISTINCT + category_id, category +FROM shoplazza_product_spu +WHERE deleted = 0 AND category_id IS NOT NULL +``` + +--- + +## 8. ES 文档示例 + +```json +{ + "tenant_id": "1", + "spu_id": "12345", + "title_zh": "iPhone 15 Pro Max", + "title_en": "iPhone 15 Pro Max", + "brief_zh": "最新款 iPhone", + "brief_en": "Latest iPhone", + "description_zh": "详细描述...", + "description_en": "Detailed description...", + "vendor_zh": "Apple", + "vendor_en": "Apple", + "tags": ["手机", "智能手机", "Apple"], + "category_path_zh": "电子产品/手机/iPhone", + "category1_name": "电子产品", + "category2_name": "手机", + "category3_name": "iPhone", + "category_id": "3", + "category_level": 3, + "option1_name": "颜色", + "option2_name": "存储容量", + "option1_values": ["深空黑色", "原色钛金属", "白色钛金属"], + "option2_values": ["256GB", "512GB", "1TB"], + "image_url": "https://example.com/image.jpg", + "sales": 1000, + "min_price": 8999.0, + "max_price": 12999.0, + "compare_at_price": 12999.0, + "sku_prices": [8999.0, 10999.0, 12999.0], + "sku_weights": [221, 221, 221], + "sku_weight_units": ["g"], + "total_inventory": 500, + "create_time": "2024-01-01T00:00:00", + "update_time": "2024-01-15T10:30:00", + "title_embedding": [0.123, 0.456, ...], + "skus": [ + { + "sku_id": "1001", + "price": 8999.0, + "compare_at_price": 9999.0, + "sku_code": "IP15PM-256-BLK", + "stock": 100, + "weight": 221.0, + "weight_unit": "g", + "option1_value": "深空黑色", + "option2_value": "256GB", + "image_src": "https://example.com/sku1.jpg" + } + ], + "specifications": [ + {"sku_id": "1001", "name": "颜色", "value": "深空黑色"}, + {"sku_id": "1001", "name": "存储容量", "value": "256GB"} + ] +} +``` + +--- + +## 9. 关键映射关系总结 + +### 9.1 类别映射 + +``` +SPU.category_path ("1,2,3") + ↓ [解析ID列表] +category_ids ["1", "2", "3"] + ↓ [通过映射表转换] +category_names ["电子产品", "手机", "iPhone"] + ↓ [构建字段] +category_path_zh: "电子产品/手机/iPhone" +category1_name: "电子产品" +category2_name: "手机" +category3_name: "iPhone" +``` + +### 9.2 SKU 和选项映射 + +``` +Option 表 (position=1, name="颜色") + ↓ +option1_name: "颜色" + +SKU 表 (option1="红色", option1="蓝色") + ↓ [收集所有值,去重] +option1_values: ["红色", "蓝色"] + +SKU 表 + Option 表 + ↓ [组合构建] +specifications: [ + {name: "颜色", value: "红色", sku_id: "123"}, + {name: "颜色", value: "蓝色", sku_id: "124"} +] +``` + +### 9.3 价格聚合 + +``` +SKU 表 (price: 99.99, 109.99, 129.99) + ↓ [聚合计算] +min_price: 99.99 +max_price: 129.99 +sku_prices: [99.99, 109.99, 129.99] +``` + +--- + +## 附录:数据表字段对照 + +### SPU 表主要字段 + +| 表字段 | ES 字段 | 说明 | +|--------|---------|------| +| `id` | `spu_id` | 商品ID | +| `tenant_id` | `tenant_id` | 租户ID | +| `title` | `title_zh/en` | 标题 | +| `brief` | `brief_zh/en` | 简介 | +| `description` | `description_zh/en` | 描述 | +| `vendor` | `vendor_zh/en` | 品牌 | +| `tags` | `tags` | 标签 | +| `category_path` | `category_path_zh` | 类目路径 | +| `category_id` | `category_id` | 类目ID | +| `category_level` | `category_level` | 类目层级 | +| `image_src` | `image_url` | 主图 | +| `fake_sales` | `sales` | 销量 | +| `create_time` | `create_time` | 创建时间 | +| `update_time` | `update_time` | 更新时间 | + +### SKU 表主要字段 + +| 表字段 | ES 字段 | 说明 | +|--------|---------|------| +| `id` | `skus[].sku_id` | SKU ID | +| `price` | `skus[].price`, `min_price`, `max_price`, `sku_prices` | 价格 | +| `compare_at_price` | `skus[].compare_at_price`, `compare_at_price` | 原价 | +| `sku` | `skus[].sku_code` | SKU编码 | +| `inventory_quantity` | `skus[].stock`, `total_inventory` | 库存 | +| `weight` | `skus[].weight`, `sku_weights` | 重量 | +| `weight_unit` | `skus[].weight_unit`, `sku_weight_units` | 重量单位 | +| `option1` | `skus[].option1_value`, `option1_values`, `specifications[].value` | 选项1值 | +| `option2` | `skus[].option2_value`, `option2_values`, `specifications[].value` | 选项2值 | +| `option3` | `skus[].option3_value`, `option3_values`, `specifications[].value` | 选项3值 | +| `image_src` | `skus[].image_src` | SKU图片 | + +### Option 表主要字段 + +| 表字段 | ES 字段 | 说明 | +|--------|---------|------| +| `position` | - | 位置(1/2/3) | +| `name` | `option1_name`, `option2_name`, `option3_name`, `specifications[].name` | 选项名称 | + diff --git a/docs/MySQL到ES文档映射说明.md b/docs/MySQL到ES文档映射说明.md new file mode 100644 index 0000000..1a40544 --- /dev/null +++ b/docs/MySQL到ES文档映射说明.md @@ -0,0 +1,961 @@ +# MySQL 到 Elasticsearch 文档映射说明 + +## 1. 概述 + +本文档详细说明从 MySQL 数据库到 Elasticsearch 的完整数据转换流程,包括 SQL 查询逻辑、字段映射规则、以及复杂字段(类别、多款式)的处理方法。 + +### 1.1 数据流转架构 + +``` +MySQL 数据库 + ↓ +[SQL 查询] → SPU表、SKU表、Option表 + ↓ +[数据加载] → Pandas DataFrame + ↓ +[文档转换] → ES 文档字典 + ↓ +[批量写入] → Elasticsearch +``` + +### 1.2 核心组件 + +- **IncrementalIndexerService**: 增量索引服务,负责数据获取和索引 +- **SPUDocumentTransformer**: 文档转换器,负责将数据库记录转换为 ES 文档 +- **BulkIndexer**: 批量写入器,负责将文档批量写入 ES + +--- + +## 2. 数据源表结构 + +### 2.1 SPU 表(shoplazza_product_spu) + +SPU(Standard Product Unit)表存储商品的基本信息。 + +**主要字段:** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `id` | BIGINT | SPU主键ID | +| `tenant_id` | BIGINT | 租户ID | +| `title` | VARCHAR(500) | 商品标题 | +| `brief` | VARCHAR(1000) | 商品简介 | +| `description` | TEXT | 商品详细描述 | +| `vendor` | VARCHAR(255) | 供应商/品牌 | +| `category` | VARCHAR(255) | 类目名称 | +| `category_id` | BIGINT | 类目ID | +| `category_path` | VARCHAR(500) | 类目路径(逗号分隔的ID列表) | +| `category_level` | INT | 类目层级 | +| `tags` | TEXT | 标签(逗号分隔) | +| `image_src` | VARCHAR(500) | 主图URL | +| `fake_sales` | INT | 假销量 | +| `create_time` | DATETIME | 创建时间 | +| `update_time` | DATETIME | 更新时间 | +| `deleted` | BIT(1) | 删除标记 | + +### 2.2 SKU 表(shoplazza_product_sku) + +SKU(Stock Keeping Unit)表存储商品的变体信息,一个 SPU 可以有多个 SKU。 + +**主要字段:** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `id` | BIGINT | SKU主键ID | +| `spu_id` | BIGINT | 关联的SPU ID | +| `tenant_id` | BIGINT | 租户ID | +| `sku` | VARCHAR(100) | SKU编码 | +| `price` | DECIMAL(10,2) | 价格 | +| `compare_at_price` | DECIMAL(10,2) | 对比价格(原价) | +| `inventory_quantity` | INT | 库存数量 | +| `weight` | DECIMAL(10,2) | 重量 | +| `weight_unit` | VARCHAR(10) | 重量单位 | +| `option1` | VARCHAR(255) | 选项1的值(如:颜色-红色) | +| `option2` | VARCHAR(255) | 选项2的值(如:尺寸-L) | +| `option3` | VARCHAR(255) | 选项3的值(如:材质-棉) | +| `image_src` | VARCHAR(500) | SKU图片URL | +| `deleted` | BIT(1) | 删除标记 | + +### 2.3 Option 表(shoplazza_product_option) + +Option 表存储选项的名称定义,用于定义 SKU 的选项维度(如:颜色、尺寸、材质)。 + +**主要字段:** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `id` | BIGINT | Option主键ID | +| `spu_id` | BIGINT | 关联的SPU ID | +| `tenant_id` | BIGINT | 租户ID | +| `position` | INT | 位置序号(1、2、3对应option1、option2、option3) | +| `name` | VARCHAR(255) | 选项名称(如:"颜色"、"尺寸") | +| `values` | TEXT | 选项值列表(JSON格式) | +| `deleted` | BIT(1) | 删除标记 | + +--- + +## 3. SQL 查询逻辑 + +### 3.1 批量查询 SPU 数据 + +**方法:** `_load_spus_for_spu_ids(tenant_id, spu_ids, include_deleted=True)` + +**SQL 查询:** + +```sql +SELECT + id, shop_id, shoplazza_id, title, brief, description, + spu, vendor, vendor_url, + image_src, image_width, image_height, image_path, image_alt, + tags, note, category, category_id, category_google_id, + category_level, category_path, + fake_sales, display_fake_sales, + tenant_id, creator, create_time, updater, update_time, deleted +FROM shoplazza_product_spu +WHERE tenant_id = :tenant_id + AND id IN :spu_ids + [AND deleted = 0] -- 如果 include_deleted=False +``` + +**查询特点:** +- 使用 `IN` 子句批量查询多个 SPU +- 支持查询已删除的记录(用于检测删除状态) +- 使用 SQLAlchemy 的 `bindparam` 和 `expanding=True` 处理动态 IN 列表 + +**代码位置:** `indexer/incremental_service.py:223-262` + +### 3.2 批量查询 SKU 数据 + +**方法:** `_load_skus_for_spu_ids(tenant_id, spu_ids)` + +**SQL 查询:** + +```sql +SELECT + id, spu_id, shop_id, shoplazza_id, shoplazza_product_id, + shoplazza_image_id, title, sku, barcode, position, + price, compare_at_price, cost_price, + option1, option2, option3, + inventory_quantity, weight, weight_unit, image_src, + wholesale_price, note, extend, + shoplazza_created_at, shoplazza_updated_at, tenant_id, + creator, create_time, updater, update_time, deleted +FROM shoplazza_product_sku +WHERE tenant_id = :tenant_id + AND deleted = 0 + AND spu_id IN :spu_ids +``` + +**查询特点:** +- 批量查询多个 SPU 的所有 SKU +- 只查询未删除的 SKU(`deleted = 0`) +- 结果按 `spu_id` 分组处理 + +**代码位置:** `indexer/incremental_service.py:264-288` + +### 3.3 批量查询 Option 数据 + +**方法:** `_load_options_for_spu_ids(tenant_id, spu_ids)` + +**SQL 查询:** + +```sql +SELECT + id, spu_id, shop_id, shoplazza_id, shoplazza_product_id, + position, name, `values`, tenant_id, + creator, create_time, updater, update_time, deleted +FROM shoplazza_product_option +WHERE tenant_id = :tenant_id + AND deleted = 0 + AND spu_id IN :spu_ids +ORDER BY spu_id, position +``` + +**查询特点:** +- 批量查询多个 SPU 的所有 Option +- 按 `spu_id` 和 `position` 排序,确保选项顺序一致 +- `position` 字段对应 SKU 表中的 `option1`、`option2`、`option3` + +**代码位置:** `indexer/incremental_service.py:290-310` + +### 3.4 分类映射加载 + +**方法:** `load_category_mapping(db_engine)` + +**SQL 查询:** + +```sql +SELECT DISTINCT + category_id, + category +FROM shoplazza_product_spu +WHERE deleted = 0 AND category_id IS NOT NULL +``` + +**查询特点:** +- 全局加载所有租户的分类映射(category_id → category_name) +- 在服务初始化时预加载,避免重复查询 +- 用于将 `category_path` 中的 ID 转换为名称 + +**代码位置:** `indexer/indexing_utils.py:17-51` + +--- + +## 4. 字段映射详细说明 + +### 4.1 基础字段映射 + +#### 4.1.1 标识字段 + +| ES 字段 | 数据来源 | 转换逻辑 | +|---------|----------|----------| +| `tenant_id` | `spu_row['tenant_id']` | 直接转换为字符串:`str(tenant_id)` | +| `spu_id` | `spu_row['id']` | 直接转换为字符串:`str(spu_id)` | + +#### 4.1.2 文本字段(多语言处理) + +文本字段支持中英文双语,根据租户配置进行自动翻译。 + +**字段列表:** +- `title_zh` / `title_en` +- `brief_zh` / `brief_en` +- `description_zh` / `description_en` +- `vendor_zh` / `vendor_en` + +**填充逻辑:** + +1. **获取租户主语言**:从 `tenant_config` 读取 `primary_language`(默认 'zh') + +2. **翻译处理**: + - 如果配置了 `translate_to_en=true` 且主语言不是 'en',则翻译到英文 + - 如果配置了 `translate_to_zh=true` 且主语言不是 'zh',则翻译到中文 + - 使用 `Translator.translate_for_indexing()` 方法进行翻译 + +3. **字段填充**: + ```python + # 示例:title 字段 + if pd.notna(spu_row.get('title')): + title_text = str(spu_row['title']) + if self.translator: + translations = self.translator.translate_for_indexing( + title_text, + shop_language=primary_lang, + source_lang=primary_lang, + translate_to_en=translate_to_en, + translate_to_zh=translate_to_zh, + ) + doc['title_zh'] = translations.get('zh') or (title_text if primary_lang == 'zh' else None) + doc['title_en'] = translations.get('en') or (title_text if primary_lang == 'en' else None) + ``` + +**代码位置:** `indexer/document_transformer.py:168-298` + +#### 4.1.3 标签字段 + +| ES 字段 | 数据来源 | 转换逻辑 | +|---------|----------|----------| +| `tags` | `spu_row['tags']` | 逗号分隔字符串 → 列表:`[tag.strip() for tag in tags_str.split(',') if tag.strip()]` | + +**示例:** +- 数据库:`"标签1,标签2,标签3"` +- ES:`["标签1", "标签2", "标签3"]` + +#### 4.1.4 图片字段 + +| ES 字段 | 数据来源 | 转换逻辑 | +|---------|----------|----------| +| `image_url` | `spu_row['image_src']` | 如果 URL 不以 `http` 开头,添加 `//` 前缀 | + +**代码位置:** `indexer/document_transformer.py:390-396` + +#### 4.1.5 销量字段 + +| ES 字段 | 数据来源 | 转换逻辑 | +|---------|----------|----------| +| `sales` | `spu_row['fake_sales']` | 转换为整数,如果为空或转换失败则设为 0 | + +#### 4.1.6 时间字段 + +| ES 字段 | 数据来源 | 转换逻辑 | +|---------|----------|----------| +| `create_time` | `spu_row['create_time']` | 转换为 ISO 格式字符串:`datetime.isoformat()` | +| `update_time` | `spu_row['update_time']` | 转换为 ISO 格式字符串:`datetime.isoformat()` | + +--- + +### 4.2 类别(Category)字段映射 + +类别字段是复杂字段,涉及多级分类的解析和映射。 + +#### 4.2.1 类别数据来源 + +类别信息主要来自 SPU 表的以下字段: +- `category_path`: 类目路径,逗号分隔的类目ID列表(如:`"1,2,3"`) +- `category`: 类目名称(备用字段) +- `category_id`: 当前类目ID +- `category_level`: 类目层级 + +#### 4.2.2 类别映射流程 + +**步骤 1:加载分类映射** + +在服务初始化时,从数据库加载全局分类映射: + +```python +category_id_to_name = load_category_mapping(db_engine) +# 结果:{"1": "电子产品", "2": "手机", "3": "iPhone"} +``` + +**步骤 2:解析 category_path** + +```python +if pd.notna(spu_row.get('category_path')): + category_path = str(spu_row['category_path']) # 例如:"1,2,3" + category_ids = [cid.strip() for cid in category_path.split(',') if cid.strip()] + # 结果:["1", "2", "3"] +``` + +**步骤 3:ID 转名称** + +```python +category_names = [] +missing_ids = [] +for cid in category_ids: + if cid in self.category_id_to_name: + category_names.append(self.category_id_to_name[cid]) + else: + missing_ids.append(cid) +``` + +**步骤 4:数据质量检查** + +如果存在缺失的类目ID(在映射中找不到),则: +- 记录错误日志 +- **不写入任何类目字段**(视为没有类目) +- 不阻塞索引流程 + +**步骤 5:填充 ES 字段** + +```python +if category_names: + # 构建类目路径字符串 + category_path_str = '/'.join(category_names) + doc['category_path_zh'] = category_path_str # 例如:"电子产品/手机/iPhone" + + # 填充分层类目名称 + if len(category_names) > 0: + doc['category1_name'] = category_names[0] # "电子产品" + if len(category_names) > 1: + doc['category2_name'] = category_names[1] # "手机" + if len(category_names) > 2: + doc['category3_name'] = category_names[2] # "iPhone" +``` + +**步骤 6:备用处理(category_path 为空时)** + +如果 `category_path` 为空,使用 `category` 字段作为备选: + +```python +elif pd.notna(spu_row.get('category')): + category = str(spu_row['category']) + doc['category_name_zh'] = category + + # 尝试从 category 字段解析多级分类(如果包含 "/") + if '/' in category: + path_parts = category.split('/') + if len(path_parts) > 0: + doc['category1_name'] = path_parts[0].strip() + if len(path_parts) > 1: + doc['category2_name'] = path_parts[1].strip() + if len(path_parts) > 2: + doc['category3_name'] = path_parts[2].strip() +``` + +#### 4.2.3 类别相关 ES 字段 + +| ES 字段 | 类型 | 说明 | +|---------|------|------| +| `category_path_zh` | text | 类目路径字符串(如:"电子产品/手机/iPhone") | +| `category_path_en` | text | 类目路径英文(暂未实现) | +| `category1_name` | keyword | 一级类目名称 | +| `category2_name` | keyword | 二级类目名称 | +| `category3_name` | keyword | 三级类目名称 | +| `category_id` | keyword | 当前类目ID | +| `category_level` | integer | 类目层级 | + +**代码位置:** `indexer/document_transformer.py:300-376` + +--- + +### 4.3 多款式(SKU/Options)字段映射 + +多款式字段是最复杂的部分,涉及 SKU 嵌套结构、选项值提取、规格构建等。 + +#### 4.3.1 SKU 数据结构 + +一个 SPU 可以有多个 SKU,每个 SKU 代表一个商品变体(如:红色-L码、蓝色-M码)。 + +**ES 中的 SKU 嵌套结构:** + +```json +{ + "skus": [ + { + "sku_id": "123", + "price": 99.99, + "compare_at_price": 129.99, + "sku_code": "SKU001", + "stock": 100, + "weight": 0.5, + "weight_unit": "kg", + "option1_value": "红色", + "option2_value": "L", + "option3_value": "棉", + "image_src": "https://..." + }, + { + "sku_id": "124", + "price": 99.99, + "compare_at_price": 129.99, + "sku_code": "SKU002", + "stock": 50, + "weight": 0.5, + "weight_unit": "kg", + "option1_value": "蓝色", + "option2_value": "M", + "option3_value": "棉", + "image_src": "https://..." + } + ] +} +``` + +#### 4.3.2 SKU 处理流程 + +**步骤 1:构建 Option 名称映射** + +从 Option 表获取选项名称: + +```python +option_name_map = {} +if not options.empty: + for _, opt_row in options.iterrows(): + position = opt_row.get('position') # 1, 2, 3 + name = opt_row.get('name') # "颜色", "尺寸", "材质" + if pd.notna(position) and pd.notna(name): + option_name_map[int(position)] = str(name) +# 结果:{1: "颜色", 2: "尺寸", 3: "材质"} +``` + +**步骤 2:转换每个 SKU** + +```python +for _, sku_row in skus.iterrows(): + sku_data = self._transform_sku_row(sku_row, option_name_map) + if sku_data: + skus_list.append(sku_data) +``` + +**SKU 转换逻辑:** + +```python +def _transform_sku_row(self, sku_row, option_name_map): + sku_data = { + 'sku_id': str(sku_row['id']), + 'price': float(sku_row['price']) if pd.notna(sku_row.get('price')) else None, + 'compare_at_price': float(sku_row['compare_at_price']) if pd.notna(sku_row.get('compare_at_price')) else None, + 'sku_code': str(sku_row['sku']) if pd.notna(sku_row.get('sku')) else None, + 'stock': int(sku_row['inventory_quantity']) if pd.notna(sku_row.get('inventory_quantity')) else 0, + 'weight': float(sku_row['weight']) if pd.notna(sku_row.get('weight')) else None, + 'weight_unit': str(sku_row['weight_unit']) if pd.notna(sku_row.get('weight_unit')) else None, + 'image_src': str(sku_row['image_src']) if pd.notna(sku_row.get('image_src')) else None, + } + + # 填充选项值 + if pd.notna(sku_row.get('option1')): + sku_data['option1_value'] = str(sku_row['option1']) + if pd.notna(sku_row.get('option2')): + sku_data['option2_value'] = str(sku_row['option2']) + if pd.notna(sku_row.get('option3')): + sku_data['option3_value'] = str(sku_row['option3']) + + return sku_data +``` + +**步骤 3:收集聚合信息** + +在处理 SKU 的同时,收集价格、重量、库存等聚合信息: + +```python +prices = [] +compare_prices = [] +sku_prices = [] +sku_weights = [] +sku_weight_units = [] +total_inventory = 0 + +for sku_data in skus_list: + if 'price' in sku_data and sku_data['price'] is not None: + prices.append(sku_data['price']) + sku_prices.append(sku_data['price']) + + if 'compare_at_price' in sku_data and sku_data['compare_at_price'] is not None: + compare_prices.append(sku_data['compare_at_price']) + + if 'weight' in sku_data and sku_data['weight'] is not None: + sku_weights.append(int(float(sku_data['weight']))) + + if 'weight_unit' in sku_data and sku_data['weight_unit']: + sku_weight_units.append(sku_data['weight_unit']) + + if 'stock' in sku_data and sku_data['stock'] is not None: + total_inventory += sku_data['stock'] +``` + +**步骤 4:填充 ES 字段** + +```python +doc['skus'] = skus_list # 嵌套 SKU 数组 + +# 价格范围 +if prices: + doc['min_price'] = float(min(prices)) + doc['max_price'] = float(max(prices)) +else: + doc['min_price'] = 0.0 + doc['max_price'] = 0.0 + +if compare_prices: + doc['compare_at_price'] = float(max(compare_prices)) + +# SKU 扁平化字段(用于过滤和聚合) +doc['sku_prices'] = sku_prices # [99.99, 99.99, 129.99] +doc['sku_weights'] = sku_weights # [500, 500, 600] +doc['sku_weight_units'] = list(set(sku_weight_units)) # ["kg"] +doc['total_inventory'] = total_inventory # 150 +``` + +**代码位置:** `indexer/document_transformer.py:398-480` + +#### 4.3.3 选项名称字段 + +从 Option 表获取选项名称,填充到 ES 文档的顶层: + +```python +def _fill_option_names(self, doc, options): + if not options.empty: + sorted_options = options.sort_values('position') + if len(sorted_options) > 0 and pd.notna(sorted_options.iloc[0].get('name')): + doc['option1_name'] = str(sorted_options.iloc[0]['name']) # "颜色" + if len(sorted_options) > 1 and pd.notna(sorted_options.iloc[1].get('name')): + doc['option2_name'] = str(sorted_options.iloc[1]['name']) # "尺寸" + if len(sorted_options) > 2 and pd.notna(sorted_options.iloc[2].get('name')): + doc['option3_name'] = str(sorted_options.iloc[2]['name']) # "材质" +``` + +**ES 字段:** +- `option1_name`: 选项1的名称(如:"颜色") +- `option2_name`: 选项2的名称(如:"尺寸") +- `option3_name`: 选项3的名称(如:"材质") + +**代码位置:** `indexer/document_transformer.py:378-388` + +#### 4.3.4 选项值字段 + +从所有 SKU 中提取选项值,去重后填充到 ES 文档: + +```python +def _fill_option_values(self, doc, skus): + option1_values = [] + option2_values = [] + option3_values = [] + + for _, sku_row in skus.iterrows(): + if pd.notna(sku_row.get('option1')): + option1_values.append(str(sku_row['option1'])) + if pd.notna(sku_row.get('option2')): + option2_values.append(str(sku_row['option2'])) + if pd.notna(sku_row.get('option3')): + option3_values.append(str(sku_row['option3'])) + + # 根据配置决定是否写入索引(searchable_option_dimensions) + if 'option1' in self.searchable_option_dimensions: + doc['option1_values'] = list(set(option1_values)) # ["红色", "蓝色", "绿色"] + else: + doc['option1_values'] = [] + + # 同样处理 option2_values 和 option3_values +``` + +**ES 字段:** +- `option1_values`: 选项1的所有值列表(如:`["红色", "蓝色", "绿色"]`) +- `option2_values`: 选项2的所有值列表(如:`["S", "M", "L"]`) +- `option3_values`: 选项3的所有值列表(如:`["棉", "涤纶"]`) + +**注意:** 只有配置在 `searchable_option_dimensions` 中的选项才会被索引。 + +**代码位置:** `indexer/document_transformer.py:482-510` + +#### 4.3.5 规格(Specifications)字段 + +规格字段用于支持按规格过滤和分面搜索,将 SKU 的选项值结构化存储。 + +**构建逻辑:** + +```python +specifications = [] + +for _, sku_row in skus.iterrows(): + sku_id = str(sku_row['id']) + + # 如果 SKU 有 option1 且 Option 表中有对应的名称 + if pd.notna(sku_row.get('option1')) and 1 in option_name_map: + specifications.append({ + 'sku_id': sku_id, + 'name': option_name_map[1], # "颜色" + 'value': str(sku_row['option1']) # "红色" + }) + + # 同样处理 option2 和 option3 + if pd.notna(sku_row.get('option2')) and 2 in option_name_map: + specifications.append({ + 'sku_id': sku_id, + 'name': option_name_map[2], # "尺寸" + 'value': str(sku_row['option2']) # "L" + }) + + if pd.notna(sku_row.get('option3')) and 3 in option_name_map: + specifications.append({ + 'sku_id': sku_id, + 'name': option_name_map[3], # "材质" + 'value': str(sku_row['option3']) # "棉" + }) + +doc['specifications'] = specifications +``` + +**ES 字段结构:** + +```json +{ + "specifications": [ + {"sku_id": "123", "name": "颜色", "value": "红色"}, + {"sku_id": "123", "name": "尺寸", "value": "L"}, + {"sku_id": "123", "name": "材质", "value": "棉"}, + {"sku_id": "124", "name": "颜色", "value": "蓝色"}, + {"sku_id": "124", "name": "尺寸", "value": "M"}, + {"sku_id": "124", "name": "材质", "value": "棉"} + ] +} +``` + +**用途:** +- 支持按规格过滤:`{"specifications": {"name": "颜色", "value": "红色"}}` +- 支持规格分面:统计每个规格值的数量 + +**代码位置:** `indexer/document_transformer.py:459-478` + +--- + +### 4.4 向量字段映射 + +#### 4.4.1 标题向量(title_embedding) + +**生成时机:** +- 在增量索引中,批量生成 embedding(性能优化) +- 在单条查询中,按需生成 + +**生成逻辑:** + +```python +# 批量生成(增量索引) +if enable_embedding and encoder and documents: + title_texts = [] + title_doc_indices = [] + for i, (_, doc) in enumerate(documents): + title_text = doc.get("title_en") or doc.get("title_zh") + if title_text and str(title_text).strip(): + title_texts.append(str(title_text)) + title_doc_indices.append(i) + + if title_texts: + embeddings = encoder.encode_batch(title_texts, batch_size=32) + for j, emb in enumerate(embeddings): + doc_idx = title_doc_indices[j] + if isinstance(emb, np.ndarray): + documents[doc_idx][1]["title_embedding"] = emb.tolist() +``` + +**ES 字段:** +- `title_embedding`: 1024 维浮点数组,用于语义搜索 + +**代码位置:** +- 批量生成:`indexer/incremental_service.py:558-576` +- 单条生成:`indexer/document_transformer.py:586-619` + +--- + +## 5. 数据转换完整流程 + +### 5.1 增量索引流程 + +**入口方法:** `IncrementalIndexerService.index_spus_to_es()` + +**流程步骤:** + +1. **参数处理** + - 去重 SPU ID 列表 + - 生成索引名称(如果未提供) + +2. **显式删除处理**(如果提供了 `delete_spu_ids`) + - 遍历 `delete_spu_ids`,从 ES 中删除对应文档 + +3. **批量加载数据** + - 调用 `_load_spus_for_spu_ids()` 批量加载 SPU 数据 + - 调用 `_load_skus_for_spu_ids()` 批量加载 SKU 数据 + - 调用 `_load_options_for_spu_ids()` 批量加载 Option 数据 + +4. **自动删除检测** + - 检查 SPU 是否在数据库中存在 + - 检查 SPU 的 `deleted` 字段 + - 如果不存在或已删除,从 ES 中删除对应文档 + +5. **数据分组** + - 将 SKU 按 `spu_id` 分组 + - 将 Option 按 `spu_id` 分组 + +6. **文档转换** + - 遍历每个 SPU + - 调用 `transformer.transform_spu_to_doc()` 转换为 ES 文档 + - 收集所有文档 + +7. **批量生成 Embedding**(如果启用) + - 收集所有文档的标题文本 + - 批量调用 `encoder.encode_batch()` 生成 embedding + - 填充到对应文档 + +8. **批量写入 ES** + - 使用 `BulkIndexer` 批量写入文档 + - 使用 `spu_id` 作为文档 ID + +9. **返回结果** + - 返回成功/失败统计 + - 返回每个 SPU 的处理状态 + +**代码位置:** `indexer/incremental_service.py:391-679` + +### 5.2 文档转换流程 + +**入口方法:** `SPUDocumentTransformer.transform_spu_to_doc()` + +**流程步骤:** + +1. **初始化文档字典** + ```python + doc = {} + doc['tenant_id'] = str(tenant_id) + doc['spu_id'] = str(spu_id) + ``` + +2. **填充文本字段**(多语言) + - 调用 `_fill_text_fields()` 填充 title、brief、description、vendor + +3. **填充标签** + - 解析 `tags` 字段,转换为列表 + +4. **填充类别字段** + - 调用 `_fill_category_fields()` 处理类别映射 + +5. **填充选项名称** + - 调用 `_fill_option_names()` 从 Option 表获取选项名称 + +6. **填充图片 URL** + - 调用 `_fill_image_url()` 处理图片 URL + +7. **填充销量** + - 转换 `fake_sales` 为整数 + +8. **处理 SKU** + - 调用 `_process_skus()` 处理所有 SKU + - 构建 `skus` 嵌套数组 + - 构建 `specifications` 数组 + - 计算价格范围、总库存等聚合字段 + +9. **填充选项值** + - 调用 `_fill_option_values()` 提取所有选项值 + +10. **填充时间字段** + - 转换 `create_time` 和 `update_time` 为 ISO 格式 + +11. **返回文档** + ```python + return doc + ``` + +**代码位置:** `indexer/document_transformer.py:57-166` + +--- + +## 6. 特殊处理逻辑 + +### 6.1 删除检测 + +系统支持两种删除方式: + +1. **显式删除**:通过 `delete_spu_ids` 参数显式指定要删除的 SPU +2. **自动检测删除**: + - SPU 在数据库中不存在 + - SPU 的 `deleted` 字段为 `1` 或 `b'\x01'` + +**删除处理:** + +```python +# 检查 deleted 字段(可能是 bit 类型) +def _is_deleted_value(v): + if isinstance(v, bytes): + return v == b"\x01" or v == 1 + return bool(v) + +spu_df["_is_deleted"] = spu_df["deleted"].apply(_is_deleted_value) +``` + +**代码位置:** `indexer/incremental_service.py:481-517` + +### 6.2 数据质量检查 + +#### 6.2.1 类别映射缺失 + +如果 `category_path` 中的类目ID在映射中不存在: +- 记录错误日志 +- 不写入任何类目字段(视为没有类目) +- 不阻塞索引流程 + +#### 6.2.2 标题缺失 + +如果 SPU 没有标题: +- 记录错误日志 +- 继续处理(但可能影响搜索效果) + +#### 6.2.3 SKU 缺失 + +如果 SPU 没有 SKU: +- 记录警告日志 +- 继续处理(但价格、库存等字段可能为空) + +### 6.3 性能优化 + +1. **批量查询**:使用 `IN` 子句批量查询,减少数据库往返 +2. **缓存分类映射**:在服务初始化时预加载,避免重复查询 +3. **缓存 Transformer**:按租户缓存 Transformer 实例,避免重复初始化 +4. **批量生成 Embedding**:收集所有标题后批量生成,利用批处理性能 +5. **批量写入 ES**:使用 `BulkIndexer` 批量写入,提高写入效率 + +--- + +## 7. ES 文档完整示例 + +```json +{ + "tenant_id": "1", + "spu_id": "12345", + "title_zh": "iPhone 15 Pro Max", + "title_en": "iPhone 15 Pro Max", + "brief_zh": "最新款 iPhone", + "brief_en": "Latest iPhone", + "description_zh": "详细描述...", + "description_en": "Detailed description...", + "vendor_zh": "Apple", + "vendor_en": "Apple", + "tags": ["手机", "智能手机", "Apple"], + "category_path_zh": "电子产品/手机/iPhone", + "category1_name": "电子产品", + "category2_name": "手机", + "category3_name": "iPhone", + "category_id": "3", + "category_level": 3, + "option1_name": "颜色", + "option2_name": "存储容量", + "option3_name": null, + "option1_values": ["深空黑色", "原色钛金属", "白色钛金属"], + "option2_values": ["256GB", "512GB", "1TB"], + "option3_values": [], + "image_url": "https://example.com/image.jpg", + "sales": 1000, + "min_price": 8999.0, + "max_price": 12999.0, + "compare_at_price": 12999.0, + "sku_prices": [8999.0, 10999.0, 12999.0], + "sku_weights": [221, 221, 221], + "sku_weight_units": ["g"], + "total_inventory": 500, + "create_time": "2024-01-01T00:00:00", + "update_time": "2024-01-15T10:30:00", + "title_embedding": [0.123, 0.456, ...], // 1024维向量 + "skus": [ + { + "sku_id": "1001", + "price": 8999.0, + "compare_at_price": 9999.0, + "sku_code": "IP15PM-256-BLK", + "stock": 100, + "weight": 221.0, + "weight_unit": "g", + "option1_value": "深空黑色", + "option2_value": "256GB", + "option3_value": null, + "image_src": "https://example.com/sku1.jpg" + }, + { + "sku_id": "1002", + "price": 10999.0, + "compare_at_price": 11999.0, + "sku_code": "IP15PM-512-BLK", + "stock": 200, + "weight": 221.0, + "weight_unit": "g", + "option1_value": "深空黑色", + "option2_value": "512GB", + "option3_value": null, + "image_src": "https://example.com/sku2.jpg" + } + ], + "specifications": [ + {"sku_id": "1001", "name": "颜色", "value": "深空黑色"}, + {"sku_id": "1001", "name": "存储容量", "value": "256GB"}, + {"sku_id": "1002", "name": "颜色", "value": "深空黑色"}, + {"sku_id": "1002", "name": "存储容量", "value": "512GB"} + ] +} +``` + +--- + +## 8. 总结 + +### 8.1 关键要点 + +1. **数据源**:三个主要表(SPU、SKU、Option) +2. **批量查询**:使用 `IN` 子句提高查询效率 +3. **类别映射**:通过预加载的映射表将 ID 转换为名称 +4. **多款式处理**:通过嵌套结构和扁平化字段支持复杂的 SKU 查询 +5. **多语言支持**:自动翻译文本字段,支持中英文双语 +6. **向量搜索**:批量生成 embedding,支持语义搜索 + +### 8.2 复杂字段处理总结 + +| 字段类型 | 处理方式 | 关键逻辑 | +|---------|---------|---------| +| **类别** | ID映射 + 路径解析 | 从 `category_path` 解析ID,通过映射表转换为名称,构建多级分类 | +| **SKU** | 嵌套数组 + 聚合计算 | 将每个 SKU 转换为嵌套对象,同时计算价格范围、总库存等聚合值 | +| **选项** | 名称 + 值分离 | 从 Option 表获取名称,从 SKU 表提取值,分别存储 | +| **规格** | 结构化数组 | 将 SKU 的选项值转换为 `{name, value, sku_id}` 结构,支持过滤和分面 | + +--- + +## 附录:相关代码文件 + +- `indexer/incremental_service.py`: 增量索引服务,数据获取逻辑 +- `indexer/document_transformer.py`: 文档转换器,字段填充逻辑 +- `indexer/indexing_utils.py`: 工具函数,分类映射加载和 Transformer 创建 +- `indexer/bulk_indexer.py`: 批量写入器,ES 写入逻辑 + diff --git a/docs/blog/img_seo_analysis.md b/docs/blog/img_seo_analysis.md new file mode 100644 index 0000000..71a32c3 --- /dev/null +++ b/docs/blog/img_seo_analysis.md @@ -0,0 +1,34 @@ +# IMG SEO 插件分析 + +## 1. 解决的具体问题 +- **图片搜索排名低**:通过优化 Alt 标签,让商品图片在 Google 图片搜索中获得更高排名。 +- **运营效率低下**:手动为大量商品图片设置 Alt 标签非常耗时,该工具提供批量处理能力。 +- **网站可访问性(Accessibility)**:为视障人士或图片加载失败时提供文字描述,符合 Web 标准。 +- **SEO 规范缺失**:商家不知道如何编写符合 SEO 最佳实践的 Alt 文本,工具提供内置推荐配置。 + +## 2. 技术方案/功能点 +- **智能 Alt 标签模板**:支持基于产品标题、集合名称等变量自动生成优化描述。 +- **三大场景覆盖**:一键覆盖商品图、专辑封面、博客配图。 +- **批量处理引擎**:支持全站图片的一键批量优化。 +- **内置最佳实践**:提供一键采用的推荐配置,确保符合搜索引擎规范。 +- **无障碍支持**:生成的 Alt 文本提升了页面的无障碍访问体验。 + +## 3. 市场需求 +- 视觉驱动型电商:如服装、家居等行业,图片搜索是重要的流量来源。 +- 自动化运维:减少重复性劳动,确保新上传的图片自动符合 SEO 要求。 +- 合规性需求:提升网站的无障碍访问水平。 + +## 4. 详细用法与案例 +### 用法步骤: +1. **安装**:在应用商店安装“图片 SEO”插件。 +2. **选择场景**:选择要优化的场景(商品、专辑或博客)。 +3. **设置模板**:使用变量(如 `{product_title}`)或推荐配置设置 Alt 规则。 +4. **执行批量优化**:点击一键优化,系统将自动处理存量图片。 + +### 使用案例: +**案例:家居装饰独立站的图片流量获取** +- **背景**:某家居站拥有大量精美的产品实拍图,但由于没有 Alt 标签,这些图片无法在 Google 图片搜索中被搜到。 +- **配置方案**: + - **模板设置**:`{product_title} - Modern Home Decor | {shop_name}`。 + - **操作**:对全站 500 个商品的 3000 张图片执行了一键批量优化。 +- **效果**:一个月后,来自 Google Images 的自然流量增长了 45%,且部分长尾关键词(如“modern minimalist vase”)的图片搜索结果排到了前三名。 diff --git a/docs/blog/seo_optimizer_analysis.md b/docs/blog/seo_optimizer_analysis.md new file mode 100644 index 0000000..b3b9a07 --- /dev/null +++ b/docs/blog/seo_optimizer_analysis.md @@ -0,0 +1,38 @@ +# SEO Optimizer 插件分析 + +## 1. 解决的具体问题 +- **SEO 诊断与评估**:帮助商家识别店铺中存在的 SEO 缺陷。 +- **全站 Meta 标签覆盖**:解决手动为成百上千个商品、专辑、博客设置 Meta 标签的低效问题。 +- **图片搜索流量缺失**:通过自动/批量设置图片 Alt 标签,提升图片在 Google Images 的排名。 +- **死链影响权重**:修复 404 无效链接,通过重定向保留页面权重。 +- **搜索引擎收录慢**:自动生成并提交 Sitemap,加速 Google 索引。 +- **高级 SEO 控制**:提供 Robots.txt 管理,满足个性化爬取需求。 + +## 2. 技术方案/功能点 +- **自动化 Meta 标签**:支持首页、商品、专辑、博客等页面的批量 Meta 设置,支持变量引用。 +- **图片 SEO 规则引擎**:基于自定义规则(如商品名+关键词)自动为新图片生成 Alt 文本。 +- **URL 重定向工具**:支持路径级和完整 URL 级的 301/302 重定向设置。 +- **Sitemap 自动更新**:每日凌晨自动更新站点地图,并提供与 Google Search Console 的集成接口。 +- **JSON-LD 结构化数据**:自动添加 Google Snippets(面包屑、商品信息、网站链接),提升搜索结果展示效果。 +- **Robots.txt 编辑器**:支持预览、编辑和一键屏蔽爬虫功能。 + +## 3. 市场需求 +- 规模化运营需求:商品量大的商家需要自动化工具。 +- 技术进阶需求:需要 Robots.txt 和结构化数据等高级 SEO 手段。 +- 流量监控需求:集成数据看板,直观查看自然流量转化。 + +## 4. 详细用法与案例 +### 用法步骤: +1. **Meta 标签设置**:在插件后台点击“Meta标签”,切换不同页面类型,输入模板并保存。 +2. **图片 Alt 设置**:在“图片SEO”模块设置生成规则,开启自动应用。 +3. **死链修复**:在“无效链接”模块输入旧路径和新目标 URL。 +4. **Sitemap 提交**:复制自动生成的 Sitemap 地址,前往 Google Search Console 提交。 + +### 使用案例: +**案例:大型 3C 电子产品站的自动化 SEO 维护** +- **背景**:某站有 2000+ SKU,手动设置 SEO 极其耗时,且经常有下架商品导致死链。 +- **配置方案**: + - **Meta 模版**:商品页 Meta 标题设为 `{product_title} - Buy Online at {shop_name}`。 + - **图片 Alt**:规则设为 `{product_title} - {shop_name} Electronics`。 + - **死链处理**:将所有已下架的旧款手机页面重定向到对应的最新款分类页。 +- **效果**:全站 SEO 覆盖率从 10% 提升到 100%,图片搜索带来的流量增长了 30%,且避免了因死链导致的 Google 惩罚。 diff --git a/docs/blog/smart_search_analysis.md b/docs/blog/smart_search_analysis.md new file mode 100644 index 0000000..4b30a4a --- /dev/null +++ b/docs/blog/smart_search_analysis.md @@ -0,0 +1,41 @@ +# 智能商品搜索插件分析 + +## 1. 解决的具体问题 +- **搜索效率低下**:帮助顾客快速定位目标商品。 +- **搜索结果不精准**:通过自定义排序规则和筛选条件优化结果。 +- **缺乏引导**:通过搜索热词和历史记录引导用户搜索。 +- **转化率提升**:智能关联推荐与搜索词相关的商品,增加曝光和转化。 + +## 2. 技术方案/功能点 +- **多维度筛选器配置**: + - **自动拉取数据**:厂商、价格、库存。 + - **手动配置/选择**:款式(Size/Color等)、标签、元字段(外形、尺寸、体积、重量、单行文本)。 +- **自定义排序条件**: + - 默认首位:Recommend(推荐)。 + - 可选维度:最新上架、价格高低、销量高低、加购最多、浏览最多、名称 A-Z/Z-A。 +- **搜索热词与引导**: + - 手动设置特定搜索热词。 + - 自动推荐高频搜索词。 + - 展示用户历史搜索记录。 +- **主题深度集成**:仅支持 Nova2023 系列(Night, Sweet, Morning, Bamboo, Moon)3.3.0 版本以上。 + +## 4. 详细用法与案例 +### 用法步骤: +1. **安装**:通过应用商店或主题装修页面快捷安装。 +2. **配置筛选器**:点击“添加筛选维度”,选择类型(如标签或元字段),并设置标题。 +3. **配置排序**:在插件后台调整排序维度的顺序。 +4. **应用**:开启“应用到店铺”开关。 + +### 使用案例: +**案例:时尚服装独立站优化搜索体验** +- **背景**:某服装站商品繁多,用户搜索“连衣裙”后结果太多,难以抉择。 +- **配置方案**: + - **筛选器**:添加“款式”筛选(尺码、颜色)、“价格”区间筛选、“标签”筛选(如“2024夏季新品”)。 + - **排序**:将“销量高低”和“加购最多”排在靠前位置,帮助用户参考他人购买偏好。 + - **热词**:设置“法式复古”、“显瘦”为热词,引导用户点击。 +- **效果**:用户通过筛选快速找到符合自己尺码和预算的连衣裙,提升了搜索到下单的转化率。 + +## 3. 市场需求 +- 提升站内搜索体验,减少用户流失。 +- 商家希望通过搜索数据引导用户购买特定商品(通过热词)。 +- 自动化推荐减少人工配置成本。 diff --git a/docs/blog/website_seo_analysis.md b/docs/blog/website_seo_analysis.md new file mode 100644 index 0000000..ec24fce --- /dev/null +++ b/docs/blog/website_seo_analysis.md @@ -0,0 +1,33 @@ +# Website SEO 插件分析 + +## 1. 解决的具体问题 +- **搜索排名靠后**:通过优化 TDK(Title, Description, Keywords)提升在 Google 等搜索引擎的排名。 +- **自然流量匮乏**:通过 SEO 吸引精准客户,减少对付费广告的依赖。 +- **SEO 门槛高**:为新手商家提供简化的设置流程,快速上手 SEO 基础配置。 + +## 2. 技术方案/功能点 +- **主页 TDK 管理**: + - **SEO 标题**:自定义主页在搜索结果中显示的标题。 + - **SEO 描述**:添加店铺整体描述和关键词,增强相关性。 + - **SEO 关键词**:添加多个核心关键字,扩大搜索覆盖面。 +- **索引优化**:通过规范化的元标签设置,增强搜索引擎的爬取和索引效率。 + +## 3. 市场需求 +- 商家需要低成本获取流量的手段。 +- 品牌化建设需求,确保搜索品牌名时能展示正确的描述。 +- 自动化/引导式 SEO 配置,降低技术理解成本。 + +## 4. 详细用法与案例 +### 用法步骤: +1. **安装**:在应用商店搜索“网站SEO”并安装。 +2. **配置主页 SEO**:进入插件,填写“主页SEO标题”、“主页SEO描述”和“SEO关键词”。 +3. **保存**:点击保存,等待搜索引擎抓取更新。 + +### 使用案例: +**案例:新兴宠物用品独立站的品牌曝光优化** +- **背景**:一家新开的宠物用品店“Pawsome Friends”,搜索品牌名时结果杂乱,且没有吸引人的描述。 +- **配置方案**: + - **标题**:Pawsome Friends | Premium Pet Supplies & Eco-friendly Toys + - **描述**:Shop at Pawsome Friends for high-quality, eco-friendly pet toys and organic treats. Free shipping on orders over $50. Your pet's happiness is our priority. + - **关键词**:pet supplies, eco-friendly dog toys, organic cat treats, Pawsome Friends. +- **效果**:两周后,Google 搜索结果显示了整洁的品牌标题和描述,点击率(CTR)明显提升,品牌专业度得到认可。 diff --git a/docs/blog/店匠(Shoplazza)插件功能分析报告:搜索与推荐方向的市场需求与技术方案提炼.md b/docs/blog/店匠(Shoplazza)插件功能分析报告:搜索与推荐方向的市场需求与技术方案提炼.md new file mode 100644 index 0000000..3d443c5 --- /dev/null +++ b/docs/blog/店匠(Shoplazza)插件功能分析报告:搜索与推荐方向的市场需求与技术方案提炼.md @@ -0,0 +1,121 @@ +# 店匠(Shoplazza)插件功能分析报告:搜索与推荐方向的市场需求与技术方案提炼 + +**作者:** Manus AI +**日期:** 2025年12月30日 + +## 引言 + +本报告旨在通过对店匠(Shoplazza)生态中四款核心插件——**智能商品搜索**、**网站 SEO**、**SEO 优化工具**和**图片 SEO**——的功能、技术方案及市场反馈进行深入分析,为贵公司的电商独立站 SaaS 系统在**搜索**和**推荐**领域提供市场需求收集和技术方向的参考。通过研究这些成熟工具,我们可以提炼出商家在站内搜索体验、站外搜索引擎优化(SEO)方面的核心痛点和解决方案。 + +## 插件功能概览与核心价值 + +这四款插件分别从站内搜索体验和站外自然流量获取两个维度,解决了电商商家在运营中的关键问题。下表总结了每个工具的核心价值和目标。 + +| 插件名称 | 核心解决问题 | 技术方案核心 | 市场需求定位 | +| :--- | :--- | :--- | :--- | +| **智能商品搜索** | 站内搜索效率低下、结果不精准 | 多维度筛选器、自定义排序规则、搜索热词引导 | 提升站内转化率、优化用户体验 | +| **网站 SEO** | 网站主页 SEO 基础配置缺失 | 主页 TDK(标题、描述、关键词)管理 | 品牌曝光、低成本自然流量获取 | +| **SEO 优化工具** | 全站 SEO 维护复杂、效率低下 | 批量 Meta 标签、Sitemap 自动化、URL 重定向、结构化数据 | 规模化运营、高级 SEO 控制、死链修复 | +| **图片 SEO** | 图片搜索流量缺失、Alt 标签配置耗时 | 智能 Alt 标签模板、批量优化引擎 | 视觉电商流量获取、无障碍访问合规 | + +## 详细分析与技术方案提炼 + +### 1. 智能商品搜索插件 + +#### 解决的具体问题与市场需求 + +该插件的核心价值在于解决电商平台**站内搜索**的效率和精准度问题 [1]。在商品数量庞大的独立站中,用户往往难以快速找到目标商品,导致跳出率升高。商家迫切需要工具来**优化搜索结果的排序和筛选**,并通过**搜索热词**等方式引导用户,最终目标是提升搜索到购买的转化率。 + +#### 技术方案与 SaaS 系统方向 + +该插件的技术方案体现了对搜索结果的精细化控制: + +* **多维度筛选器配置**:技术上需要支持从商品数据中**自动拉取**(如价格、库存)和**手动配置**(如款式、标签、元字段)两种类型的筛选维度。这要求 SaaS 系统具备灵活的**商品属性元数据管理**能力。 +* **自定义排序引擎**:除了默认的推荐排序,还允许商家根据**业务需求**(如销量、加购数、上新时间)自定义排序权重。这表明 SaaS 系统应提供一个**可配置的搜索结果排序算法接口**,允许商家通过后台配置影响搜索结果的展示逻辑。 +* **搜索引导机制**:通过**搜索热词**和**历史记录**实现用户搜索意图的捕捉和引导。这要求系统具备**搜索日志分析**能力,能够识别高频搜索词并将其作为推荐热词。 + +#### 使用案例:时尚服装独立站优化搜索体验 + +某服装站通过配置**款式**(尺码、颜色)和**价格**筛选器,并将**销量高低**和**加购最多**设置为靠前排序规则,显著提升了用户在搜索“连衣裙”后找到目标商品的效率,从而提高了转化率。 + +### 2. 网站 SEO 插件 + +#### 解决的具体问题与市场需求 + +网站 SEO 插件专注于解决**网站主页**的 SEO 基础配置问题 [2]。对于新手商家而言,如何设置**标题(Title)**、**描述(Description)**和**关键词(Keywords)**是进入 SEO 领域的第一步。市场需求集中在**简化操作**、**快速上手**,以低成本方式获取搜索引擎的**品牌曝光**和**自然流量**。 + +#### 技术方案与 SaaS 系统方向 + +其技术方案相对基础,但至关重要: + +* **TDK 集中管理**:提供一个统一的界面来设置网站主页的元标签。 +* **搜索引擎预览**:在后台提供 Google 搜索结果的**实时预览**功能,帮助商家优化文案以提高点击率(CTR)。 + +对于 SaaS 系统而言,这是**基础功能**,应内置于核心建站模块中,确保每个新站点都能轻松完成基础 SEO 配置。 + +#### 使用案例:新兴宠物用品独立站的品牌曝光优化 + +一家新开的宠物用品店通过设置清晰的品牌标题和吸引人的描述(如包含“Premium Pet Supplies”和“Free shipping”),在 Google 搜索结果中展示了专业的品牌形象,有效提升了点击率。 + +### 3. SEO 优化工具插件 + +#### 解决的具体问题与市场需求 + +SEO 优化工具是针对**规模化运营**和**高级 SEO 维护**的解决方案 [3]。它解决了商家在商品数量增加后,手动维护全站 SEO 标签、处理死链、提交网站地图等一系列复杂且耗时的任务。市场需求是**自动化**、**高级控制**和**合规性**。 + +#### 技术方案与 SaaS 系统方向 + +该插件的技术方案涉及多个高级 SEO 模块: + +* **批量 Meta 标签与规则引擎**:通过**变量引用**(如 `{product_title}`)实现全站商品、专辑、博客等页面的 Meta 标签**自动化生成**。这要求 SaaS 系统提供强大的**模板引擎**和**数据变量接口**。 +* **URL 重定向管理**:提供路径级和完整 URL 级的 **301/302 重定向**功能,用于修复死链和保留页面权重。这是维护网站健康度的关键技术。 +* **Sitemap 自动化与集成**:**实时或定时**生成并更新网站地图,并提供与 **Google Search Console** 的**集成接口**,实现一键提交。 +* **结构化数据(JSON-LD)**:自动生成并嵌入 **Google Snippets**(如面包屑、商品信息),以增强搜索结果的丰富度和可信度。 +* **Robots.txt 编辑器**:提供对爬虫访问权限的**高级控制**,满足个性化 SEO 需求。 + +对于 SaaS 系统,这表明**自动化 SEO 规则引擎**和**外部工具集成**(如 Google Search Console)是提升产品专业度的重要方向。 + +#### 使用案例:大型 3C 电子产品站的自动化 SEO 维护 + +一个拥有数千 SKU 的电子产品站,通过设置 Meta 标签模板和 Alt 标签规则,实现了全站 SEO 的自动化覆盖。同时,利用重定向功能将所有旧款下架商品页面导向最新款分类页,成功避免了因死链导致的 SEO 惩罚。 + +### 4. 图片 SEO 插件 + +#### 解决的具体问题与市场需求 + +图片 SEO 插件专注于解决**图片搜索流量**的获取问题 [4]。对于以视觉为驱动的电商(如服装、家居),图片是重要的流量入口。该插件通过**批量、智能地设置图片 Alt 标签**,解决了手动操作的低效性,并提升了网站的**无障碍访问**水平。 + +#### 技术方案与 SaaS 系统方向 + +该插件的技术方案是 SEO 规则引擎在图片领域的应用: + +* **智能 Alt 标签模板**:与 SEO 优化工具类似,使用**变量**(如产品标题、集合名称)来构造符合 SEO 规范的 Alt 文本。 +* **批量处理能力**:核心技术在于能够**高效地遍历**全站图片资源,并**批量写入** Alt 属性,同时支持对新上传图片的**自动应用**。 +* **场景覆盖**:支持商品图、专辑封面、博客配图等**多场景**的 Alt 标签优化。 + +对于 SaaS 系统,这强调了**媒体资源管理**模块中应内置**智能 Alt 文本生成**功能,可以考虑集成 AI 能力,根据图片内容自动生成描述性 Alt 文本,进一步提升自动化水平。 + +## 总结与对 SaaS 系统的方向建议 + +通过对上述插件的分析,我们可以为贵公司的电商独立站 SaaS 系统在**搜索**和**推荐**方向提炼出以下关键市场需求和技术方向: + +| 领域 | 市场需求提炼 | 技术方案方向 | +| :--- | :--- | :--- | +| **站内搜索** | **精准度与引导**:用户需要快速找到目标商品,商家需要引导用户购买特定商品。 | **可配置的搜索排序引擎**:允许商家自定义权重(销量、价格、新品等)。
**智能搜索引导**:基于搜索日志的**热词推荐**和**联想词**功能。 | +| **站外 SEO** | **自动化与规模化**:商品数量多,需要自动化工具进行全站 SEO 维护。 | **全站 SEO 规则模板引擎**:支持变量引用,批量生成 Meta 标签和 Alt 文本。
**网站健康度工具**:内置 **URL 重定向**、**Sitemap 自动化**、**Robots.txt 管理**等高级功能。 | +| **推荐** | **关联性与转化**:在搜索结果页和商品详情页提供相关推荐,以提升转化。 | **智能关联推荐**:基于搜索词、浏览历史、商品属性的**实时推荐算法**。 | + +**核心建议:** + +1. **构建统一的规则引擎**:将 SEO 优化工具和图片 SEO 中的**模板/变量**技术方案整合,形成一个统一的**“元数据自动化规则引擎”**,用于批量管理 TDK、Alt 标签等,大幅提升商家运营效率。 +2. **深化站内搜索能力**:将“智能商品搜索”的功能作为核心竞争力,提供**多维度筛选**和**自定义排序**的灵活配置,并利用搜索数据进行**智能推荐**,将搜索功能从简单的匹配升级为**转化工具**。 + +## 参考文献 + +[1] 智能商品搜索 | 店匠应用商店: +[2] Website SEO | 店匠应用商店: +[3] SEO优化工具 | 店匠应用商店: +[4] 图片 SEO | 店匠应用商店: +[5] 智能商品搜索|配置筛选项 – Shoplazza 帮助中心: +[6] 设置网站SEO – Shoplazza 帮助中心: +[7] SEO优化工具使用说明 – Shoplazza 帮助中心: diff --git a/docs/亚马逊到店匠格式转换分析.md b/docs/亚马逊到店匠格式转换分析.md index d965362..8ebeea3 100644 --- a/docs/亚马逊到店匠格式转换分析.md +++ b/docs/亚马逊到店匠格式转换分析.md @@ -370,3 +370,4 @@ python scripts/amazon_xlsx_to_shoplazza_xlsx.py \ + diff --git a/scripts/compare_index_mappings.py b/scripts/compare_index_mappings.py new file mode 100644 index 0000000..7554e56 --- /dev/null +++ b/scripts/compare_index_mappings.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +对比不同租户索引的 mapping 结构 +""" + +import os +import sys +import json +from pathlib import Path +from typing import Dict, Any + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.es_client import get_es_client_from_env + + +def get_field_type(mapping_dict: Dict, field_path: str) -> Dict[str, Any]: + """递归获取字段的 mapping 信息""" + parts = field_path.split('.') + current = mapping_dict + + for part in parts: + if isinstance(current, dict): + current = current.get(part) + if current is None: + return None + else: + return None + return current + + +def compare_mappings(mapping1: Dict[str, Any], mapping2: Dict[str, Any], index1_name: str, index2_name: str): + """对比两个索引的 mapping""" + props1 = mapping1.get('mappings', {}).get('properties', {}) + props2 = mapping2.get('mappings', {}).get('properties', {}) + + all_fields = set(props1.keys()) | set(props2.keys()) + + print(f"\n{'='*80}") + print(f"对比索引映射结构") + print(f"{'='*80}") + print(f"索引1: {index1_name}") + print(f"索引2: {index2_name}") + print(f"{'='*80}\n") + + differences = [] + same_fields = [] + + for field in sorted(all_fields): + field1 = props1.get(field) + field2 = props2.get(field) + + if field1 is None: + differences.append((field, f"只在 {index2_name} 中存在", field2)) + continue + if field2 is None: + differences.append((field, f"只在 {index1_name} 中存在", field1)) + continue + + type1 = field1.get('type') + type2 = field2.get('type') + + if type1 != type2: + differences.append((field, f"类型不同: {index1_name}={type1}, {index2_name}={type2}", (field1, field2))) + else: + same_fields.append((field, type1)) + + # 打印相同的字段 + print(f"✓ 相同字段 ({len(same_fields)} 个):") + for field, field_type in same_fields[:20]: # 只显示前20个 + print(f" - {field}: {field_type}") + if len(same_fields) > 20: + print(f" ... 还有 {len(same_fields) - 20} 个相同字段") + + # 打印不同的字段 + if differences: + print(f"\n✗ 不同字段 ({len(differences)} 个):") + for field, reason, details in differences: + print(f"\n {field}:") + print(f" {reason}") + if isinstance(details, tuple): + print(f" {index1_name}: {json.dumps(details[0], indent=4, ensure_ascii=False)}") + print(f" {index2_name}: {json.dumps(details[1], indent=4, ensure_ascii=False)}") + else: + print(f" 详情: {json.dumps(details, indent=4, ensure_ascii=False)}") + else: + print(f"\n✓ 所有字段类型一致!") + + # 特别检查 tags 字段 + print(f"\n{'='*80}") + print(f"特别检查: tags 字段") + print(f"{'='*80}") + + tags1 = get_field_type(props1, 'tags') + tags2 = get_field_type(props2, 'tags') + + if tags1: + print(f"\n{index1_name}.tags:") + print(f" 类型: {tags1.get('type')}") + print(f" 完整定义: {json.dumps(tags1, indent=2, ensure_ascii=False)}") + else: + print(f"\n{index1_name}.tags: 不存在") + + if tags2: + print(f"\n{index2_name}.tags:") + print(f" 类型: {tags2.get('type')}") + print(f" 完整定义: {json.dumps(tags2, indent=2, ensure_ascii=False)}") + else: + print(f"\n{index2_name}.tags: 不存在") + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='对比 Elasticsearch 索引的 mapping 结构') + parser.add_argument('index1', help='第一个索引名称 (例如: search_products_tenant_171)') + parser.add_argument('index2', nargs='?', help='第二个索引名称 (例如: search_products_tenant_162)') + parser.add_argument('--list', action='store_true', help='列出所有以 index1 为前缀的索引') + + args = parser.parse_args() + + # 连接 ES + try: + es_client = get_es_client_from_env() + if not es_client.ping(): + print("✗ 无法连接到 Elasticsearch") + return 1 + print("✓ Elasticsearch 连接成功\n") + except Exception as e: + print(f"✗ 连接 Elasticsearch 失败: {e}") + return 1 + + # 如果指定了 --list,列出所有匹配的索引 + if args.list or not args.index2: + try: + # 使用 cat API 列出所有索引 + indices = es_client.client.cat.indices(format='json') + matching_indices = [idx['index'] for idx in indices if idx['index'].startswith(args.index1)] + + if matching_indices: + print(f"找到 {len(matching_indices)} 个匹配的索引:") + for idx in sorted(matching_indices): + print(f" - {idx}") + return 0 + else: + print(f"未找到以 '{args.index1}' 开头的索引") + return 1 + except Exception as e: + print(f"✗ 列出索引失败: {e}") + return 1 + + # 获取两个索引的 mapping + index1 = args.index1 + index2 = args.index2 + + print(f"正在获取索引映射...") + print(f" 索引1: {index1}") + print(f" 索引2: {index2}\n") + + # 检查索引是否存在 + if not es_client.index_exists(index1): + print(f"✗ 索引 '{index1}' 不存在") + return 1 + + if not es_client.index_exists(index2): + print(f"✗ 索引 '{index2}' 不存在") + return 1 + + # 获取 mapping + mapping1 = es_client.get_mapping(index1) + mapping2 = es_client.get_mapping(index2) + + if not mapping1 or index1 not in mapping1: + print(f"✗ 无法获取索引 '{index1}' 的映射") + return 1 + + if not mapping2 or index2 not in mapping2: + print(f"✗ 无法获取索引 '{index2}' 的映射") + return 1 + + # 对比 mapping + compare_mappings(mapping1[index1], mapping2[index2], index1, index2) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) + diff --git a/scripts/test_cnclip_service.py b/scripts/test_cnclip_service.py new file mode 100755 index 0000000..2fcfc7b --- /dev/null +++ b/scripts/test_cnclip_service.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +CN-CLIP 服务测试脚本 + +用途: + 测试 CN-CLIP 服务的文本和图像编码功能(使用 gRPC 协议) + +使用方法: + python scripts/test_cnclip_service.py [PORT] + +参数: + PORT: 服务端口(默认:51000) +""" + +import sys +import numpy as np +from clip_client import Client + + +def test_encoding(client, test_name, inputs): + """测试编码功能""" + print(f"\n{test_name}...") + try: + result = client.encode(inputs) + if isinstance(result, np.ndarray): + print(f"✓ 成功! 形状: {result.shape}") + print(f" 输入数量: {len(inputs)}") + print(f" 输出维度: {result.shape[1]}") + + # 显示每个 embedding 的维度和前20个数字 + for i in range(min(len(inputs), result.shape[0])): + emb = result[i] + first_20 = emb[:20].tolist() + + # 计算 L2 归一化 + norm = np.linalg.norm(emb) + normalized_emb = emb / norm if norm > 0 else emb + normalized_first_20 = normalized_emb[:20].tolist() + + print(f" input: {inputs[i]}") + print(f" Embedding[{i}] 维度: {len(emb)}") + print(f" 前20个数字: {first_20}") + print(f" normalize后的前20个数字: {normalized_first_20}") + return True + else: + print(f"✗ 失败: 返回类型错误: {type(result)}") + return False + except Exception as e: + print(f"✗ 失败: {e}") + import traceback + traceback.print_exc() + return False + + +def main(): + # 获取端口参数 + port = sys.argv[1] if len(sys.argv) > 1 else "51000" + grpc_url = f"grpc://localhost:{port}" + + print("=" * 50) + print("CN-CLIP 服务测试") + print("=" * 50) + print(f"服务地址: {grpc_url} (gRPC 协议)") + print() + + # 创建客户端 + try: + client = Client(grpc_url) + except Exception as e: + print(f"✗ 客户端创建失败: {e}") + sys.exit(1) + + # 运行测试 + results = [] + + # 测试1: 文本编码 + results.append(test_encoding( + client, + "测试1: 编码文本", + ['这是一个测试文本', '另一个测试文本'] + )) + + # 测试2: 图像编码 + results.append(test_encoding( + client, + "测试2: 编码图像(远程 URL)", + ['https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg'] + )) + + # 测试3: 混合编码 + results.append(test_encoding( + client, + "测试3: 混合编码(文本和图像)", + ['这是一段文本', 'https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg'] + )) + + # 汇总 + print("\n" + "=" * 50) + print("测试结果汇总") + print("=" * 50) + print(f"总测试数: {len(results)}") + print(f"通过: {sum(results)}") + print(f"失败: {len(results) - sum(results)}") + + if all(results): + print("\n✓ 所有测试通过!") + sys.exit(0) + else: + print("\n✗ 部分测试失败") + sys.exit(1) + + +if __name__ == '__main__': + main() + diff --git a/scripts/test_cnclip_service.sh b/scripts/test_cnclip_service.sh deleted file mode 100755 index a499b51..0000000 --- a/scripts/test_cnclip_service.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash - -############################################################################### -# CN-CLIP 服务测试脚本 -# -# 用途: -# 测试 CN-CLIP 服务的文本和图像编码功能(使用 gRPC 协议) -# -# 使用方法: -# ./scripts/test_cnclip_service.sh [PORT] -# -# 参数: -# PORT: 服务端口(默认:51000) -# -############################################################################### - -set -e - -# 颜色定义 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# 默认端口 -PORT=${1:-51000} -GRPC_URL="grpc://localhost:${PORT}" - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}CN-CLIP 服务测试${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" -echo -e "服务地址: ${GRPC_URL} (gRPC 协议)" -echo "" - -# 检查 clip_client 是否安装 -if ! python3 -c "from clip_client import Client" 2>/dev/null; then - echo -e "${RED}错误: clip_client 未安装${NC}" - echo -e "${YELLOW}请运行: pip install clip-client${NC}" - exit 1 -fi - -# 使用 Python 客户端测试(因为服务使用 gRPC 协议) -python3 << PYTHON_EOF -import sys -import numpy as np -from clip_client import Client - -def test_encoding(client, test_name, inputs): - print(f"\n{test_name}...") - try: - result = client.encode(inputs) - if isinstance(result, np.ndarray): - print(f"✓ 成功! 形状: {result.shape}") - print(f" 输入数量: {len(inputs)}") - print(f" 输出维度: {result.shape[1]}") - - # 显示每个 embedding 的维度和前20个数字 - for i in range(min(len(inputs), result.shape[0])): - emb = result[i] - first_20 = emb[:20].tolist() - print(f" Embedding[{i}] 维度: {len(emb)}") - print(f" 前20个数字: {first_20}") - return True - else: - print(f"✗ 失败: 返回类型错误: {type(result)}") - return False - except Exception as e: - print(f"✗ 失败: {e}") - import traceback - traceback.print_exc() - return False - -# 测试 -port = "${PORT}" -client = Client(f'grpc://localhost:{port}') - -results = [] - -# 测试1: 文本编码 -results.append(test_encoding( - client, - "测试1: 编码文本", - ['这是一个测试文本', '另一个测试文本'] -)) - -# 测试2: 图像编码 -results.append(test_encoding( - client, - "测试2: 编码图像(远程 URL)", - ['https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg'] -)) - -# 测试3: 混合编码 -results.append(test_encoding( - client, - "测试3: 混合编码(文本和图像)", - ['这是一段文本', 'https://oss.essa.cn/98532128-cf8e-456c-9e30-6f2a5ea0c19f.jpg'] -)) - -# 汇总 -print("\n" + "="*50) -print("测试结果汇总") -print("="*50) -print(f"总测试数: {len(results)}") -print(f"通过: {sum(results)}") -print(f"失败: {len(results) - sum(results)}") - -if all(results): - print("\n✓ 所有测试通过!") - sys.exit(0) -else: - print("\n✗ 部分测试失败") - sys.exit(1) -PYTHON_EOF - -TEST_RESULT=$? - -if [ $TEST_RESULT -eq 0 ]; then - echo "" - echo -e "${GREEN}========================================${NC}" - echo -e "${GREEN}✓ 所有测试通过!${NC}" - echo -e "${GREEN}========================================${NC}" -else - echo "" - echo -e "${RED}========================================${NC}" - echo -e "${RED}✗ 部分测试失败${NC}" - echo -e "${RED}========================================${NC}" - exit 1 -fi diff --git a/search/searcher.py b/search/searcher.py index f54b8de..e83b79a 100644 --- a/search/searcher.py +++ b/search/searcher.py @@ -275,6 +275,7 @@ class Searcher: try: # Generate tenant-specific index name index_name = get_tenant_index_name(tenant_id) + index_name = "search_products" # No longer need to add tenant_id to filters since each tenant has its own index -- libgit2 0.21.2