Commit 950a640e392f52ccd2d836e2fedd7186ada0720f

Authored by tangwang
1 parent cc11ae04

embeddings

@@ -38,7 +38,7 @@ START_TRANSLATOR=0 @@ -38,7 +38,7 @@ START_TRANSLATOR=0
38 START_RERANKER=0 38 START_RERANKER=0
39 39
40 # Embedding Models 40 # Embedding Models
41 -TEXT_MODEL_DIR=/data/tw/models/bge-m3 # 已经改为web请求了,不使用本地模型 41 +TEXT_MODEL_ID=Qwen/Qwen3-Embedding-0.6B
42 IMAGE_MODEL_DIR=/data/tw/models/cn-clip # 已经改为web请求了,不使用本地模型 42 IMAGE_MODEL_DIR=/data/tw/models/cn-clip # 已经改为web请求了,不使用本地模型
43 43
44 # Cache Directory 44 # Cache Directory
CHANGES.md deleted
@@ -1,218 +0,0 @@ @@ -1,218 +0,0 @@
1 -# 云端向量化模块更新 - 变更摘要  
2 -  
3 -## 📅 更新日期  
4 -2025-12-05  
5 -  
6 -## 🎯 更新目标  
7 -为 saas-search 项目添加基于阿里云 DashScope API 的云端文本向量化功能。  
8 -  
9 -## 📝 新增文件  
10 -  
11 -### 1. 核心模块  
12 -- ✅ `embeddings/cloud_text_encoder.py`  
13 - - 云端文本向量化编码器  
14 - - 使用 text-embedding-v4 模型  
15 - - 单例模式,线程安全  
16 - - 支持批量处理  
17 -  
18 -### 2. 测试脚本  
19 -- ✅ `scripts/test_cloud_embedding.py`  
20 - - 读取 queries.txt 前 100 条  
21 - - 记录发送/接收时间和耗时  
22 - - 输出详细测试报告  
23 -  
24 -### 3. 示例代码  
25 -- ✅ `examples/cloud_embedding_example.py`  
26 - - 单个文本向量化示例  
27 - - 批量处理示例  
28 - - 相似度计算示例  
29 - - 完整的错误处理  
30 -  
31 -### 4. 文档  
32 -- ✅ `docs/cloud_embedding_usage.md` - 详细使用文档  
33 -- ✅ `docs/cloud_embedding_quickstart.md` - 快速入门指南  
34 -- ✅ `CLOUD_EMBEDDING_README.md` - 功能说明  
35 -- ✅ `CHANGES.md` - 本文档  
36 -  
37 -## 🔧 修改文件  
38 -  
39 -### 1. requirements.txt  
40 -- ✅ 添加依赖:`openai>=1.0.0`  
41 -  
42 -## 📊 功能特性  
43 -  
44 -### CloudTextEncoder 类  
45 -  
46 -**方法:**  
47 -- `__init__(api_key=None, base_url=None)` - 初始化(单例)  
48 -- `encode(sentences, ...)` - 向量化文本  
49 -- `encode_batch(texts, batch_size=32, ...)` - 批量处理  
50 -- `get_embedding_dimension()` - 获取向量维度  
51 -  
52 -**特性:**  
53 -- 单例模式,确保只有一个实例  
54 -- 线程安全  
55 -- 自动错误处理  
56 -- 支持批量处理避免 API 限制  
57 -- 返回 numpy 数组(兼容现有代码)  
58 -  
59 -### 测试脚本功能  
60 -  
61 -**test_cloud_embedding.py:**  
62 -- 读取 queries.txt(可配置数量)  
63 -- 逐条发送向量化请求  
64 -- 记录每次请求的:  
65 - - 发送时间(精确到毫秒)  
66 - - 接收时间(精确到毫秒)  
67 - - 耗时(秒)  
68 - - 向量维度  
69 - - 成功/失败状态  
70 -- 输出汇总统计:  
71 - - 总查询数  
72 - - 成功/失败数量  
73 - - 成功率  
74 - - 总耗时  
75 - - 平均耗时  
76 - - 吞吐量  
77 -  
78 -## 📖 使用示例  
79 -  
80 -### 基本使用  
81 -```python  
82 -from embeddings.cloud_text_encoder import CloudTextEncoder  
83 -  
84 -encoder = CloudTextEncoder()  
85 -embedding = encoder.encode("衣服的质量杠杠的")  
86 -```  
87 -  
88 -### 批量处理  
89 -```python  
90 -texts = ["文本1", "文本2", "文本3"]  
91 -embeddings = encoder.encode(texts)  
92 -```  
93 -  
94 -### 运行测试  
95 -```bash  
96 -export DASHSCOPE_API_KEY="sk-xxx"  
97 -python scripts/test_cloud_embedding.py  
98 -```  
99 -  
100 -## 🔍 测试结果示例  
101 -  
102 -```  
103 -================================================================================  
104 -Test Summary  
105 -================================================================================  
106 -Total Queries: 100  
107 -Successful: 100  
108 -Failed: 0  
109 -Success Rate: 100.0%  
110 -Total Time: 35.123s  
111 -Total API Time: 32.456s  
112 -Average Duration: 0.325s per query  
113 -Throughput: 2.85 queries/second  
114 -================================================================================  
115 -```  
116 -  
117 -## 📚 文档结构  
118 -  
119 -```  
120 -docs/  
121 -├── cloud_embedding_usage.md # 详细使用说明  
122 -│ ├── 模块说明  
123 -│ ├── 使用步骤  
124 -│ ├── 注意事项  
125 -│ ├── 地域选择  
126 -│ ├── 故障排除  
127 -│ └── 更多信息  
128 -│  
129 -└── cloud_embedding_quickstart.md # 快速入门  
130 - ├── 新增文件列表  
131 - ├── 快速开始步骤  
132 - ├── 代码示例  
133 - ├── 性能指标  
134 - ├── 配置选项  
135 - ├── 常见问题  
136 - └── 最佳实践  
137 -```  
138 -  
139 -## ✅ 验证步骤  
140 -  
141 -1. **环境准备**  
142 - - [x] 安装 openai 包  
143 - - [x] 设置 DASHSCOPE_API_KEY  
144 -  
145 -2. **功能测试**  
146 - - [ ] 运行 `python scripts/test_cloud_embedding.py`  
147 - - [ ] 运行 `python examples/cloud_embedding_example.py`  
148 - - [ ] 检查输出是否正常  
149 -  
150 -3. **集成测试**  
151 - - [ ] 在项目中导入模块  
152 - - [ ] 测试与现有代码的兼容性  
153 -  
154 -## 🎯 预期性能  
155 -  
156 -基于 text-embedding-v4 模型:  
157 -- **向量维度**:1024  
158 -- **平均延迟**:300-400ms  
159 -- **吞吐量**:~2-3 queries/秒(单线程)  
160 -- **成功率**:>99%  
161 -  
162 -## ⚠️ 注意事项  
163 -  
164 -1. **API Key 安全**  
165 - - 不要将 API Key 提交到代码仓库  
166 - - 使用环境变量或配置文件  
167 -  
168 -2. **成本控制**  
169 - - 云端 API 按使用量计费  
170 - - 建议缓存常用查询的向量  
171 - - 监控使用量  
172 -  
173 -3. **速率限制**  
174 - - 注意 API 速率限制  
175 - - 测试脚本已添加适当延迟  
176 - - 可根据需要调整 batch_size  
177 -  
178 -4. **网络依赖**  
179 - - 需要稳定的网络连接  
180 - - 考虑添加重试机制  
181 -  
182 -## 🔄 后续计划  
183 -  
184 -建议的改进方向:  
185 -- [ ] 添加向量缓存机制  
186 -- [ ] 实现自动重试逻辑  
187 -- [ ] 添加多线程/异步支持  
188 -- [ ] 集成到搜索模块  
189 -- [ ] 添加性能监控  
190 -- [ ] 实现成本追踪  
191 -  
192 -## 🆚 与本地编码器对比  
193 -  
194 -| 特性 | CloudTextEncoder | BgeEncoder |  
195 -|------|------------------|------------|  
196 -| 部署 | 无需部署 | 需要本地服务 |  
197 -| 延迟 | ~350ms | <100ms |  
198 -| 成本 | 按使用付费 | 固定成本 |  
199 -| 维护 | 无需维护 | 需要维护 |  
200 -| 离线 | 不支持 | 支持 |  
201 -| 扩展 | 自动扩展 | 手动扩展 |  
202 -  
203 -## 📞 获取帮助  
204 -  
205 -- 查看文档:`docs/cloud_embedding_*.md`  
206 -- 运行示例:`python examples/cloud_embedding_example.py`  
207 -- 阿里云文档:https://help.aliyun.com/zh/model-studio/  
208 -  
209 -## ✨ 总结  
210 -  
211 -本次更新成功添加了云端向量化功能,包括:  
212 -- ✅ 完整的编码器实现  
213 -- ✅ 详细的测试脚本  
214 -- ✅ 丰富的示例代码  
215 -- ✅ 完善的文档  
216 -  
217 -所有功能已测试通过,可以直接使用。  
218 -  
CLIP_SERVICE_README.md
@@ -82,7 +82,7 @@ cd /data/saas-search @@ -82,7 +82,7 @@ cd /data/saas-search
82 82
83 - 现有本地向量服务: 83 - 现有本地向量服务:
84 - 启动脚本:`./scripts/start_embedding_service.sh` 84 - 启动脚本:`./scripts/start_embedding_service.sh`
85 - - 实现:`embeddings/server.py`(FastAPI + 本地模型 `bge_model.py` / `clip_model.py`) 85 + - 实现:`embeddings/server.py`(FastAPI + 本地模型 `qwen3_model.py` / `clip_model.py`)
86 - 新增的 `clip-server`: 86 - 新增的 `clip-server`:
87 - 使用官方实现,单独进程、单独环境 87 - 使用官方实现,单独进程、单独环境
88 - 面向图像 / 文本的 CLIP 向量化服务 88 - 面向图像 / 文本的 CLIP 向量化服务
@@ -189,8 +189,6 @@ INFO gateway/rep-0@XXXXX start server bound to 0.0.0.0:51000 @@ -189,8 +189,6 @@ INFO gateway/rep-0@XXXXX start server bound to 0.0.0.0:51000
189 ## 5. 参考 189 ## 5. 参考
190 190
191 - 项目地址:`https://github.com/jina-ai/clip-as-service` 191 - 项目地址:`https://github.com/jina-ai/clip-as-service`
192 -- 本项目向量模块文档:`embeddings/README.md`、`CLOUD_EMBEDDING_README.md`  
193 -  
194 - 192 +- 本项目向量模块文档:`embeddings/README.md`
195 193
196 194
config/config.yaml
@@ -162,16 +162,11 @@ services: @@ -162,16 +162,11 @@ services:
162 location: "global" 162 location: "global"
163 model: "" 163 model: ""
164 embedding: 164 embedding:
165 - provider: "http" # http | vllm(reserved) 165 + provider: "http" # http
166 base_url: "http://127.0.0.1:6005" 166 base_url: "http://127.0.0.1:6005"
167 providers: 167 providers:
168 http: 168 http:
169 base_url: "http://127.0.0.1:6005" 169 base_url: "http://127.0.0.1:6005"
170 - vllm:  
171 - enabled: false  
172 - base_url: ""  
173 - model: ""  
174 - note: "reserved for future vLLM embedding backend"  
175 rerank: 170 rerank:
176 provider: "http" 171 provider: "http"
177 base_url: "http://127.0.0.1:6007" 172 base_url: "http://127.0.0.1:6007"
config/env_config.py
@@ -79,8 +79,8 @@ EMBEDDING_SERVICE_URL = os.getenv(&#39;EMBEDDING_SERVICE_URL&#39;) or f&#39;http://{EMBEDDIN @@ -79,8 +79,8 @@ EMBEDDING_SERVICE_URL = os.getenv(&#39;EMBEDDING_SERVICE_URL&#39;) or f&#39;http://{EMBEDDIN
79 TRANSLATION_SERVICE_URL = os.getenv('TRANSLATION_SERVICE_URL') or f'http://{TRANSLATION_HOST}:{TRANSLATION_PORT}' 79 TRANSLATION_SERVICE_URL = os.getenv('TRANSLATION_SERVICE_URL') or f'http://{TRANSLATION_HOST}:{TRANSLATION_PORT}'
80 RERANKER_SERVICE_URL = os.getenv('RERANKER_SERVICE_URL') or f'http://{RERANKER_HOST}:{RERANKER_PORT}/rerank' 80 RERANKER_SERVICE_URL = os.getenv('RERANKER_SERVICE_URL') or f'http://{RERANKER_HOST}:{RERANKER_PORT}/rerank'
81 81
82 -# Model Directories  
83 -TEXT_MODEL_DIR = os.getenv('TEXT_MODEL_DIR', '/data/tw/models/bge-m3') 82 +# Model IDs / paths
  83 +TEXT_MODEL_DIR = os.getenv('TEXT_MODEL_DIR', os.getenv('TEXT_MODEL_ID', 'Qwen/Qwen3-Embedding-0.6B'))
84 IMAGE_MODEL_DIR = os.getenv('IMAGE_MODEL_DIR', '/data/tw/models/cn-clip') 84 IMAGE_MODEL_DIR = os.getenv('IMAGE_MODEL_DIR', '/data/tw/models/cn-clip')
85 85
86 # Cache Directory 86 # Cache Directory
docs/DEVELOPER_GUIDE.md
@@ -154,7 +154,7 @@ docs/ # 文档(含本指南) @@ -154,7 +154,7 @@ docs/ # 文档(含本指南)
154 154
155 ### 4.6 embeddings 155 ### 4.6 embeddings
156 156
157 -- **职责**:提供向量服务(FastAPI):`POST /embed/text`、`POST /embed/image`;服务内按配置加载文本后端(如 BGE)与图像后端(如 clip-as-service 或本地 CN-CLIP),实现协议即可插拔。 157 +- **职责**:提供向量服务(FastAPI):`POST /embed/text`、`POST /embed/image`;服务内按配置加载文本后端(如 Qwen3-Embedding-0.6B)与图像后端(如 clip-as-service 或本地 CN-CLIP),实现协议即可插拔。
158 - **原则**:图片后端实现 `embeddings/protocols.ImageEncoderProtocol`;配置优先从 `config` 或 `embeddings/config.py` 读取,与 `services.embedding` 的 URL 分离。 158 - **原则**:图片后端实现 `embeddings/protocols.ImageEncoderProtocol`;配置优先从 `config` 或 `embeddings/config.py` 读取,与 `services.embedding` 的 URL 分离。
159 - **详见**:`embeddings/README.md`、本指南 §7.5–§7.6。 159 - **详见**:`embeddings/README.md`、本指南 §7.5–§7.6。
160 160
@@ -303,7 +303,7 @@ services: @@ -303,7 +303,7 @@ services:
303 |------|--------|------|--------| 303 |------|--------|------|--------|
304 | 调用方 | `services.<capability>.provider` | http | http | 304 | 调用方 | `services.<capability>.provider` | http | http |
305 | 调用方 | `services.<capability>.providers.http.base_url` | 6007 | 6005 | 305 | 调用方 | `services.<capability>.providers.http.base_url` | 6007 | 6005 |
306 -| 服务内 | `services.<capability>.backend` | bge / qwen3_vllm 等 | 见 embeddings/config | 306 +| 服务内 | `services.<capability>.backend` | qwen3_embedding_st / qwen3_vllm 等 | 见 embeddings/config |
307 | 服务内 | `services.<capability>.backends.<name>` | 模型名、batch、vLLM 参数 | 模型名、device 等 | 307 | 服务内 | `services.<capability>.backends.<name>` | 模型名、batch、vLLM 参数 | 模型名、device 等 |
308 308
309 ### 7.6 新增后端清单(以 Qwen3-Reranker 为例) 309 ### 7.6 新增后端清单(以 Qwen3-Reranker 为例)
docs/QUICKSTART.md
@@ -396,7 +396,7 @@ services: @@ -396,7 +396,7 @@ services:
396 这里重点区分两层: 396 这里重点区分两层:
397 397
398 - **Provider 层(调用方式)**:调用方如何访问能力(http/direct) 398 - **Provider 层(调用方式)**:调用方如何访问能力(http/direct)
399 -- **Backend 层(推理实现)**:服务进程内部具体模型实现(bge / qwen3_vllm / ...) 399 +- **Backend 层(推理实现)**:服务进程内部具体模型实现(qwen3_embedding_st / qwen3_vllm / ...)
400 400
401 ### 4.1 Rerank(重排)扩展 401 ### 4.1 Rerank(重排)扩展
402 402
@@ -429,7 +429,7 @@ services: @@ -429,7 +429,7 @@ services:
429 服务实现: 429 服务实现:
430 430
431 - `embeddings/server.py`(文本和图像可独立加载) 431 - `embeddings/server.py`(文本和图像可独立加载)
432 -- 文本后端现用 BGE 432 +- 文本后端现用 Qwen3-Embedding-0.6B(Sentence Transformers)
433 - 图像后端支持本地 CLIP 或 clip-as-service 433 - 图像后端支持本地 CLIP 或 clip-as-service
434 434
435 扩展建议: 435 扩展建议:
@@ -438,6 +438,12 @@ services: @@ -438,6 +438,12 @@ services:
438 - 图像后端实现 `ImageEncoderProtocol`(`encode_image_urls`) 438 - 图像后端实现 `ImageEncoderProtocol`(`encode_image_urls`)
439 - 如后续后端增多,建议与 rerank 一样在 `services.embedding.backend(s)` 统一配置 439 - 如后续后端增多,建议与 rerank 一样在 `services.embedding.backend(s)` 统一配置
440 440
  441 +选型建议(Qwen3-Embedding-0.6B):
  442 +
  443 +- 当前仓库默认:`Sentence Transformers` 本地推理(改造成本最低,便于直接替换 6005 现有服务)
  444 +- 生产高并发优先:`TEI`(Text Embeddings Inference)通常比 vLLM 更贴合 embedding-only 场景
  445 +- `vLLM` 更适合生成式/重排混合场景;若仅做文本 embedding,通常不作为首选
  446 +
441 ### 4.3 新增后端检查清单(以 `qwen3_vllm` 为例) 447 ### 4.3 新增后端检查清单(以 `qwen3_vllm` 为例)
442 448
443 1. 实现后端协议(服务内) 449 1. 实现后端协议(服务内)
@@ -32,28 +32,18 @@ HOST:10.200.16.14 / localhost @@ -32,28 +32,18 @@ HOST:10.200.16.14 / localhost
32 32
33 你安装过nvidia-container-toolkit吗 33 你安装过nvidia-container-toolkit吗
34 现在有一些开源的推理引擎对向量化模型和重排模型支持的比较好,我们这块也正好要单独拎出来,因此想改造下。 34 现在有一些开源的推理引擎对向量化模型和重排模型支持的比较好,我们这块也正好要单独拎出来,因此想改造下。
35 -调研了TEI, vLLM, xinference,目前觉得最合适的是xinference+vLLM后端,  
36 -最好以docker方式部署,让gpu对docker可见需要nvidia-container-toolkit,  
37 -我试了多种方法安装赖nvidia-container-toolkit都失败了 35 +已决策:embedding 先统一为 Qwen3-Embedding-0.6B(Sentence Transformers,本地 6005),后续若要独立高并发服务优先评估 TEI。
  36 +最好以 docker 方式部署,让 gpu 对 docker 可见需要 nvidia-container-toolkit,
  37 +我试了多种方法安装 nvidia-container-toolkit 都失败了
38 https://mirrors.aliyun.com/github/releases/NVIDIA/nvidia-container-toolkit/ 38 https://mirrors.aliyun.com/github/releases/NVIDIA/nvidia-container-toolkit/
39 https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html 39 https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html
40 40
41 41
42 -bge-m3  
43 qwen3-embedding 42 qwen3-embedding
44 qwen3-reranker 43 qwen3-reranker
45 44
46 -大概耗时是0.026S,现在用这个xinference都得0.5S,看这个xinference的安装和embedding模型的部署是不是有问题  
47 -  
48 -  
49 -  
50 -xinference是直接支持了embedding和reranker模型这些模型类型,相当于vllm的上层的封装,因此调用接口很简单,也支持bge和qwen3系列。 但是性能这么差 估计是有啥问题。  
51 -不好查的话,用vllm或者其他的推理引擎也行,  
52 -  
53 -选一个推理引擎,相比于我自己直接调modelscope/sentence-transformers,主要是多进程和负载均衡、连续批处理,比较有用  
54 -不知道我理解的有没有问题  
55 -调研了TEI, vLLM, xinference,+vLLM后端  
56 -这个推理引擎怎么选合适,是选VLLM还是xinference 45 +选一个推理引擎,相比于我自己直接调 sentence-transformers,主要是多进程和负载均衡、连续批处理,比较有用
  46 +当前结论:embedding 场景优先 TEI;vLLM 更偏向生成式与 rerank 场景。
57 47
58 48
59 49
@@ -83,4 +73,3 @@ https://cloud.tencent.com/document/product/1729/113395#4.-.E7.A4.BA.E4.BE.8B @@ -83,4 +73,3 @@ https://cloud.tencent.com/document/product/1729/113395#4.-.E7.A4.BA.E4.BE.8B
83 登录 百炼美国地域控制台:https://modelstudio.console.aliyun.com/us-east-1?spm=5176.2020520104.0.0.6b383a98WjpXff 73 登录 百炼美国地域控制台:https://modelstudio.console.aliyun.com/us-east-1?spm=5176.2020520104.0.0.6b383a98WjpXff
84 在 API Key 管理 中创建或复制一个适用于美国地域的 Key 74 在 API Key 管理 中创建或复制一个适用于美国地域的 Key
85 75
86 -  
docs/向量化模块和API说明文档.md
@@ -10,6 +10,7 @@ @@ -10,6 +10,7 @@
10 ## 配置 10 ## 配置
11 11
12 - Provider/URL:`config/config.yaml` 的 `services.embedding` 12 - Provider/URL:`config/config.yaml` 的 `services.embedding`
13 -- 模型路径:`embeddings/config.py` 或 env `TEXT_MODEL_DIR`、`IMAGE_MODEL_DIR` 13 +- 文本模型:`embeddings/config.py` 的 `TEXT_MODEL_ID`(默认 `Qwen/Qwen3-Embedding-0.6B`)
  14 +- 运行参数:`TEXT_DEVICE`、`TEXT_BATCH_SIZE`、`TEXT_NORMALIZE_EMBEDDINGS`
14 15
15 详见 `embeddings/README.md`。 16 详见 `embeddings/README.md`。
docs/系统设计文档.md
@@ -420,11 +420,11 @@ query_config: @@ -420,11 +420,11 @@ query_config:
420 1. **条件生成**: 420 1. **条件生成**:
421 - 仅当 `enable_text_embedding=true` 时生成向量 421 - 仅当 `enable_text_embedding=true` 时生成向量
422 - 仅对 `default` 域查询生成向量 422 - 仅对 `default` 域查询生成向量
423 -2. **向量模型**:BGE-M3 模型(1024维向量) 423 +2. **向量模型**:Qwen3-Embedding-0.6B(1024维向量)
424 3. **用途**:用于语义搜索(KNN 检索) 424 3. **用途**:用于语义搜索(KNN 检索)
425 425
426 #### 实现模块 426 #### 实现模块
427 -- `embeddings/bge_encoder.py` - BGE 文本编码器 427 +- `embeddings/text_encoder.py` + `embeddings/server.py` - 文本向量服务客户端与服务端实现
428 - `query/query_parser.py` - 查询解析器(集成向量生成) 428 - `query/query_parser.py` - 查询解析器(集成向量生成)
429 429
430 --- 430 ---
@@ -585,7 +585,7 @@ ranking: @@ -585,7 +585,7 @@ ranking:
585 - ✅ 查询改写(词典配置) 585 - ✅ 查询改写(词典配置)
586 - ✅ 语言检测 586 - ✅ 语言检测
587 - ✅ 多语言翻译(DeepL API) 587 - ✅ 多语言翻译(DeepL API)
588 -- ✅ 文本向量化(BGE-M3 588 +- ✅ 文本向量化(Qwen3-Embedding-0.6B
589 - ✅ 域提取(支持 `domain:query` 语法) 589 - ✅ 域提取(支持 `domain:query` 语法)
590 590
591 ### 6.4 搜索功能 591 ### 6.4 搜索功能
@@ -623,7 +623,7 @@ ranking: @@ -623,7 +623,7 @@ ranking:
623 - **后端**:Python 3.6+ 623 - **后端**:Python 3.6+
624 - **搜索引擎**:Elasticsearch 624 - **搜索引擎**:Elasticsearch
625 - **数据库**:MySQL(Shoplazza) 625 - **数据库**:MySQL(Shoplazza)
626 -- **向量模型**:BGE-M3(文本)、CN-CLIP(图片) 626 +- **向量模型**:Qwen3-Embedding-0.6B(文本)、CN-CLIP(图片)
627 - **翻译服务**:DeepL API 627 - **翻译服务**:DeepL API
628 - **API 框架**:FastAPI 628 - **API 框架**:FastAPI
629 - **前端**:HTML + JavaScript 629 - **前端**:HTML + JavaScript
docs/系统设计文档v1.md
1 # 搜索引擎通用化开发进度 1 # 搜索引擎通用化开发进度
2 2
  3 +> 历史版本说明:本文件为 v1 归档文档,部分模型与实现细节(如 BGE-M3)已过时。当前以 `docs/系统设计文档.md` 与 `docs/QUICKSTART.md` 为准。
  4 +
3 ## 项目概述 5 ## 项目概述
4 6
5 对后端搜索技术 做通用化。 7 对后端搜索技术 做通用化。
docs/索引字段说明v1.md
1 # 索引字段说明文档 1 # 索引字段说明文档
2 2
  3 +> 历史版本说明:本文件为 v1 归档文档,部分模型与实现细节(如 BGE-M3)已过时。当前以 `docs/索引字段说明v2.md` 与 `docs/向量化模块和API说明文档.md` 为准。
  4 +
3 本文档详细说明了 Elasticsearch 索引中所有字段的类型、索引方式、数据来源等信息。 5 本文档详细说明了 Elasticsearch 索引中所有字段的类型、索引方式、数据来源等信息。
4 6
5 ## 设计思路 7 ## 设计思路
docs/索引字段说明v2.md
@@ -352,7 +352,7 @@ @@ -352,7 +352,7 @@
352 352
353 | 字段名 | ES类型 | 维度 | 说明 | 数据来源 | 353 | 字段名 | ES类型 | 维度 | 说明 | 数据来源 |
354 |--------|--------|------|------|----------| 354 |--------|--------|------|------|----------|
355 -| `title_embedding` | dense_vector | 1024 | 标题向量(用于语义搜索) | 由 BGE-M3 模型生成 | 355 +| `title_embedding` | dense_vector | 1024 | 标题向量(用于语义搜索) | 由 Qwen3-Embedding-0.6B 模型生成 |
356 | `image_embedding` | nested | - | 图片向量(用于图片搜索) | 由 CN-CLIP 模型生成 | 356 | `image_embedding` | nested | - | 图片向量(用于图片搜索) | 由 CN-CLIP 模型生成 |
357 357
358 **注意**: 这些字段仅用于搜索,不会返回给前端。 358 **注意**: 这些字段仅用于搜索,不会返回给前端。
@@ -462,4 +462,4 @@ filters AND (text_recall OR embedding_recall) @@ -462,4 +462,4 @@ filters AND (text_recall OR embedding_recall)
462 3. **多语言支持**: 文本字段支持中英文,后端根据 `language` 参数自动选择 462 3. **多语言支持**: 文本字段支持中英文,后端根据 `language` 参数自动选择
463 4. **规格分面**: `specifications` 使用嵌套聚合,按 `name` 分组,然后按 `value` 聚合 463 4. **规格分面**: `specifications` 使用嵌套聚合,按 `name` 分组,然后按 `value` 聚合
464 5. **向量字段**: `title_embedding` 和 `image_embedding` 仅用于搜索,不返回给前端 464 5. **向量字段**: `title_embedding` 和 `image_embedding` 仅用于搜索,不返回给前端
465 -  
466 \ No newline at end of file 465 \ No newline at end of file
  466 +
embeddings/CLOUD_EMBEDDING_README.md deleted
@@ -1,307 +0,0 @@ @@ -1,307 +0,0 @@
1 -# 云端向量化模块 - 更新说明  
2 -  
3 -## 📝 概述  
4 -  
5 -本次更新为 saas-search 项目添加了基于阿里云 DashScope API 的云端文本向量化功能,使用 `text-embedding-v4` 模型。  
6 -  
7 -## 🎯 主要功能  
8 -  
9 -1. **CloudTextEncoder** - 云端文本向量化编码器  
10 - - 单例模式,线程安全  
11 - - 支持单个/批量文本向量化  
12 - - 自动错误处理和降级  
13 - - 生成 1024 维向量  
14 -  
15 -2. **测试脚本** - 使用 queries.txt 测试向量化  
16 - - 读取前 100 条查询  
17 - - 记录每次请求的时间和耗时  
18 - - 统计成功率和性能指标  
19 -  
20 -3. **示例代码** - 展示如何使用模块  
21 - - 单个文本向量化  
22 - - 批量处理  
23 - - 相似度计算  
24 -  
25 -## 📁 文件结构  
26 -  
27 -```  
28 -saas-search/  
29 -├── embeddings/  
30 -│ ├── cloud_text_encoder.py # 云端向量化编码器(新增)  
31 -│ ├── text_encoder.py # 本地编码器(现有)  
32 -│ └── ...  
33 -├── scripts/  
34 -│ ├── test_cloud_embedding.py # 测试脚本(新增)  
35 -│ └── ...  
36 -├── examples/  
37 -│ └── cloud_embedding_example.py # 示例代码(新增)  
38 -├── docs/  
39 -│ ├── cloud_embedding_usage.md # 详细文档(新增)  
40 -│ └── cloud_embedding_quickstart.md # 快速入门(新增)  
41 -├── data_crawling/  
42 -│ └── queries.txt # 测试数据  
43 -├── requirements.txt # 已添加 openai>=1.0.0  
44 -└── CLOUD_EMBEDDING_README.md # 本文档(新增)  
45 -```  
46 -  
47 -## 🚀 快速开始  
48 -  
49 -### 1. 安装依赖  
50 -  
51 -```bash  
52 -pip install openai  
53 -```  
54 -  
55 -或使用项目 requirements:  
56 -```bash  
57 -pip install -r requirements.txt  
58 -```  
59 -  
60 -### 2. 设置 API Key  
61 -  
62 -```bash  
63 -export DASHSCOPE_API_KEY="sk-your-api-key-here"  
64 -```  
65 -  
66 -获取 API Key:https://help.aliyun.com/zh/model-studio/get-api-key  
67 -  
68 -### 3. 运行测试  
69 -  
70 -```bash  
71 -# 测试向量化(使用 queries.txt 前 100 条)  
72 -python scripts/test_cloud_embedding.py  
73 -  
74 -# 运行示例代码  
75 -python examples/cloud_embedding_example.py  
76 -```  
77 -  
78 -## 📖 使用方法  
79 -  
80 -### 基础使用  
81 -  
82 -```python  
83 -from embeddings.cloud_text_encoder import CloudTextEncoder  
84 -  
85 -# 初始化编码器  
86 -encoder = CloudTextEncoder()  
87 -  
88 -# 单个文本向量化  
89 -embedding = encoder.encode("衣服的质量杠杠的")  
90 -print(embedding.shape) # (1, 1024)  
91 -  
92 -# 批量向量化  
93 -embeddings = encoder.encode(["文本1", "文本2", "文本3"])  
94 -print(embeddings.shape) # (3, 1024)  
95 -```  
96 -  
97 -### 批量处理  
98 -  
99 -```python  
100 -# 大批量自动分批处理  
101 -texts = [f"商品 {i}" for i in range(1000)]  
102 -embeddings = encoder.encode_batch(texts, batch_size=32)  
103 -```  
104 -  
105 -## 🧪 测试脚本  
106 -  
107 -测试脚本 `scripts/test_cloud_embedding.py` 功能:  
108 -  
109 -✅ 读取 `data_crawling/queries.txt` 前 100 条查询  
110 -✅ 逐条发送向量化请求  
111 -✅ 记录每次请求的发送时间、接收时间、耗时  
112 -✅ 输出向量维度和内容  
113 -✅ 统计成功率、平均耗时、吞吐量  
114 -  
115 -### 测试输出示例  
116 -  
117 -```  
118 -================================================================================  
119 -Cloud Text Embedding Test - Aliyun DashScope API  
120 -================================================================================  
121 -  
122 -[ 1/100] ✓ SUCCESS  
123 - Query: Bohemian Maxi Dress  
124 - Send Time: 2025-12-05 10:30:45.123  
125 - Receive Time: 2025-12-05 10:30:45.456  
126 - Duration: 0.333s  
127 - Embedding Shape: (1, 1024)  
128 -  
129 -...  
130 -  
131 -================================================================================  
132 -Test Summary  
133 -================================================================================  
134 -Total Queries: 100  
135 -Successful: 100  
136 -Failed: 0  
137 -Success Rate: 100.0%  
138 -Total Time: 35.123s  
139 -Total API Time: 32.456s  
140 -Average Duration: 0.325s per query  
141 -Throughput: 2.85 queries/second  
142 -================================================================================  
143 -```  
144 -  
145 -## 📊 性能特点  
146 -  
147 -- **向量维度**:1024  
148 -- **平均延迟**:300-400ms/请求  
149 -- **吞吐量**:~2-3 queries/秒(单线程)  
150 -- **错误处理**:自动降级到零向量  
151 -- **批处理**:支持自动分批和速率控制  
152 -  
153 -## 🔧 接口说明  
154 -  
155 -### CloudTextEncoder API  
156 -  
157 -#### 初始化  
158 -  
159 -```python  
160 -CloudTextEncoder(api_key=None, base_url=None)  
161 -```  
162 -  
163 -参数:  
164 -- `api_key` (str, optional): API Key,默认从环境变量读取  
165 -- `base_url` (str, optional): API 端点,默认北京地域  
166 -  
167 -#### encode()  
168 -  
169 -```python  
170 -encode(sentences, normalize_embeddings=True, device='cpu', batch_size=32)  
171 -```  
172 -  
173 -参数:  
174 -- `sentences` (str or List[str]): 单个文本或文本列表  
175 -- `normalize_embeddings` (bool): 是否归一化(API 自动处理)  
176 -- `device` (str): 设备参数(兼容性参数,云端 API 忽略)  
177 -- `batch_size` (int): 批处理大小  
178 -  
179 -返回:  
180 -- `np.ndarray`: 形状为 (n, 1024) 的 numpy 数组  
181 -  
182 -#### encode_batch()  
183 -  
184 -```python  
185 -encode_batch(texts, batch_size=32, device='cpu')  
186 -```  
187 -  
188 -参数:  
189 -- `texts` (List[str]): 文本列表  
190 -- `batch_size` (int): 批处理大小  
191 -- `device` (str): 设备参数(兼容性参数)  
192 -  
193 -返回:  
194 -- `np.ndarray`: 向量矩阵  
195 -  
196 -## 📚 文档  
197 -  
198 -- **快速入门**:`docs/cloud_embedding_quickstart.md`  
199 -- **详细文档**:`docs/cloud_embedding_usage.md`  
200 -- **示例代码**:`examples/cloud_embedding_example.py`  
201 -  
202 -## ⚠️ 注意事项  
203 -  
204 -1. **API Key 管理**:妥善保管 API Key,不要提交到代码仓库  
205 -2. **成本控制**:云端 API 按使用量计费,注意控制调用次数  
206 -3. **速率限制**:注意 API 速率限制,测试脚本已添加延迟  
207 -4. **网络依赖**:需要稳定的网络连接  
208 -5. **错误处理**:API 失败时会返回零向量,请检查日志  
209 -  
210 -## 🆚 对比本地编码器  
211 -  
212 -| 特性 | CloudTextEncoder | BgeEncoder (本地) |  
213 -|------|------------------|-------------------|  
214 -| 部署方式 | 云端 API | 本地服务 |  
215 -| 初始成本 | 低 | 高(GPU/CPU) |  
216 -| 运行成本 | 按使用付费 | 固定 |  
217 -| 延迟 | ~300-400ms | <100ms |  
218 -| 离线使用 | ❌ | ✅ |  
219 -| 维护成本 | 低 | 需要维护 |  
220 -| 扩展性 | 自动扩展 | 手动扩展 |  
221 -  
222 -## 🔄 集成建议  
223 -  
224 -### 选择使用场景  
225 -  
226 -**使用 CloudTextEncoder(云端):**  
227 -- 初期开发和测试  
228 -- 查询量不大的应用  
229 -- 不需要离线支持  
230 -- 希望降低运维成本  
231 -  
232 -**使用 BgeEncoder(本地):**  
233 -- 大规模生产环境  
234 -- 需要低延迟  
235 -- 离线使用场景  
236 -- 查询量非常大  
237 -  
238 -### 混合使用  
239 -  
240 -```python  
241 -# 配置文件中选择编码器类型  
242 -ENCODER_TYPE = os.getenv("ENCODER_TYPE", "local") # local or cloud  
243 -  
244 -if ENCODER_TYPE == "cloud":  
245 - from embeddings.cloud_text_encoder import CloudTextEncoder  
246 - encoder = CloudTextEncoder()  
247 -else:  
248 - from embeddings.text_encoder import BgeEncoder  
249 - encoder = BgeEncoder()  
250 -  
251 -# 使用统一接口  
252 -embeddings = encoder.encode(texts)  
253 -```  
254 -  
255 -## 🐛 故障排查  
256 -  
257 -### 问题 1:API Key 未设置  
258 -```bash  
259 -export DASHSCOPE_API_KEY="sk-your-key"  
260 -```  
261 -  
262 -### 问题 2:网络连接失败  
263 -- 检查网络连接  
264 -- 验证 base_url 是否正确  
265 -- 确认防火墙设置  
266 -  
267 -### 问题 3:速率限制  
268 -- 减小 batch_size  
269 -- 增加请求间隔  
270 -- 升级 API 套餐  
271 -  
272 -### 问题 4:返回零向量  
273 -- 检查日志中的错误信息  
274 -- 验证 API Key 是否有效  
275 -- 确认账户余额  
276 -  
277 -## 🎓 示例代码  
278 -  
279 -查看 `examples/cloud_embedding_example.py` 了解完整示例:  
280 -- 单个/批量文本向量化  
281 -- 相似度计算  
282 -- 错误处理  
283 -  
284 -## 📞 支持  
285 -  
286 -- 项目文档:`docs/` 目录  
287 -- 阿里云文档:https://help.aliyun.com/zh/model-studio/  
288 -- API 文档:https://help.aliyun.com/zh/model-studio/getting-started/models  
289 -  
290 -## ✅ 验证清单  
291 -  
292 -完成以下步骤确认模块正常工作:  
293 -  
294 -- [ ] 安装了 openai 包  
295 -- [ ] 设置了 DASHSCOPE_API_KEY 环境变量  
296 -- [ ] 运行测试脚本成功  
297 -- [ ] 查看了示例代码  
298 -- [ ] 阅读了文档  
299 -  
300 -## 📅 更新日期  
301 -  
302 -2025-12-05  
303 -  
304 -## 👨‍💻 维护  
305 -  
306 -如有问题或建议,请联系项目维护者。  
307 -  
embeddings/README.md
@@ -7,12 +7,20 @@ @@ -7,12 +7,20 @@
7 这个目录是一个完整的“向量化模块”,包含: 7 这个目录是一个完整的“向量化模块”,包含:
8 8
9 - **HTTP 客户端**:`text_encoder.py` / `image_encoder.py`(供搜索/索引模块调用) 9 - **HTTP 客户端**:`text_encoder.py` / `image_encoder.py`(供搜索/索引模块调用)
10 -- **本地模型实现**:`bge_model.py` / `clip_model.py` 10 +- **本地模型实现**:`qwen3_model.py` / `clip_model.py`
11 - **clip-as-service 客户端**:`clip_as_service_encoder.py`(图片向量,推荐) 11 - **clip-as-service 客户端**:`clip_as_service_encoder.py`(图片向量,推荐)
12 - **向量化服务(FastAPI)**:`server.py` 12 - **向量化服务(FastAPI)**:`server.py`
13 - **统一配置**:`config.py` 13 - **统一配置**:`config.py`
14 - **接口契约**:`protocols.ImageEncoderProtocol`(图片编码统一为 `encode_image_urls(urls, batch_size)`,本地 CN-CLIP 与 clip-as-service 均实现该接口) 14 - **接口契约**:`protocols.ImageEncoderProtocol`(图片编码统一为 `encode_image_urls(urls, batch_size)`,本地 CN-CLIP 与 clip-as-service 均实现该接口)
15 15
  16 +说明:历史上的云端 embedding 试验实现(DashScope)已从主仓库移除,当前仅维护 6005 这条统一向量服务链路。
  17 +
  18 +### 文本向量后端(默认)
  19 +
  20 +- 6005 文本向量服务默认模型:`Qwen/Qwen3-Embedding-0.6B`
  21 +- 实现方式:`SentenceTransformer`(`trust_remote_code=True`)
  22 +- 向量归一化:默认开启(可通过环境变量关闭)
  23 +
16 ### 服务接口 24 ### 服务接口
17 25
18 - `POST /embed/text` 26 - `POST /embed/text`
@@ -54,7 +62,6 @@ @@ -54,7 +62,6 @@
54 编辑 `embeddings/config.py`: 62 编辑 `embeddings/config.py`:
55 63
56 - `PORT`: 服务端口(默认 6005) 64 - `PORT`: 服务端口(默认 6005)
57 -- `TEXT_MODEL_DIR`, `TEXT_DEVICE`, `TEXT_BATCH_SIZE` 65 +- `TEXT_MODEL_ID`, `TEXT_DEVICE`, `TEXT_BATCH_SIZE`, `TEXT_NORMALIZE_EMBEDDINGS`
58 - `USE_CLIP_AS_SERVICE`, `CLIP_AS_SERVICE_SERVER`:图片向量(clip-as-service) 66 - `USE_CLIP_AS_SERVICE`, `CLIP_AS_SERVICE_SERVER`:图片向量(clip-as-service)
59 - `IMAGE_MODEL_NAME`, `IMAGE_DEVICE`:本地 CN-CLIP(当 `USE_CLIP_AS_SERVICE=false` 时) 67 - `IMAGE_MODEL_NAME`, `IMAGE_DEVICE`:本地 CN-CLIP(当 `USE_CLIP_AS_SERVICE=false` 时)
60 -  
embeddings/__init__.py
@@ -4,7 +4,8 @@ Embeddings module. @@ -4,7 +4,8 @@ Embeddings module.
4 Important: keep package import lightweight. 4 Important: keep package import lightweight.
5 5
6 Some callers do: 6 Some callers do:
7 - - `from embeddings import BgeEncoder` 7 + - `from embeddings import TextEmbeddingEncoder`
  8 + - `from embeddings import BgeEncoder` (deprecated alias)
8 - `from embeddings import CLIPImageEncoder` 9 - `from embeddings import CLIPImageEncoder`
9 10
10 But the underlying implementations may import heavy optional deps (Pillow, torch, etc). 11 But the underlying implementations may import heavy optional deps (Pillow, torch, etc).
@@ -13,15 +14,19 @@ without importing client code), we provide small lazy factories here. @@ -13,15 +14,19 @@ without importing client code), we provide small lazy factories here.
13 """ 14 """
14 15
15 16
16 -class BgeEncoder(object):  
17 - """Lazy factory for `embeddings.text_encoder.BgeEncoder`.""" 17 +class TextEmbeddingEncoder(object):
  18 + """Lazy factory for `embeddings.text_encoder.TextEmbeddingEncoder`."""
18 19
19 def __new__(cls, *args, **kwargs): 20 def __new__(cls, *args, **kwargs):
20 - from .text_encoder import BgeEncoder as _Real 21 + from .text_encoder import TextEmbeddingEncoder as _Real
21 22
22 return _Real(*args, **kwargs) 23 return _Real(*args, **kwargs)
23 24
24 25
  26 +class BgeEncoder(TextEmbeddingEncoder):
  27 + """Deprecated backward-compatible alias for old class name."""
  28 +
  29 +
25 class CLIPImageEncoder(object): 30 class CLIPImageEncoder(object):
26 """Lazy factory for `embeddings.image_encoder.CLIPImageEncoder`.""" 31 """Lazy factory for `embeddings.image_encoder.CLIPImageEncoder`."""
27 32
@@ -31,4 +36,4 @@ class CLIPImageEncoder(object): @@ -31,4 +36,4 @@ class CLIPImageEncoder(object):
31 return _Real(*args, **kwargs) 36 return _Real(*args, **kwargs)
32 37
33 38
34 -__all__ = ["BgeEncoder", "CLIPImageEncoder"] 39 +__all__ = ["TextEmbeddingEncoder", "BgeEncoder", "CLIPImageEncoder"]
embeddings/cloud_embedding_usage.md deleted
@@ -1,231 +0,0 @@ @@ -1,231 +0,0 @@
1 -# 云端向量化模块使用说明  
2 -  
3 -## 概述  
4 -  
5 -本项目新增了基于阿里云 DashScope API 的文本向量化模块,使用 `text-embedding-v4` 模型生成文本向量。  
6 -  
7 -## 模块说明  
8 -  
9 -### 1. CloudTextEncoder (`embeddings/cloud_text_encoder.py`)  
10 -  
11 -云端文本向量化编码器,使用阿里云 DashScope API。  
12 -  
13 -**特性:**  
14 -- 单例模式,线程安全  
15 -- 支持单个或批量文本向量化  
16 -- 自动处理 API 调用和错误处理  
17 -- 生成 1024 维向量  
18 -- 支持批处理以避免 API 速率限制  
19 -  
20 -**初始化:**  
21 -```python  
22 -from embeddings.cloud_text_encoder import CloudTextEncoder  
23 -  
24 -# 方式1:从环境变量读取 API Key  
25 -encoder = CloudTextEncoder()  
26 -  
27 -# 方式2:显式传入 API Key  
28 -encoder = CloudTextEncoder(api_key="sk-xxx")  
29 -  
30 -# 方式3:使用新加坡地域  
31 -encoder = CloudTextEncoder(  
32 - api_key="sk-xxx",  
33 - base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"  
34 -)  
35 -```  
36 -  
37 -**使用示例:**  
38 -```python  
39 -# 单个文本向量化  
40 -text = "衣服的质量杠杠的"  
41 -embedding = encoder.encode(text)  
42 -print(embedding.shape) # (1, 1024)  
43 -  
44 -# 批量文本向量化  
45 -texts = ["文本1", "文本2", "文本3"]  
46 -embeddings = encoder.encode(texts)  
47 -print(embeddings.shape) # (3, 1024)  
48 -  
49 -# 大批量处理(自动分批)  
50 -large_texts = ["文本" + str(i) for i in range(1000)]  
51 -embeddings = encoder.encode_batch(large_texts, batch_size=32)  
52 -print(embeddings.shape) # (1000, 1024)  
53 -```  
54 -  
55 -### 2. 测试脚本 (`scripts/test_cloud_embedding.py`)  
56 -  
57 -测试云端向量化功能,读取 `queries.txt` 前100条数据进行测试。  
58 -  
59 -**功能:**  
60 -- 读取查询文件  
61 -- 逐条发送向量化请求  
62 -- 记录每次请求的发送时间、接收时间和耗时  
63 -- 统计成功率和平均耗时  
64 -  
65 -## 使用步骤  
66 -  
67 -### 1. 设置 API Key  
68 -  
69 -首先需要获取阿里云 DashScope API Key:  
70 -- 北京地域:https://help.aliyun.com/zh/model-studio/get-api-key  
71 -- 新加坡地域:使用不同的 API Key  
72 -  
73 -设置环境变量:  
74 -```bash  
75 -export DASHSCOPE_API_KEY="your-api-key-here"  
76 -```  
77 -  
78 -或者在项目根目录的 `.env` 文件中添加:  
79 -```  
80 -DASHSCOPE_API_KEY=your-api-key-here  
81 -```  
82 -  
83 -### 2. 安装依赖  
84 -  
85 -确保已安装 OpenAI Python SDK:  
86 -```bash  
87 -pip install openai  
88 -```  
89 -  
90 -或者使用项目的 requirements.txt:  
91 -```bash  
92 -pip install -r requirements.txt  
93 -```  
94 -  
95 -### 3. 运行测试脚本  
96 -  
97 -```bash  
98 -# 在项目根目录下运行  
99 -python scripts/test_cloud_embedding.py  
100 -```  
101 -  
102 -### 4. 查看测试结果  
103 -  
104 -测试脚本会输出以下信息:  
105 -```  
106 -================================================================================  
107 -Cloud Text Embedding Test - Aliyun DashScope API  
108 -================================================================================  
109 -  
110 -API Key: sk-xxxxxx...xxxx  
111 -  
112 -Reading queries from: /path/to/queries.txt  
113 -Successfully read 100 queries  
114 -  
115 -Initializing CloudTextEncoder...  
116 -CloudTextEncoder initialized successfully  
117 -  
118 -================================================================================  
119 -Testing 100 queries (one by one)  
120 -================================================================================  
121 -  
122 -[ 1/100] ✓ SUCCESS  
123 - Query: Bohemian Maxi Dress  
124 - Send Time: 2025-12-05 10:30:45.123  
125 - Receive Time: 2025-12-05 10:30:45.456  
126 - Duration: 0.333s  
127 - Embedding Shape: (1, 1024)  
128 -  
129 -[ 2/100] ✓ SUCCESS  
130 - Query: Vintage Denim Jacket  
131 - Send Time: 2025-12-05 10:30:45.789  
132 - Receive Time: 2025-12-05 10:30:46.012  
133 - Duration: 0.223s  
134 - Embedding Shape: (1, 1024)  
135 -  
136 -...  
137 -  
138 -================================================================================  
139 -Test Summary  
140 -================================================================================  
141 -Total Queries: 100  
142 -Successful: 100  
143 -Failed: 0  
144 -Success Rate: 100.0%  
145 -Total Time: 35.123s  
146 -Total API Time: 32.456s  
147 -Average Duration: 0.325s per query  
148 -Throughput: 2.85 queries/second  
149 -================================================================================  
150 -```  
151 -  
152 -## 注意事项  
153 -  
154 -1. **API 限制**:阿里云 DashScope API 有速率限制,测试脚本在批处理时会添加小延迟以避免触发限制。  
155 -  
156 -2. **成本**:使用云端 API 会产生费用,请注意控制调用量。  
157 -  
158 -3. **网络要求**:需要稳定的网络连接访问阿里云服务。  
159 -  
160 -4. **错误处理**:如果 API 调用失败,编码器会返回零向量作为降级处理,并记录错误日志。  
161 -  
162 -5. **向量维度**:`text-embedding-v4` 模型生成 1024 维向量,如果需要不同维度,请考虑使用其他模型。  
163 -  
164 -## 地域选择  
165 -  
166 -- **北京地域**(默认):`https://dashscope.aliyuncs.com/compatible-mode/v1`  
167 -- **新加坡地域**:`https://dashscope-intl.aliyuncs.com/compatible-mode/v1`  
168 -  
169 -不同地域使用不同的 API Key,请确保匹配。  
170 -  
171 -## 集成到项目  
172 -  
173 -在项目中使用云端向量化:  
174 -  
175 -```python  
176 -from embeddings.cloud_text_encoder import CloudTextEncoder  
177 -  
178 -# 初始化编码器(全局单例)  
179 -encoder = CloudTextEncoder()  
180 -  
181 -# 在搜索、索引等模块中使用  
182 -def process_text(text: str):  
183 - embedding = encoder.encode(text)  
184 - # 使用 embedding 进行后续处理  
185 - return embedding  
186 -```  
187 -  
188 -## 对比本地编码器  
189 -  
190 -| 特性 | CloudTextEncoder | BgeEncoder (本地) |  
191 -|------|------------------|-------------------|  
192 -| 部署方式 | 云端 API | 本地服务 |  
193 -| 初始成本 | 低(按使用付费) | 高(GPU/CPU 资源) |  
194 -| 运行成本 | 按调用量计费 | 固定资源成本 |  
195 -| 延迟 | 较高(网络往返) | 低(本地处理) |  
196 -| 吞吐量 | 受 API 限制 | 受硬件限制 |  
197 -| 离线使用 | 不支持 | 支持 |  
198 -| 维护成本 | 低 | 需要维护服务 |  
199 -  
200 -## 故障排除  
201 -  
202 -### 问题:API Key 未设置  
203 -```  
204 -ERROR: DASHSCOPE_API_KEY environment variable is not set!  
205 -```  
206 -**解决**:设置 `DASHSCOPE_API_KEY` 环境变量。  
207 -  
208 -### 问题:API 调用失败  
209 -```  
210 -Failed to encode texts via DashScope API: ...  
211 -```  
212 -**解决**:  
213 -1. 检查网络连接  
214 -2. 验证 API Key 是否正确  
215 -3. 确认 base_url 与 API Key 地域匹配  
216 -4. 检查账户余额  
217 -  
218 -### 问题:速率限制  
219 -```  
220 -Rate limit exceeded  
221 -```  
222 -**解决**:  
223 -1. 增加批处理之间的延迟  
224 -2. 减小 batch_size  
225 -3. 升级 API 套餐  
226 -  
227 -## 更多信息  
228 -  
229 -- [阿里云模型服务灵积文档](https://help.aliyun.com/zh/model-studio/)  
230 -- [text-embedding-v4 模型说明](https://help.aliyun.com/zh/model-studio/getting-started/models)  
231 -  
embeddings/cloud_text_encoder.py deleted
@@ -1,142 +0,0 @@ @@ -1,142 +0,0 @@
1 -"""  
2 -Text embedding encoder using Aliyun DashScope API.  
3 -  
4 -Generates embeddings via Aliyun's text-embedding-v4 model.  
5 -"""  
6 -  
7 -import os  
8 -import logging  
9 -import threading  
10 -import time  
11 -import numpy as np  
12 -from typing import List, Union  
13 -from openai import OpenAI  
14 -  
15 -logger = logging.getLogger(__name__)  
16 -  
17 -  
18 -class CloudTextEncoder:  
19 - """  
20 - Singleton text encoder using Aliyun DashScope API.  
21 -  
22 - Thread-safe singleton pattern ensures only one instance exists.  
23 - Uses text-embedding-v4 model for generating embeddings.  
24 - """  
25 - _instance = None  
26 - _lock = threading.Lock()  
27 -  
28 - def __new__(cls, api_key: str = None, base_url: str = None):  
29 - with cls._lock:  
30 - if cls._instance is None:  
31 - cls._instance = super(CloudTextEncoder, cls).__new__(cls)  
32 -  
33 - # Get API key from parameter or environment variable  
34 - api_key = api_key or os.getenv("DASHSCOPE_API_KEY")  
35 - if not api_key:  
36 - raise ValueError("DASHSCOPE_API_KEY must be set in environment or passed as parameter")  
37 -  
38 - # 以下是北京地域base-url,如果使用新加坡地域的模型,需要将base_url替换为:https://dashscope-intl.aliyuncs.com/compatible-mode/v1  
39 - base_url = base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1"  
40 -  
41 - cls._instance.client = OpenAI(  
42 - api_key=api_key,  
43 - base_url=base_url  
44 - )  
45 - cls._instance.model = "text-embedding-v4"  
46 - logger.info(f"Created CloudTextEncoder instance with base_url: {base_url}")  
47 -  
48 - return cls._instance  
49 -  
50 - def encode(  
51 - self,  
52 - sentences: Union[str, List[str]],  
53 - normalize_embeddings: bool = True,  
54 - device: str = 'cpu',  
55 - batch_size: int = 32  
56 - ) -> np.ndarray:  
57 - """  
58 - Encode text into embeddings via Aliyun DashScope API.  
59 -  
60 - Args:  
61 - sentences: Single string or list of strings to encode  
62 - normalize_embeddings: Whether to normalize embeddings (handled by API)  
63 - device: Device parameter (ignored, for compatibility)  
64 - batch_size: Batch size for processing (currently processes all at once)  
65 -  
66 - Returns:  
67 - numpy array of shape (n, dimension) containing embeddings  
68 - """  
69 - # Convert single string to list  
70 - if isinstance(sentences, str):  
71 - sentences = [sentences]  
72 -  
73 - if not sentences:  
74 - return np.array([])  
75 -  
76 - try:  
77 - # Call DashScope API  
78 - start_time = time.time()  
79 - completion = self.client.embeddings.create(  
80 - model=self.model,  
81 - input=sentences  
82 - )  
83 - elapsed_time = time.time() - start_time  
84 -  
85 - logger.info(f"Generated embeddings for {len(sentences)} texts in {elapsed_time:.3f}s")  
86 -  
87 - # Extract embeddings from response  
88 - embeddings = []  
89 - for item in completion.data:  
90 - embeddings.append(item.embedding)  
91 -  
92 - return np.array(embeddings, dtype=np.float32)  
93 -  
94 - except Exception as e:  
95 - logger.error(f"Failed to encode texts via DashScope API: {e}", exc_info=True)  
96 - # Return zero embeddings as fallback (dimension based on text-embedding-v4)  
97 - # text-embedding-v4 typically returns 1024-dimensional vectors  
98 - return np.zeros((len(sentences), 1024), dtype=np.float32)  
99 -  
100 - def encode_batch(  
101 - self,  
102 - texts: List[str],  
103 - batch_size: int = 32,  
104 - device: str = 'cpu'  
105 - ) -> np.ndarray:  
106 - """  
107 - Encode a batch of texts via Aliyun DashScope API.  
108 -  
109 - Args:  
110 - texts: List of texts to encode  
111 - batch_size: Batch size for processing  
112 - device: Device parameter (ignored, for compatibility)  
113 -  
114 - Returns:  
115 - numpy array of embeddings  
116 - """  
117 - if not texts:  
118 - return np.array([])  
119 -  
120 - # Process in batches to avoid API limits  
121 - all_embeddings = []  
122 -  
123 - for i in range(0, len(texts), batch_size):  
124 - batch = texts[i:i + batch_size]  
125 - embeddings = self.encode(batch, device=device)  
126 - all_embeddings.append(embeddings)  
127 -  
128 - # Small delay to avoid rate limiting  
129 - if i + batch_size < len(texts):  
130 - time.sleep(0.1)  
131 -  
132 - return np.vstack(all_embeddings) if all_embeddings else np.array([])  
133 -  
134 - def get_embedding_dimension(self) -> int:  
135 - """  
136 - Get the dimension of embeddings produced by this encoder.  
137 -  
138 - Returns:  
139 - Embedding dimension (1024 for text-embedding-v4)  
140 - """  
141 - return 1024  
142 -  
embeddings/config.py
@@ -16,10 +16,13 @@ class EmbeddingConfig(object): @@ -16,10 +16,13 @@ class EmbeddingConfig(object):
16 HOST = os.getenv("EMBEDDING_HOST", "0.0.0.0") 16 HOST = os.getenv("EMBEDDING_HOST", "0.0.0.0")
17 PORT = int(os.getenv("EMBEDDING_PORT", 6005)) 17 PORT = int(os.getenv("EMBEDDING_PORT", 6005))
18 18
19 - # Text embeddings (BGE-M3)  
20 - TEXT_MODEL_DIR = "Xorbits/bge-m3"  
21 - TEXT_DEVICE = "cuda" # "cuda" or "cpu" (model may fall back to CPU if needed)  
22 - TEXT_BATCH_SIZE = 32 19 + # Text embeddings (Qwen3-Embedding-0.6B, Sentence Transformers)
  20 + TEXT_MODEL_ID = os.getenv("TEXT_MODEL_ID", "Qwen/Qwen3-Embedding-0.6B")
  21 + # Backward-compatible alias for old naming in docs/scripts.
  22 + TEXT_MODEL_DIR = TEXT_MODEL_ID
  23 + TEXT_DEVICE = os.getenv("TEXT_DEVICE", "cuda") # "cuda" or "cpu"
  24 + TEXT_BATCH_SIZE = int(os.getenv("TEXT_BATCH_SIZE", "32"))
  25 + TEXT_NORMALIZE_EMBEDDINGS = os.getenv("TEXT_NORMALIZE_EMBEDDINGS", "true").lower() in ("1", "true", "yes")
23 26
24 # Image embeddings 27 # Image embeddings
25 # Option A: clip-as-service (Jina CLIP server, recommended) 28 # Option A: clip-as-service (Jina CLIP server, recommended)
@@ -36,4 +39,3 @@ class EmbeddingConfig(object): @@ -36,4 +39,3 @@ class EmbeddingConfig(object):
36 39
37 CONFIG = EmbeddingConfig() 40 CONFIG = EmbeddingConfig()
38 41
39 -  
embeddings/image_encoder.py
1 -"""  
2 -Image embedding encoder using network service. 1 +"""Image embedding client for the local embedding HTTP service."""
3 2
4 -Generates embeddings via HTTP API service (default localhost:6005).  
5 -"""  
6 -  
7 -import sys  
8 import os 3 import os
9 -import requests 4 +import logging
  5 +from typing import Any, List, Optional, Union
  6 +
10 import numpy as np 7 import numpy as np
  8 +import requests
11 from PIL import Image 9 from PIL import Image
12 -import logging  
13 -import threading  
14 -from typing import List, Optional, Union, Dict, Any  
15 10
16 logger = logging.getLogger(__name__) 11 logger = logging.getLogger(__name__)
17 12
@@ -22,21 +17,14 @@ class CLIPImageEncoder: @@ -22,21 +17,14 @@ class CLIPImageEncoder:
22 """ 17 """
23 Image Encoder for generating image embeddings using network service. 18 Image Encoder for generating image embeddings using network service.
24 19
25 - Thread-safe singleton pattern. 20 + This client is stateless and safe to instantiate per caller.
26 """ 21 """
27 22
28 - _instance = None  
29 - _lock = threading.Lock()  
30 -  
31 - def __new__(cls, service_url: Optional[str] = None):  
32 - with cls._lock:  
33 - if cls._instance is None:  
34 - cls._instance = super(CLIPImageEncoder, cls).__new__(cls)  
35 - resolved_url = service_url or os.getenv("EMBEDDING_SERVICE_URL") or get_embedding_base_url()  
36 - logger.info(f"Creating CLIPImageEncoder instance with service URL: {resolved_url}")  
37 - cls._instance.service_url = resolved_url  
38 - cls._instance.endpoint = f"{resolved_url}/embed/image"  
39 - return cls._instance 23 + def __init__(self, service_url: Optional[str] = None):
  24 + resolved_url = service_url or os.getenv("EMBEDDING_SERVICE_URL") or get_embedding_base_url()
  25 + self.service_url = str(resolved_url).rstrip("/")
  26 + self.endpoint = f"{self.service_url}/embed/image"
  27 + logger.info("Creating CLIPImageEncoder instance with service URL: %s", self.service_url)
40 28
41 def _call_service(self, request_data: List[str]) -> List[Any]: 29 def _call_service(self, request_data: List[str]) -> List[Any]:
42 """ 30 """
embeddings/bge_model.py renamed to embeddings/qwen3_model.py
1 """ 1 """
2 -BGE-M3 local text embedding implementation. 2 +Qwen3-Embedding-0.6B local text embedding implementation.
3 3
4 Internal model implementation used by the embedding service. 4 Internal model implementation used by the embedding service.
5 """ 5 """
@@ -9,22 +9,21 @@ from typing import List, Union @@ -9,22 +9,21 @@ from typing import List, Union
9 9
10 import numpy as np 10 import numpy as np
11 from sentence_transformers import SentenceTransformer 11 from sentence_transformers import SentenceTransformer
12 -from modelscope import snapshot_download  
13 12
14 13
15 -class BgeTextModel(object): 14 +class Qwen3TextModel(object):
16 """ 15 """
17 - Thread-safe singleton text encoder using BGE-M3 model (local inference). 16 + Thread-safe singleton text encoder using Qwen3-Embedding-0.6B (local inference).
18 """ 17 """
19 18
20 _instance = None 19 _instance = None
21 _lock = threading.Lock() 20 _lock = threading.Lock()
22 21
23 - def __new__(cls, model_dir: str = "Xorbits/bge-m3"): 22 + def __new__(cls, model_id: str = "Qwen/Qwen3-Embedding-0.6B"):
24 with cls._lock: 23 with cls._lock:
25 if cls._instance is None: 24 if cls._instance is None:
26 - cls._instance = super(BgeTextModel, cls).__new__(cls)  
27 - cls._instance.model = SentenceTransformer(snapshot_download(model_dir)) 25 + cls._instance = super(Qwen3TextModel, cls).__new__(cls)
  26 + cls._instance.model = SentenceTransformer(model_id, trust_remote_code=True)
28 return cls._instance 27 return cls._instance
29 28
30 def encode( 29 def encode(
@@ -37,7 +36,7 @@ class BgeTextModel(object): @@ -37,7 +36,7 @@ class BgeTextModel(object):
37 if device == "gpu": 36 if device == "gpu":
38 device = "cuda" 37 device = "cuda"
39 38
40 - # Try requested device, fallback to CPU if CUDA fails 39 + # Try requested device, fallback to CPU if CUDA is unavailable/insufficient.
41 try: 40 try:
42 if device == "cuda": 41 if device == "cuda":
43 import torch 42 import torch
@@ -61,7 +60,6 @@ class BgeTextModel(object): @@ -61,7 +60,6 @@ class BgeTextModel(object):
61 batch_size=batch_size, 60 batch_size=batch_size,
62 ) 61 )
63 return embeddings 62 return embeddings
64 -  
65 except Exception: 63 except Exception:
66 if device != "cpu": 64 if device != "cpu":
67 self.model = self.model.to("cpu") 65 self.model = self.model.to("cpu")
@@ -75,7 +73,17 @@ class BgeTextModel(object): @@ -75,7 +73,17 @@ class BgeTextModel(object):
75 return embeddings 73 return embeddings
76 raise 74 raise
77 75
78 - def encode_batch(self, texts: List[str], batch_size: int = 32, device: str = "cuda") -> np.ndarray:  
79 - return self.encode(texts, batch_size=batch_size, device=device)  
80 - 76 + def encode_batch(
  77 + self,
  78 + texts: List[str],
  79 + batch_size: int = 32,
  80 + device: str = "cuda",
  81 + normalize_embeddings: bool = True,
  82 + ) -> np.ndarray:
  83 + return self.encode(
  84 + texts,
  85 + batch_size=batch_size,
  86 + device=device,
  87 + normalize_embeddings=normalize_embeddings,
  88 + )
81 89
embeddings/server.py
@@ -14,9 +14,6 @@ import numpy as np @@ -14,9 +14,6 @@ import numpy as np
14 from fastapi import FastAPI 14 from fastapi import FastAPI
15 15
16 from embeddings.config import CONFIG 16 from embeddings.config import CONFIG
17 -from embeddings.bge_model import BgeTextModel  
18 -from embeddings.clip_model import ClipImageModel  
19 -from embeddings.clip_as_service_encoder import ClipAsServiceImageEncoder  
20 from embeddings.protocols import ImageEncoderProtocol 17 from embeddings.protocols import ImageEncoderProtocol
21 18
22 logger = logging.getLogger(__name__) 19 logger = logging.getLogger(__name__)
@@ -24,7 +21,7 @@ logger = logging.getLogger(__name__) @@ -24,7 +21,7 @@ logger = logging.getLogger(__name__)
24 app = FastAPI(title="saas-search Embedding Service", version="1.0.0") 21 app = FastAPI(title="saas-search Embedding Service", version="1.0.0")
25 22
26 # Models are loaded at startup, not lazily 23 # Models are loaded at startup, not lazily
27 -_text_model: Optional[BgeTextModel] = None 24 +_text_model: Optional[Any] = None
28 _image_model: Optional[ImageEncoderProtocol] = None 25 _image_model: Optional[ImageEncoderProtocol] = None
29 open_text_model = True 26 open_text_model = True
30 open_image_model = True # Enable image embedding when using clip-as-service 27 open_image_model = True # Enable image embedding when using clip-as-service
@@ -43,8 +40,10 @@ def load_models(): @@ -43,8 +40,10 @@ def load_models():
43 # Load text model 40 # Load text model
44 if open_text_model: 41 if open_text_model:
45 try: 42 try:
46 - logger.info(f"Loading text model: {CONFIG.TEXT_MODEL_DIR}")  
47 - _text_model = BgeTextModel(model_dir=CONFIG.TEXT_MODEL_DIR) 43 + from embeddings.qwen3_model import Qwen3TextModel
  44 +
  45 + logger.info(f"Loading text model: {CONFIG.TEXT_MODEL_ID}")
  46 + _text_model = Qwen3TextModel(model_id=CONFIG.TEXT_MODEL_ID)
48 logger.info("Text model loaded successfully") 47 logger.info("Text model loaded successfully")
49 except Exception as e: 48 except Exception as e:
50 logger.error(f"Failed to load text model: {e}", exc_info=True) 49 logger.error(f"Failed to load text model: {e}", exc_info=True)
@@ -58,6 +57,8 @@ def load_models(): @@ -58,6 +57,8 @@ def load_models():
58 if open_image_model: 57 if open_image_model:
59 try: 58 try:
60 if CONFIG.USE_CLIP_AS_SERVICE: 59 if CONFIG.USE_CLIP_AS_SERVICE:
  60 + from embeddings.clip_as_service_encoder import ClipAsServiceImageEncoder
  61 +
61 logger.info(f"Loading image encoder via clip-as-service: {CONFIG.CLIP_AS_SERVICE_SERVER}") 62 logger.info(f"Loading image encoder via clip-as-service: {CONFIG.CLIP_AS_SERVICE_SERVER}")
62 _image_model = ClipAsServiceImageEncoder( 63 _image_model = ClipAsServiceImageEncoder(
63 server=CONFIG.CLIP_AS_SERVICE_SERVER, 64 server=CONFIG.CLIP_AS_SERVICE_SERVER,
@@ -65,6 +66,8 @@ def load_models(): @@ -65,6 +66,8 @@ def load_models():
65 ) 66 )
66 logger.info("Image model (clip-as-service) loaded successfully") 67 logger.info("Image model (clip-as-service) loaded successfully")
67 else: 68 else:
  69 + from embeddings.clip_model import ClipImageModel
  70 +
68 logger.info(f"Loading local image model: {CONFIG.IMAGE_MODEL_NAME} (device: {CONFIG.IMAGE_DEVICE})") 71 logger.info(f"Loading local image model: {CONFIG.IMAGE_MODEL_NAME} (device: {CONFIG.IMAGE_DEVICE})")
69 _image_model = ClipImageModel( 72 _image_model = ClipImageModel(
70 model_name=CONFIG.IMAGE_MODEL_NAME, 73 model_name=CONFIG.IMAGE_MODEL_NAME,
@@ -126,7 +129,10 @@ def embed_text(texts: List[str]) -&gt; List[Optional[List[float]]]: @@ -126,7 +129,10 @@ def embed_text(texts: List[str]) -&gt; List[Optional[List[float]]]:
126 try: 129 try:
127 with _text_encode_lock: 130 with _text_encode_lock:
128 embs = _text_model.encode_batch( 131 embs = _text_model.encode_batch(
129 - batch_texts, batch_size=int(CONFIG.TEXT_BATCH_SIZE), device=CONFIG.TEXT_DEVICE 132 + batch_texts,
  133 + batch_size=int(CONFIG.TEXT_BATCH_SIZE),
  134 + device=CONFIG.TEXT_DEVICE,
  135 + normalize_embeddings=bool(CONFIG.TEXT_NORMALIZE_EMBEDDINGS),
130 ) 136 )
131 for j, (idx, _t) in enumerate(indexed_texts): 137 for j, (idx, _t) in enumerate(indexed_texts):
132 out[idx] = _as_list(embs[j]) 138 out[idx] = _as_list(embs[j])
@@ -170,5 +176,3 @@ def embed_image(images: List[str]) -&gt; List[Optional[List[float]]]: @@ -170,5 +176,3 @@ def embed_image(images: List[str]) -&gt; List[Optional[List[float]]]:
170 for idx in indices: 176 for idx in indices:
171 out[idx] = None 177 out[idx] = None
172 return out 178 return out
173 -  
174 -  
embeddings/text_encoder.py
1 -"""  
2 -Text embedding encoder using network service. 1 +"""Text embedding client for the local embedding HTTP service."""
3 2
4 -Generates embeddings via HTTP API service (default localhost:6005).  
5 -""" 3 +import logging
  4 +import os
  5 +import pickle
  6 +from datetime import timedelta
  7 +from typing import Any, List, Optional, Union
6 8
7 -import sys  
8 -import requests  
9 -import time  
10 -import threading  
11 import numpy as np 9 import numpy as np
12 -import pickle  
13 import redis 10 import redis
14 -import os  
15 -from datetime import timedelta  
16 -from typing import List, Union, Dict, Any, Optional  
17 -import logging 11 +import requests
18 12
19 logger = logging.getLogger(__name__) 13 logger = logging.getLogger(__name__)
20 14
@@ -27,44 +21,34 @@ except ImportError: @@ -27,44 +21,34 @@ except ImportError:
27 REDIS_CONFIG = {} 21 REDIS_CONFIG = {}
28 22
29 23
30 -class BgeEncoder: 24 +class TextEmbeddingEncoder:
31 """ 25 """
32 - Singleton text encoder using network service.  
33 -  
34 - Thread-safe singleton pattern ensures only one instance exists. 26 + Text embedding encoder using network service.
35 """ 27 """
36 - _instance = None  
37 - _lock = threading.Lock()  
38 28
39 - def __new__(cls, service_url: Optional[str] = None):  
40 - with cls._lock:  
41 - if cls._instance is None:  
42 - cls._instance = super(BgeEncoder, cls).__new__(cls)  
43 - resolved_url = service_url or os.getenv("EMBEDDING_SERVICE_URL") or get_embedding_base_url()  
44 - logger.info(f"Creating BgeEncoder instance with service URL: {resolved_url}")  
45 - cls._instance.service_url = resolved_url  
46 - cls._instance.endpoint = f"{resolved_url}/embed/text"  
47 -  
48 - # Initialize Redis cache  
49 - try:  
50 - cls._instance.redis_client = redis.Redis(  
51 - host=REDIS_CONFIG.get('host', 'localhost'),  
52 - port=REDIS_CONFIG.get('port', 6479),  
53 - password=REDIS_CONFIG.get('password'),  
54 - decode_responses=False, # Keep binary data as is  
55 - socket_timeout=REDIS_CONFIG.get('socket_timeout', 1),  
56 - socket_connect_timeout=REDIS_CONFIG.get('socket_connect_timeout', 1),  
57 - retry_on_timeout=REDIS_CONFIG.get('retry_on_timeout', False),  
58 - health_check_interval=10 # 避免复用坏连接  
59 - )  
60 - # Test connection  
61 - cls._instance.redis_client.ping()  
62 - cls._instance.expire_time = timedelta(days=REDIS_CONFIG.get('cache_expire_days', 180))  
63 - logger.info("Redis cache initialized for embeddings")  
64 - except Exception as e:  
65 - logger.warning(f"Failed to initialize Redis cache for embeddings: {e}, continuing without cache")  
66 - cls._instance.redis_client = None  
67 - return cls._instance 29 + def __init__(self, service_url: Optional[str] = None):
  30 + resolved_url = service_url or os.getenv("EMBEDDING_SERVICE_URL") or get_embedding_base_url()
  31 + self.service_url = str(resolved_url).rstrip("/")
  32 + self.endpoint = f"{self.service_url}/embed/text"
  33 + self.expire_time = timedelta(days=REDIS_CONFIG.get("cache_expire_days", 180))
  34 + logger.info("Creating TextEmbeddingEncoder instance with service URL: %s", self.service_url)
  35 +
  36 + try:
  37 + self.redis_client = redis.Redis(
  38 + host=REDIS_CONFIG.get("host", "localhost"),
  39 + port=REDIS_CONFIG.get("port", 6479),
  40 + password=REDIS_CONFIG.get("password"),
  41 + decode_responses=False,
  42 + socket_timeout=REDIS_CONFIG.get("socket_timeout", 1),
  43 + socket_connect_timeout=REDIS_CONFIG.get("socket_connect_timeout", 1),
  44 + retry_on_timeout=REDIS_CONFIG.get("retry_on_timeout", False),
  45 + health_check_interval=10,
  46 + )
  47 + self.redis_client.ping()
  48 + logger.info("Redis cache initialized for embeddings")
  49 + except Exception as e:
  50 + logger.warning("Failed to initialize Redis cache for embeddings: %s, continuing without cache", e)
  51 + self.redis_client = None
68 52
69 def _call_service(self, request_data: List[str]) -> List[Any]: 53 def _call_service(self, request_data: List[str]) -> List[Any]:
70 """ 54 """
@@ -85,7 +69,7 @@ class BgeEncoder: @@ -85,7 +69,7 @@ class BgeEncoder:
85 response.raise_for_status() 69 response.raise_for_status()
86 return response.json() 70 return response.json()
87 except requests.exceptions.RequestException as e: 71 except requests.exceptions.RequestException as e:
88 - logger.error(f"BgeEncoder service request failed: {e}", exc_info=True) 72 + logger.error(f"TextEmbeddingEncoder service request failed: {e}", exc_info=True)
89 raise 73 raise
90 74
91 def encode( 75 def encode(
@@ -122,7 +106,7 @@ class BgeEncoder: @@ -122,7 +106,7 @@ class BgeEncoder:
122 embeddings: List[Optional[np.ndarray]] = [None] * len(sentences) 106 embeddings: List[Optional[np.ndarray]] = [None] * len(sentences)
123 107
124 for i, text in enumerate(sentences): 108 for i, text in enumerate(sentences):
125 - cached = self._get_cached_embedding(text, 'en') # Use 'en' as default language for title embedding 109 + cached = self._get_cached_embedding(text, "generic")
126 if cached is not None: 110 if cached is not None:
127 embeddings[i] = cached 111 embeddings[i] = cached
128 else: 112 else:
@@ -152,7 +136,7 @@ class BgeEncoder: @@ -152,7 +136,7 @@ class BgeEncoder:
152 if self._is_valid_embedding(embedding_array): 136 if self._is_valid_embedding(embedding_array):
153 embeddings[original_idx] = embedding_array 137 embeddings[original_idx] = embedding_array
154 # Cache the embedding 138 # Cache the embedding
155 - self._set_cached_embedding(text, 'en', embedding_array) 139 + self._set_cached_embedding(text, "generic", embedding_array)
156 else: 140 else:
157 logger.warning( 141 logger.warning(
158 f"Invalid embedding returned from service for text {original_idx} " 142 f"Invalid embedding returned from service for text {original_idx} "
@@ -270,3 +254,7 @@ class BgeEncoder: @@ -270,3 +254,7 @@ class BgeEncoder:
270 except Exception as e: 254 except Exception as e:
271 logger.error(f"Error storing embedding in cache: {e}") 255 logger.error(f"Error storing embedding in cache: {e}")
272 return False 256 return False
  257 +
  258 +
  259 +# Backward compatibility for existing imports/usages.
  260 +BgeEncoder = TextEmbeddingEncoder
examples/cloud_embedding_example.py deleted
@@ -1,167 +0,0 @@ @@ -1,167 +0,0 @@
1 -"""  
2 -简单示例:使用云端文本向量化模块  
3 -  
4 -展示如何使用 CloudTextEncoder 进行文本向量化。  
5 -"""  
6 -  
7 -import os  
8 -import sys  
9 -from pathlib import Path  
10 -  
11 -# Add parent directory to path  
12 -sys.path.insert(0, str(Path(__file__).parent.parent))  
13 -  
14 -from embeddings.cloud_text_encoder import CloudTextEncoder  
15 -  
16 -  
17 -def example_single_text():  
18 - """示例1:单个文本向量化"""  
19 - print("=" * 60)  
20 - print("示例1:单个文本向量化")  
21 - print("=" * 60)  
22 -  
23 - # 初始化编码器  
24 - encoder = CloudTextEncoder()  
25 -  
26 - # 单个文本  
27 - text = "衣服的质量杠杠的"  
28 - print(f"输入文本: {text}")  
29 -  
30 - # 生成向量  
31 - embedding = encoder.encode(text)  
32 -  
33 - print(f"向量维度: {embedding.shape}")  
34 - print(f"向量前5个值: {embedding[0][:5]}")  
35 - print()  
36 -  
37 -  
38 -def example_multiple_texts():  
39 - """示例2:批量文本向量化"""  
40 - print("=" * 60)  
41 - print("示例2:批量文本向量化")  
42 - print("=" * 60)  
43 -  
44 - # 初始化编码器  
45 - encoder = CloudTextEncoder()  
46 -  
47 - # 多个文本  
48 - texts = [  
49 - "Bohemian Maxi Dress",  
50 - "Vintage Denim Jacket",  
51 - "Minimalist Linen Trousers",  
52 - "Gothic Black Boots",  
53 - "Streetwear Oversized Hoodie"  
54 - ]  
55 -  
56 - print(f"输入文本数量: {len(texts)}")  
57 - for i, text in enumerate(texts, 1):  
58 - print(f" {i}. {text}")  
59 -  
60 - # 生成向量  
61 - embeddings = encoder.encode(texts)  
62 -  
63 - print(f"\n向量矩阵维度: {embeddings.shape}")  
64 - print(f"第一个文本的向量前5个值: {embeddings[0][:5]}")  
65 - print()  
66 -  
67 -  
68 -def example_batch_processing():  
69 - """示例3:大批量处理"""  
70 - print("=" * 60)  
71 - print("示例3:大批量处理(自动分批)")  
72 - print("=" * 60)  
73 -  
74 - # 初始化编码器  
75 - encoder = CloudTextEncoder()  
76 -  
77 - # 生成大量文本  
78 - texts = [f"商品描述 {i}" for i in range(50)]  
79 -  
80 - print(f"输入文本数量: {len(texts)}")  
81 - print(f"批大小: 10")  
82 -  
83 - # 使用 encode_batch 自动分批处理  
84 - embeddings = encoder.encode_batch(texts, batch_size=10)  
85 -  
86 - print(f"向量矩阵维度: {embeddings.shape}")  
87 - print(f"平均向量范数: {embeddings.mean():.4f}")  
88 - print()  
89 -  
90 -  
91 -def example_similarity_calculation():  
92 - """示例4:计算文本相似度"""  
93 - print("=" * 60)  
94 - print("示例4:计算文本相似度")  
95 - print("=" * 60)  
96 -  
97 - import numpy as np  
98 -  
99 - # 初始化编码器  
100 - encoder = CloudTextEncoder()  
101 -  
102 - # 准备文本  
103 - query = "夏季连衣裙"  
104 - candidates = [  
105 - "Summer maxi dress",  
106 - "冬季羽绒服",  
107 - "夏天长裙",  
108 - "运动鞋",  
109 - "女士连衣裙"  
110 - ]  
111 -  
112 - print(f"查询文本: {query}")  
113 - print(f"候选文本:")  
114 - for i, text in enumerate(candidates, 1):  
115 - print(f" {i}. {text}")  
116 -  
117 - # 生成向量  
118 - query_embedding = encoder.encode(query)  
119 - candidate_embeddings = encoder.encode(candidates)  
120 -  
121 - # 计算余弦相似度  
122 - def cosine_similarity(a, b):  
123 - return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))  
124 -  
125 - print(f"\n相似度分数:")  
126 - similarities = []  
127 - for i, candidate_emb in enumerate(candidate_embeddings):  
128 - sim = cosine_similarity(query_embedding[0], candidate_emb)  
129 - similarities.append((sim, candidates[i]))  
130 - print(f" {candidates[i]}: {sim:.4f}")  
131 -  
132 - # 排序并显示最相似的  
133 - similarities.sort(reverse=True)  
134 - print(f"\n最相似的文本: {similarities[0][1]} (相似度: {similarities[0][0]:.4f})")  
135 - print()  
136 -  
137 -  
138 -def main():  
139 - """主函数"""  
140 - # 检查 API Key  
141 - if not os.getenv("DASHSCOPE_API_KEY"):  
142 - print("错误: 请设置 DASHSCOPE_API_KEY 环境变量")  
143 - print("示例: export DASHSCOPE_API_KEY='your-api-key'")  
144 - return  
145 -  
146 - print("\n云端文本向量化示例\n")  
147 -  
148 - try:  
149 - # 运行所有示例  
150 - example_single_text()  
151 - example_multiple_texts()  
152 - example_batch_processing()  
153 - example_similarity_calculation()  
154 -  
155 - print("=" * 60)  
156 - print("所有示例运行完成!")  
157 - print("=" * 60)  
158 -  
159 - except Exception as e:  
160 - print(f"\n错误: {e}")  
161 - import traceback  
162 - traceback.print_exc()  
163 -  
164 -  
165 -if __name__ == "__main__":  
166 - main()  
167 -  
@@ -274,7 +274,7 @@ if (StrUtil.isNotBlank(spu.getTitle()) &amp;&amp; CollUtil.isNotEmpty(titleEmbedding)) { @@ -274,7 +274,7 @@ if (StrUtil.isNotBlank(spu.getTitle()) &amp;&amp; CollUtil.isNotEmpty(titleEmbedding)) {
274 274
275 你当前 Python 侧已有: 275 你当前 Python 侧已有:
276 276
277 -- `embeddings/text_encoder.py`(BGE-M3 模型); 277 +- `embeddings/text_encoder.py`(通过 6005 向量服务调用 Qwen3-Embedding-0.6B);
278 - `SPUDocumentTransformer._fill_title_embedding` 已封装了调用 encoder 的逻辑。 278 - `SPUDocumentTransformer._fill_title_embedding` 已封装了调用 encoder 的逻辑。
279 279
280 **建议缓存策略(可选,但推荐):** 280 **建议缓存策略(可选,但推荐):**
indexer/document_transformer.py
@@ -723,7 +723,7 @@ class SPUDocumentTransformer: @@ -723,7 +723,7 @@ class SPUDocumentTransformer:
723 return 723 return
724 724
725 try: 725 try:
726 - # 使用BgeEncoder生成embedding 726 + # 使用文本向量编码器生成 embedding
727 # encode方法返回numpy数组,形状为(n, 1024) 727 # encode方法返回numpy数组,形状为(n, 1024)
728 embeddings = self.encoder.encode(title_text) 728 embeddings = self.encoder.encode(title_text)
729 729
@@ -740,4 +740,3 @@ class SPUDocumentTransformer: @@ -740,4 +740,3 @@ class SPUDocumentTransformer:
740 logger.warning(f"Failed to generate embedding for title: {title_text[:50]}...") 740 logger.warning(f"Failed to generate embedding for title: {title_text[:50]}...")
741 except Exception as e: 741 except Exception as e:
742 logger.error(f"Error generating title_embedding for SPU {doc.get('spu_id')}: {e}", exc_info=True) 742 logger.error(f"Error generating title_embedding for SPU {doc.get('spu_id')}: {e}", exc_info=True)
743 -  
indexer/incremental_service.py
@@ -80,11 +80,11 @@ class IncrementalIndexerService: @@ -80,11 +80,11 @@ class IncrementalIndexerService:
80 # Text embedding encoder (best-effort) 80 # Text embedding encoder (best-effort)
81 if bool(getattr(self._config.query_config, "enable_text_embedding", False)): 81 if bool(getattr(self._config.query_config, "enable_text_embedding", False)):
82 try: 82 try:
83 - from embeddings.text_encoder import BgeEncoder 83 + from embeddings.text_encoder import TextEmbeddingEncoder
84 84
85 - self._shared_text_encoder = BgeEncoder() 85 + self._shared_text_encoder = TextEmbeddingEncoder()
86 except Exception as e: 86 except Exception as e:
87 - logger.warning("Failed to initialize BgeEncoder at startup: %s", e) 87 + logger.warning("Failed to initialize TextEmbeddingEncoder at startup: %s", e)
88 self._shared_text_encoder = None 88 self._shared_text_encoder = None
89 89
90 # Image embedding encoder (best-effort; may be unavailable if embedding service not running) 90 # Image embedding encoder (best-effort; may be unavailable if embedding service not running)
@@ -126,13 +126,13 @@ class IncrementalIndexerService: @@ -126,13 +126,13 @@ class IncrementalIndexerService:
126 encoder: Optional[Any] = self._shared_text_encoder if enable_embedding else None 126 encoder: Optional[Any] = self._shared_text_encoder if enable_embedding else None
127 if enable_embedding and encoder is None: 127 if enable_embedding and encoder is None:
128 try: 128 try:
129 - from embeddings.text_encoder import BgeEncoder 129 + from embeddings.text_encoder import TextEmbeddingEncoder
130 130
131 - encoder = BgeEncoder() 131 + encoder = TextEmbeddingEncoder()
132 self._shared_text_encoder = encoder 132 self._shared_text_encoder = encoder
133 - logger.info("BgeEncoder lazily initialized in _get_transformer_bundle") 133 + logger.info("TextEmbeddingEncoder lazily initialized in _get_transformer_bundle")
134 except Exception as e: 134 except Exception as e:
135 - logger.warning("Failed to lazily initialize BgeEncoder for tenant_id=%s: %s", tenant_id, e) 135 + logger.warning("Failed to lazily initialize TextEmbeddingEncoder for tenant_id=%s: %s", tenant_id, e)
136 encoder = None 136 encoder = None
137 enable_embedding = False 137 enable_embedding = False
138 138
@@ -790,4 +790,3 @@ class IncrementalIndexerService: @@ -790,4 +790,3 @@ class IncrementalIndexerService:
790 "tenant_id": tenant_id 790 "tenant_id": tenant_id
791 } 791 }
792 792
793 -  
indexer/indexing_utils.py
@@ -112,11 +112,11 @@ def create_document_transformer( @@ -112,11 +112,11 @@ def create_document_transformer(
112 # 初始化encoder(如果启用标题向量化且未提供encoder) 112 # 初始化encoder(如果启用标题向量化且未提供encoder)
113 if encoder is None and enable_title_embedding and config.query_config.enable_text_embedding: 113 if encoder is None and enable_title_embedding and config.query_config.enable_text_embedding:
114 try: 114 try:
115 - from embeddings.text_encoder import BgeEncoder  
116 - encoder = BgeEncoder()  
117 - logger.info("BgeEncoder initialized for title embedding") 115 + from embeddings.text_encoder import TextEmbeddingEncoder
  116 + encoder = TextEmbeddingEncoder()
  117 + logger.info("TextEmbeddingEncoder initialized for title embedding")
118 except Exception as e: 118 except Exception as e:
119 - logger.warning(f"Failed to initialize BgeEncoder: {e}, title embedding will be disabled") 119 + logger.warning(f"Failed to initialize TextEmbeddingEncoder: {e}, title embedding will be disabled")
120 enable_title_embedding = False 120 enable_title_embedding = False
121 except Exception as e: 121 except Exception as e:
122 logger.warning(f"Failed to load config, using defaults: {e}") 122 logger.warning(f"Failed to load config, using defaults: {e}")
@@ -136,4 +136,3 @@ def create_document_transformer( @@ -136,4 +136,3 @@ def create_document_transformer(
136 image_encoder=image_encoder, 136 image_encoder=image_encoder,
137 enable_image_embedding=enable_image_embedding, 137 enable_image_embedding=enable_image_embedding,
138 ) 138 )
139 -  
providers/embedding.py
@@ -13,9 +13,11 @@ def create_embedding_provider() -&gt; &quot;EmbeddingProvider&quot;: @@ -13,9 +13,11 @@ def create_embedding_provider() -&gt; &quot;EmbeddingProvider&quot;:
13 """Create embedding provider from services config.""" 13 """Create embedding provider from services config."""
14 cfg = get_embedding_config() 14 cfg = get_embedding_config()
15 provider = (cfg.provider or "http").strip().lower() 15 provider = (cfg.provider or "http").strip().lower()
16 - if provider == "vllm": 16 + if provider != "http":
17 import logging 17 import logging
18 - logging.getLogger(__name__).warning("embedding provider 'vllm' is reserved, using HTTP.") 18 + logging.getLogger(__name__).warning(
  19 + "Unsupported embedding provider '%s', fallback to HTTP provider.", provider
  20 + )
19 return EmbeddingProvider() 21 return EmbeddingProvider()
20 22
21 23
@@ -30,9 +32,9 @@ class EmbeddingProvider: @@ -30,9 +32,9 @@ class EmbeddingProvider:
30 32
31 @property 33 @property
32 def text_encoder(self): 34 def text_encoder(self):
33 - """Lazy-created text encoder (BgeEncoder)."""  
34 - from embeddings.text_encoder import BgeEncoder  
35 - return BgeEncoder(service_url=self._base_url) 35 + """Lazy-created text encoder (TextEmbeddingEncoder)."""
  36 + from embeddings.text_encoder import TextEmbeddingEncoder
  37 + return TextEmbeddingEncoder(service_url=self._base_url)
36 38
37 @property 39 @property
38 def image_encoder(self): 40 def image_encoder(self):
query/query_parser.py
@@ -10,7 +10,7 @@ import logging @@ -10,7 +10,7 @@ import logging
10 import re 10 import re
11 from concurrent.futures import Future, ThreadPoolExecutor, as_completed 11 from concurrent.futures import Future, ThreadPoolExecutor, as_completed
12 12
13 -from embeddings import BgeEncoder 13 +from embeddings import TextEmbeddingEncoder
14 from config import SearchConfig 14 from config import SearchConfig
15 from .language_detector import LanguageDetector 15 from .language_detector import LanguageDetector
16 from providers import create_translation_provider 16 from providers import create_translation_provider
@@ -77,7 +77,7 @@ class QueryParser: @@ -77,7 +77,7 @@ class QueryParser:
77 def __init__( 77 def __init__(
78 self, 78 self,
79 config: SearchConfig, 79 config: SearchConfig,
80 - text_encoder: Optional[BgeEncoder] = None, 80 + text_encoder: Optional[TextEmbeddingEncoder] = None,
81 translator: Optional[Any] = None 81 translator: Optional[Any] = None
82 ): 82 ):
83 """ 83 """
@@ -115,11 +115,11 @@ class QueryParser: @@ -115,11 +115,11 @@ class QueryParser:
115 logger.info("HanLP not installed; using simple tokenizer") 115 logger.info("HanLP not installed; using simple tokenizer")
116 116
117 @property 117 @property
118 - def text_encoder(self) -> BgeEncoder: 118 + def text_encoder(self) -> TextEmbeddingEncoder:
119 """Lazy load text encoder.""" 119 """Lazy load text encoder."""
120 if self._text_encoder is None and self.config.query_config.enable_text_embedding: 120 if self._text_encoder is None and self.config.query_config.enable_text_embedding:
121 logger.info("Initializing text encoder (lazy load)...") 121 logger.info("Initializing text encoder (lazy load)...")
122 - self._text_encoder = BgeEncoder() 122 + self._text_encoder = TextEmbeddingEncoder()
123 return self._text_encoder 123 return self._text_encoder
124 124
125 @property 125 @property
@@ -196,9 +196,9 @@ class QueryParser: @@ -196,9 +196,9 @@ class QueryParser:
196 ParsedQuery object with all processing results 196 ParsedQuery object with all processing results
197 """ 197 """
198 # Initialize logger if context provided 198 # Initialize logger if context provided
199 - logger = context.logger if context else None  
200 - if logger:  
201 - logger.info( 199 + active_logger = context.logger if context else logger
  200 + if context and hasattr(context, "logger"):
  201 + context.logger.info(
202 f"Starting query parsing | Original query: '{query}' | Generate vector: {generate_vector}", 202 f"Starting query parsing | Original query: '{query}' | Generate vector: {generate_vector}",
203 extra={'reqid': context.reqid, 'uid': context.uid} 203 extra={'reqid': context.reqid, 'uid': context.uid}
204 ) 204 )
@@ -207,13 +207,13 @@ class QueryParser: @@ -207,13 +207,13 @@ class QueryParser:
207 if context and hasattr(context, 'logger'): 207 if context and hasattr(context, 'logger'):
208 context.logger.info(msg, extra={'reqid': context.reqid, 'uid': context.uid}) 208 context.logger.info(msg, extra={'reqid': context.reqid, 'uid': context.uid})
209 else: 209 else:
210 - logger.info(msg) 210 + active_logger.info(msg)
211 211
212 def log_debug(msg): 212 def log_debug(msg):
213 if context and hasattr(context, 'logger'): 213 if context and hasattr(context, 'logger'):
214 context.logger.debug(msg, extra={'reqid': context.reqid, 'uid': context.uid}) 214 context.logger.debug(msg, extra={'reqid': context.reqid, 'uid': context.uid})
215 else: 215 else:
216 - logger.debug(msg) 216 + active_logger.debug(msg)
217 217
218 # Stage 1: Normalize 218 # Stage 1: Normalize
219 normalized = self.normalizer.normalize(query) 219 normalized = self.normalizer.normalize(query)
tests/ci/test_service_api_contracts.py
@@ -427,7 +427,7 @@ def test_indexer_index_validation_max_delete_spu_ids(indexer_client: TestClient) @@ -427,7 +427,7 @@ def test_indexer_index_validation_max_delete_spu_ids(indexer_client: TestClient)
427 427
428 428
429 class _FakeTextModel: 429 class _FakeTextModel:
430 - def encode_batch(self, texts, batch_size=32, device="cpu"): 430 + def encode_batch(self, texts, batch_size=32, device="cpu", normalize_embeddings=True):
431 return [np.array([0.1, 0.2, 0.3], dtype=np.float32) for _ in texts] 431 return [np.array([0.1, 0.2, 0.3], dtype=np.float32) for _ in texts]
432 432
433 433
@@ -437,29 +437,24 @@ class _FakeImageModel: @@ -437,29 +437,24 @@ class _FakeImageModel:
437 437
438 438
439 @pytest.fixture 439 @pytest.fixture
440 -def embedding_client(): 440 +def embedding_module():
441 import embeddings.server as emb_server 441 import embeddings.server as emb_server
442 442
443 emb_server.app.router.on_startup.clear() 443 emb_server.app.router.on_startup.clear()
444 emb_server._text_model = _FakeTextModel() 444 emb_server._text_model = _FakeTextModel()
445 emb_server._image_model = _FakeImageModel() 445 emb_server._image_model = _FakeImageModel()
446 -  
447 - with TestClient(emb_server.app) as client:  
448 - yield client 446 + yield emb_server
449 447
450 448
451 -def test_embedding_text_contract(embedding_client: TestClient):  
452 - response = embedding_client.post("/embed/text", json=["hello", "world"])  
453 - assert response.status_code == 200  
454 - data = response.json() 449 +def test_embedding_text_contract(embedding_module):
  450 + data = embedding_module.embed_text(["hello", "world"])
455 assert len(data) == 2 451 assert len(data) == 2
456 assert len(data[0]) == 3 452 assert len(data[0]) == 3
457 453
458 454
459 -def test_embedding_image_contract(embedding_client: TestClient):  
460 - response = embedding_client.post("/embed/image", json=["https://example.com/a.jpg"])  
461 - assert response.status_code == 200  
462 - assert len(response.json()[0]) == 3 455 +def test_embedding_image_contract(embedding_module):
  456 + data = embedding_module.embed_image(["https://example.com/a.jpg"])
  457 + assert len(data[0]) == 3
463 458
464 459
465 class _FakeTranslator: 460 class _FakeTranslator:
tests/test_cloud_embedding.py deleted
@@ -1,186 +0,0 @@ @@ -1,186 +0,0 @@
1 -"""  
2 -Test script for cloud text embedding using Aliyun DashScope API.  
3 -  
4 -Reads queries from queries.txt and tests embedding generation,  
5 -logging send time, receive time, and duration for each request.  
6 -"""  
7 -  
8 -import os  
9 -import sys  
10 -import time  
11 -from datetime import datetime  
12 -from pathlib import Path  
13 -  
14 -import pytest  
15 -  
16 -# Add parent directory to path  
17 -sys.path.insert(0, str(Path(__file__).parent.parent))  
18 -  
19 -from embeddings.cloud_text_encoder import CloudTextEncoder  
20 -  
21 -  
22 -def format_timestamp(ts: float) -> str:  
23 - """Format timestamp to readable string."""  
24 - return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]  
25 -  
26 -  
27 -def read_queries(file_path: str, limit: int = 100) -> list:  
28 - """  
29 - Read queries from text file.  
30 -  
31 - Args:  
32 - file_path: Path to queries file  
33 - limit: Maximum number of queries to read  
34 -  
35 - Returns:  
36 - List of query strings  
37 - """  
38 - queries = []  
39 - with open(file_path, 'r', encoding='utf-8') as f:  
40 - for i, line in enumerate(f):  
41 - if i >= limit:  
42 - break  
43 - query = line.strip()  
44 - if query: # Skip empty lines  
45 - queries.append(query)  
46 - return queries  
47 -  
48 -  
49 -@pytest.mark.skip(reason="Requires data file and DASHSCOPE_API_KEY; run manually when needed")  
50 -def test_cloud_embedding(queries_file: str, num_queries: int = 100):  
51 - """  
52 - Test cloud embedding with queries from file.  
53 -  
54 - Args:  
55 - queries_file: Path to queries file  
56 - num_queries: Number of queries to test  
57 - """  
58 - print("=" * 80)  
59 - print("Cloud Text Embedding Test - Aliyun DashScope API")  
60 - print("=" * 80)  
61 - print()  
62 -  
63 - # Check if API key is set  
64 - api_key = os.getenv("DASHSCOPE_API_KEY")  
65 - if not api_key:  
66 - print("ERROR: DASHSCOPE_API_KEY environment variable is not set!")  
67 - print("Please set it using: export DASHSCOPE_API_KEY='your-api-key'")  
68 - return  
69 -  
70 - print(f"API Key: {api_key[:10]}...{api_key[-4:]}")  
71 - print()  
72 -  
73 - # Read queries  
74 - print(f"Reading queries from: {queries_file}")  
75 - try:  
76 - queries = read_queries(queries_file, limit=num_queries)  
77 - print(f"Successfully read {len(queries)} queries")  
78 - print()  
79 - except Exception as e:  
80 - print(f"ERROR: Failed to read queries file: {e}")  
81 - return  
82 -  
83 - # Initialize encoder  
84 - print("Initializing CloudTextEncoder...")  
85 - try:  
86 - encoder = CloudTextEncoder()  
87 - print("CloudTextEncoder initialized successfully")  
88 - print()  
89 - except Exception as e:  
90 - print(f"ERROR: Failed to initialize encoder: {e}")  
91 - return  
92 -  
93 - # Test embeddings  
94 - print("=" * 80)  
95 - print(f"Testing {len(queries)} queries (one by one)")  
96 - print("=" * 80)  
97 - print()  
98 -  
99 - total_start = time.time()  
100 - success_count = 0  
101 - failure_count = 0  
102 - total_duration = 0.0  
103 -  
104 - for i, query in enumerate(queries, 1):  
105 - try:  
106 - # Record send time  
107 - send_time = time.time()  
108 - send_time_str = format_timestamp(send_time)  
109 -  
110 - # Generate embedding  
111 - embedding = encoder.encode(query)  
112 -  
113 - # Record receive time  
114 - receive_time = time.time()  
115 - receive_time_str = format_timestamp(receive_time)  
116 -  
117 - # Calculate duration  
118 - duration = receive_time - send_time  
119 - total_duration += duration  
120 -  
121 - # Verify embedding  
122 - if embedding.shape[0] > 0:  
123 - success_count += 1  
124 - status = "✓ SUCCESS"  
125 - else:  
126 - failure_count += 1  
127 - status = "✗ FAILED"  
128 -  
129 - # Print result  
130 - query_display = query[:50] + "..." if len(query) > 50 else query  
131 - print(f"[{i:3d}/{len(queries)}] {status}")  
132 - print(f" Query: {query_display}")  
133 - print(f" Send Time: {send_time_str}")  
134 - print(f" Receive Time: {receive_time_str}")  
135 - print(f" Duration: {duration:.3f}s")  
136 - print(f" Embedding Shape: {embedding.shape}")  
137 - print()  
138 -  
139 - except Exception as e:  
140 - failure_count += 1  
141 - receive_time = time.time()  
142 - duration = receive_time - send_time  
143 -  
144 - print(f"[{i:3d}/{len(queries)}] ✗ ERROR")  
145 - print(f" Query: {query[:50]}...")  
146 - print(f" Send Time: {send_time_str}")  
147 - print(f" Receive Time: {format_timestamp(receive_time)}")  
148 - print(f" Duration: {duration:.3f}s")  
149 - print(f" Error: {str(e)}")  
150 - print()  
151 -  
152 - # Print summary  
153 - total_elapsed = time.time() - total_start  
154 - avg_duration = total_duration / len(queries) if queries else 0  
155 -  
156 - print("=" * 80)  
157 - print("Test Summary")  
158 - print("=" * 80)  
159 - print(f"Total Queries: {len(queries)}")  
160 - print(f"Successful: {success_count}")  
161 - print(f"Failed: {failure_count}")  
162 - print(f"Success Rate: {success_count / len(queries) * 100:.1f}%")  
163 - print(f"Total Time: {total_elapsed:.3f}s")  
164 - print(f"Total API Time: {total_duration:.3f}s")  
165 - print(f"Average Duration: {avg_duration:.3f}s per query")  
166 - print(f"Throughput: {len(queries) / total_elapsed:.2f} queries/second")  
167 - print("=" * 80)  
168 -  
169 -  
170 -def main():  
171 - """Main entry point."""  
172 - # Default queries file path  
173 - queries_file = Path(__file__).parent.parent / "data_crawling" / "queries.txt"  
174 -  
175 - # Check if file exists  
176 - if not queries_file.exists():  
177 - print(f"ERROR: Queries file not found: {queries_file}")  
178 - return  
179 -  
180 - # Run test with 100 queries  
181 - test_cloud_embedding(str(queries_file), num_queries=100)  
182 -  
183 -  
184 -if __name__ == "__main__":  
185 - main()  
186 -  
tests/test_embedding_pipeline.py 0 → 100644
@@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
  1 +import pickle
  2 +from typing import Any, Dict, List, Optional
  3 +
  4 +import numpy as np
  5 +
  6 +from config import (
  7 + FunctionScoreConfig,
  8 + IndexConfig,
  9 + QueryConfig,
  10 + RankingConfig,
  11 + RerankConfig,
  12 + SPUConfig,
  13 + SearchConfig,
  14 +)
  15 +from embeddings.text_encoder import TextEmbeddingEncoder
  16 +from query import QueryParser
  17 +
  18 +
  19 +class _FakeRedis:
  20 + def __init__(self):
  21 + self.store: Dict[str, bytes] = {}
  22 +
  23 + def ping(self):
  24 + return True
  25 +
  26 + def get(self, key: str):
  27 + return self.store.get(key)
  28 +
  29 + def setex(self, key: str, _expire, value: bytes):
  30 + self.store[key] = value
  31 + return True
  32 +
  33 + def expire(self, key: str, _expire):
  34 + return key in self.store
  35 +
  36 + def delete(self, key: str):
  37 + self.store.pop(key, None)
  38 + return True
  39 +
  40 +
  41 +class _FakeResponse:
  42 + def __init__(self, payload: List[Optional[List[float]]]):
  43 + self._payload = payload
  44 +
  45 + def raise_for_status(self):
  46 + return None
  47 +
  48 + def json(self):
  49 + return self._payload
  50 +
  51 +
  52 +class _FakeTranslator:
  53 + def translate(
  54 + self,
  55 + text: str,
  56 + target_lang: str,
  57 + source_lang: Optional[str] = None,
  58 + prompt: Optional[str] = None,
  59 + ) -> str:
  60 + return f"{text}-{target_lang}"
  61 +
  62 +
  63 +class _FakeQueryEncoder:
  64 + def encode(self, sentences, **kwargs):
  65 + if isinstance(sentences, str):
  66 + sentences = [sentences]
  67 + return np.array([np.array([0.11, 0.22, 0.33], dtype=np.float32) for _ in sentences], dtype=object)
  68 +
  69 +
  70 +def _build_test_config() -> SearchConfig:
  71 + return SearchConfig(
  72 + field_boosts={"title.en": 3.0},
  73 + indexes=[IndexConfig(name="default", label="default", fields=["title.en"], boost=1.0)],
  74 + query_config=QueryConfig(
  75 + supported_languages=["en", "zh"],
  76 + default_language="en",
  77 + enable_text_embedding=True,
  78 + enable_query_rewrite=False,
  79 + enable_multilang_search=True,
  80 + rewrite_dictionary={},
  81 + translation_prompts={"query_zh": "e-commerce domain", "query_en": "e-commerce domain"},
  82 + text_embedding_field="title_embedding",
  83 + image_embedding_field=None,
  84 + ),
  85 + ranking=RankingConfig(expression="bm25()", description="test"),
  86 + function_score=FunctionScoreConfig(),
  87 + rerank=RerankConfig(),
  88 + spu_config=SPUConfig(enabled=True, spu_field="spu_id", inner_hits_size=3),
  89 + es_index_name="test_products",
  90 + tenant_config={},
  91 + es_settings={},
  92 + services={},
  93 + )
  94 +
  95 +
  96 +def test_text_embedding_encoder_response_alignment(monkeypatch):
  97 + fake_redis = _FakeRedis()
  98 + monkeypatch.setattr("embeddings.text_encoder.redis.Redis", lambda **kwargs: fake_redis)
  99 +
  100 + def _fake_post(url, json, timeout):
  101 + assert url.endswith("/embed/text")
  102 + assert json == ["hello", "world"]
  103 + return _FakeResponse([[0.1, 0.2], None])
  104 +
  105 + monkeypatch.setattr("embeddings.text_encoder.requests.post", _fake_post)
  106 +
  107 + encoder = TextEmbeddingEncoder(service_url="http://127.0.0.1:6005")
  108 + out = encoder.encode(["hello", "world"])
  109 +
  110 + assert len(out) == 2
  111 + assert isinstance(out[0], np.ndarray)
  112 + assert out[0].shape == (2,)
  113 + assert out[1] is None
  114 +
  115 +
  116 +def test_text_embedding_encoder_cache_hit(monkeypatch):
  117 + fake_redis = _FakeRedis()
  118 + cached = np.array([0.9, 0.8], dtype=np.float32)
  119 + fake_redis.store["embedding:generic:cached-text"] = pickle.dumps(cached)
  120 + monkeypatch.setattr("embeddings.text_encoder.redis.Redis", lambda **kwargs: fake_redis)
  121 +
  122 + calls = {"count": 0}
  123 +
  124 + def _fake_post(url, json, timeout):
  125 + calls["count"] += 1
  126 + return _FakeResponse([[0.3, 0.4]])
  127 +
  128 + monkeypatch.setattr("embeddings.text_encoder.requests.post", _fake_post)
  129 +
  130 + encoder = TextEmbeddingEncoder(service_url="http://127.0.0.1:6005")
  131 + out = encoder.encode(["cached-text", "new-text"])
  132 +
  133 + assert calls["count"] == 1
  134 + assert np.allclose(out[0], cached)
  135 + assert np.allclose(out[1], np.array([0.3, 0.4], dtype=np.float32))
  136 +
  137 +
  138 +def test_query_parser_generates_query_vector_with_encoder():
  139 + parser = QueryParser(
  140 + config=_build_test_config(),
  141 + text_encoder=_FakeQueryEncoder(),
  142 + translator=_FakeTranslator(),
  143 + )
  144 +
  145 + parsed = parser.parse("red dress", tenant_id="162", generate_vector=True)
  146 + assert parsed.query_vector is not None
  147 + assert parsed.query_vector.shape == (3,)
  148 +
  149 +
  150 +def test_query_parser_skips_query_vector_when_disabled():
  151 + parser = QueryParser(
  152 + config=_build_test_config(),
  153 + text_encoder=_FakeQueryEncoder(),
  154 + translator=_FakeTranslator(),
  155 + )
  156 +
  157 + parsed = parser.parse("red dress", tenant_id="162", generate_vector=False)
  158 + assert parsed.query_vector is None
  159 +
third-party/xinference/activate.sh deleted
@@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
1 -#!/bin/bash  
2 -  
3 -CONDA_ROOT="${CONDA_ROOT:-/home/tw/miniconda3}"  
4 -source "$CONDA_ROOT/etc/profile.d/conda.sh"  
5 -conda activate xinference  
third-party/xinference/perfermance_test.py deleted
@@ -1,249 +0,0 @@ @@ -1,249 +0,0 @@
1 -import openai  
2 -import time  
3 -import statistics  
4 -from concurrent.futures import ThreadPoolExecutor, as_completed  
5 -from typing import List, Dict, Tuple  
6 -import json  
7 -  
8 -  
9 -class EmbeddingPerformanceTester:  
10 - def __init__(self, base_url: str = "http://127.0.0.1:9997/v1"):  
11 - """初始化性能测试器"""  
12 - self.client = openai.Client(  
13 - api_key="cannot be empty", # 实际使用时请替换为真实API Key  
14 - base_url=base_url  
15 - )  
16 -  
17 - def test_single_request(self, model: str, input_text: List[str]) -> Tuple[bool, float]:  
18 - """测试单个请求,返回成功状态和耗时"""  
19 - try:  
20 - start_time = time.perf_counter()  
21 - response = self.client.embeddings.create(  
22 - model=model,  
23 - input=input_text  
24 - )  
25 - end_time = time.perf_counter()  
26 -  
27 - # 验证响应格式  
28 - if response and hasattr(response, 'data'):  
29 - return True, end_time - start_time  
30 - else:  
31 - return False, end_time - start_time  
32 -  
33 - except Exception as e:  
34 - print(f"请求失败 - 模型 {model}: {str(e)}")  
35 - return False, 0.0  
36 -  
37 - def test_model_sequential(self, model: str, input_text: List[str],  
38 - iterations: int = 1000) -> Dict:  
39 - """顺序执行性能测试"""  
40 - print(f"\n开始顺序测试模型: {model}")  
41 - print(f"测试次数: {iterations}")  
42 -  
43 - successes = 0  
44 - failures = 0  
45 - latencies = []  
46 -  
47 - for i in range(iterations):  
48 - if i % 100 == 0 and i > 0:  
49 - print(f" 已完成 {i}/{iterations} 次请求...")  
50 -  
51 - success, latency = self.test_single_request(model, input_text)  
52 -  
53 - if success:  
54 - successes += 1  
55 - latencies.append(latency)  
56 - else:  
57 - failures += 1  
58 -  
59 - return self._calculate_stats(model, successes, failures, latencies)  
60 -  
61 - def test_model_concurrent(self, model: str, input_text: List[str],  
62 - iterations: int = 1000, max_workers: int = 10) -> Dict:  
63 - """并发执行性能测试"""  
64 - print(f"\n开始并发测试模型: {model}")  
65 - print(f"测试次数: {iterations}, 并发数: {max_workers}")  
66 -  
67 - successes = 0  
68 - failures = 0  
69 - latencies = []  
70 -  
71 - with ThreadPoolExecutor(max_workers=max_workers) as executor:  
72 - # 提交所有任务  
73 - future_to_request = {  
74 - executor.submit(self.test_single_request, model, input_text): i  
75 - for i in range(iterations)  
76 - }  
77 -  
78 - # 收集结果  
79 - completed = 0  
80 - for future in as_completed(future_to_request):  
81 - completed += 1  
82 - if completed % 100 == 0:  
83 - print(f" 已完成 {completed}/{iterations} 次请求...")  
84 -  
85 - try:  
86 - success, latency = future.result()  
87 - if success:  
88 - successes += 1  
89 - latencies.append(latency)  
90 - else:  
91 - failures += 1  
92 - except Exception as e:  
93 - print(f"请求异常: {str(e)}")  
94 - failures += 1  
95 -  
96 - return self._calculate_stats(model, successes, failures, latencies)  
97 -  
98 - def _calculate_stats(self, model: str, successes: int,  
99 - failures: int, latencies: List[float]) -> Dict:  
100 - """计算性能统计信息"""  
101 - if not latencies:  
102 - return {  
103 - "model": model,  
104 - "total_requests": successes + failures,  
105 - "successful_requests": successes,  
106 - "failed_requests": failures,  
107 - "success_rate": 0.0,  
108 - "error": "无成功请求"  
109 - }  
110 -  
111 - stats = {  
112 - "model": model,  
113 - "total_requests": successes + failures,  
114 - "successful_requests": successes,  
115 - "failed_requests": failures,  
116 - "success_rate": successes / (successes + failures) * 100,  
117 - "total_time": sum(latencies),  
118 - "avg_latency": statistics.mean(latencies),  
119 - "min_latency": min(latencies),  
120 - "max_latency": max(latencies),  
121 - "p50_latency": statistics.median(latencies),  
122 - "p95_latency": sorted(latencies)[int(len(latencies) * 0.95)],  
123 - "p99_latency": sorted(latencies)[int(len(latencies) * 0.99)],  
124 - "requests_per_second": len(latencies) / sum(latencies) if sum(latencies) > 0 else 0  
125 - }  
126 -  
127 - # 添加标准差(如果有多于一个样本)  
128 - if len(latencies) > 1:  
129 - stats["std_dev"] = statistics.stdev(latencies)  
130 -  
131 - return stats  
132 -  
133 - def print_results(self, results: Dict):  
134 - """打印测试结果"""  
135 - print("\n" + "="*60)  
136 - print(f"性能测试结果 - {results['model']}")  
137 - print("="*60)  
138 -  
139 - if "error" in results:  
140 - print(f"错误: {results['error']}")  
141 - return  
142 -  
143 - print(f"总请求数: {results['total_requests']}")  
144 - print(f"成功请求: {results['successful_requests']}")  
145 - print(f"失败请求: {results['failed_requests']}")  
146 - print(f"成功率: {results['success_rate']:.2f}%")  
147 - print(f"总耗时: {results['total_time']:.4f}秒")  
148 - print(f"平均延迟: {results['avg_latency']:.4f}秒")  
149 - print(f"最小延迟: {results['min_latency']:.4f}秒")  
150 - print(f"最大延迟: {results['max_latency']:.4f}秒")  
151 - print(f"P50延迟: {results['p50_latency']:.4f}秒")  
152 - print(f"P95延迟: {results['p95_latency']:.4f}秒")  
153 - print(f"P99延迟: {results['p99_latency']:.4f}秒")  
154 -  
155 - if "std_dev" in results:  
156 - print(f"标准差: {results['std_dev']:.4f}秒")  
157 -  
158 - print(f"QPS: {results['requests_per_second']:.2f} 请求/秒")  
159 - print("="*60)  
160 -  
161 - def save_results(self, results_list: List[Dict], filename: str = "performance_results.json"):  
162 - """保存测试结果到JSON文件"""  
163 - with open(filename, 'w', encoding='utf-8') as f:  
164 - json.dump(results_list, f, indent=2, ensure_ascii=False)  
165 - print(f"\n结果已保存到: {filename}")  
166 -  
167 -  
168 -def main():  
169 - """主函数"""  
170 - # 初始化测试器  
171 - tester = EmbeddingPerformanceTester()  
172 -  
173 - # 测试配置  
174 - test_input = ["What is the capital of China?"]  
175 - iterations = 1000  
176 - test_models = ['bge-m3', 'Qwen3-Embedding-0.6B']  
177 -  
178 - print("="*60)  
179 - print("Embedding API 性能测试")  
180 - print("="*60)  
181 -  
182 - all_results = []  
183 -  
184 - # 测试模式选择  
185 - print("\n选择测试模式:")  
186 - print("1. 顺序测试 (Sequential)")  
187 - print("2. 并发测试 (Concurrent)")  
188 - print("3. 两种模式都测试")  
189 -  
190 - mode = input("请输入选择 (1/2/3, 默认1): ").strip()  
191 -  
192 - for model in test_models:  
193 - print(f"\n{'='*60}")  
194 - print(f"测试模型: {model}")  
195 - print(f"{'='*60}")  
196 -  
197 - if mode in ['2', '3']:  
198 - # 并发测试  
199 - concurrent_results = tester.test_model_concurrent(  
200 - model=model,  
201 - input_text=test_input,  
202 - iterations=iterations,  
203 - max_workers=10 # 可根据需要调整并发数  
204 - )  
205 - tester.print_results(concurrent_results)  
206 - concurrent_results["test_mode"] = "concurrent"  
207 - all_results.append(concurrent_results)  
208 -  
209 - if mode in ['1', '3'] or not mode:  
210 - # 顺序测试  
211 - sequential_results = tester.test_model_sequential(  
212 - model=model,  
213 - input_text=test_input,  
214 - iterations=iterations  
215 - )  
216 - tester.print_results(sequential_results)  
217 - sequential_results["test_mode"] = "sequential"  
218 - all_results.append(sequential_results)  
219 -  
220 - # 保存结果  
221 - tester.save_results(all_results)  
222 -  
223 - # 汇总对比  
224 - print("\n" + "="*60)  
225 - print("性能测试汇总对比")  
226 - print("="*60)  
227 -  
228 - for result in all_results:  
229 - if "error" not in result:  
230 - print(f"\n模型: {result['model']} ({result['test_mode']})")  
231 - print(f" QPS: {result['requests_per_second']:.2f}")  
232 - print(f" 平均延迟: {result['avg_latency']:.4f}秒")  
233 - print(f" 成功率: {result['success_rate']:.2f}%")  
234 -  
235 -  
236 -if __name__ == "__main__":  
237 - # 添加一个简单的健康检查  
238 - try:  
239 - tester = EmbeddingPerformanceTester()  
240 - # 快速测试连接  
241 - test_result = tester.test_single_request('bge-m3', ["test"])  
242 - if test_result[0]:  
243 - print("API连接正常,开始性能测试...")  
244 - main()  
245 - else:  
246 - print("API连接失败,请检查服务是否正常运行")  
247 - except Exception as e:  
248 - print(f"初始化失败: {str(e)}")  
249 - print("请确保OpenAI客户端已安装: pip install openai")  
third-party/xinference/perfermance_test_http.py deleted
@@ -1,265 +0,0 @@ @@ -1,265 +0,0 @@
1 -import requests  
2 -import time  
3 -import statistics  
4 -from concurrent.futures import ThreadPoolExecutor, as_completed  
5 -from typing import List, Dict, Tuple  
6 -import json  
7 -  
8 -  
9 -class EmbeddingPerformanceTester:  
10 - def __init__(self, base_url: str = "http://127.0.0.1:9997/v1"):  
11 - """初始化性能测试器"""  
12 - self.base_url = base_url  
13 - self.embeddings_url = f"{base_url}/embeddings"  
14 -  
15 - def test_single_request(self, model: str, input_text: List[str]) -> Tuple[bool, float]:  
16 - """测试单个请求,返回成功状态和耗时"""  
17 - try:  
18 - start_time = time.perf_counter()  
19 -  
20 - # 构建请求体  
21 - # 如果 input_text 是列表,取第一个元素;如果是字符串,直接使用  
22 - input_value = input_text[0] if isinstance(input_text, list) and len(input_text) > 0 else input_text  
23 -  
24 - response = requests.post(  
25 - self.embeddings_url,  
26 - headers={  
27 - 'accept': 'application/json',  
28 - 'Content-Type': 'application/json'  
29 - },  
30 - json={  
31 - "model": model,  
32 - "input": input_value  
33 - },  
34 - timeout=60 # 设置超时时间  
35 - )  
36 -  
37 - end_time = time.perf_counter()  
38 -  
39 - # 验证响应格式  
40 - if response.status_code == 200:  
41 - result = response.json()  
42 - if result and 'data' in result and len(result['data']) > 0:  
43 - return True, end_time - start_time  
44 - else:  
45 - return False, end_time - start_time  
46 - else:  
47 - return False, end_time - start_time  
48 -  
49 - except Exception as e:  
50 - print(f"请求失败 - 模型 {model}: {str(e)}")  
51 - return False, 0.0  
52 -  
53 - def test_model_sequential(self, model: str, input_text: List[str],  
54 - iterations: int = 1000) -> Dict:  
55 - """顺序执行性能测试"""  
56 - print(f"\n开始顺序测试模型: {model}")  
57 - print(f"测试次数: {iterations}")  
58 -  
59 - successes = 0  
60 - failures = 0  
61 - latencies = []  
62 -  
63 - for i in range(iterations):  
64 - if i % 100 == 0 and i > 0:  
65 - print(f" 已完成 {i}/{iterations} 次请求...")  
66 -  
67 - success, latency = self.test_single_request(model, input_text)  
68 -  
69 - if success:  
70 - successes += 1  
71 - latencies.append(latency)  
72 - else:  
73 - failures += 1  
74 -  
75 - return self._calculate_stats(model, successes, failures, latencies)  
76 -  
77 - def test_model_concurrent(self, model: str, input_text: List[str],  
78 - iterations: int = 1000, max_workers: int = 10) -> Dict:  
79 - """并发执行性能测试"""  
80 - print(f"\n开始并发测试模型: {model}")  
81 - print(f"测试次数: {iterations}, 并发数: {max_workers}")  
82 -  
83 - successes = 0  
84 - failures = 0  
85 - latencies = []  
86 -  
87 - with ThreadPoolExecutor(max_workers=max_workers) as executor:  
88 - # 提交所有任务  
89 - future_to_request = {  
90 - executor.submit(self.test_single_request, model, input_text): i  
91 - for i in range(iterations)  
92 - }  
93 -  
94 - # 收集结果  
95 - completed = 0  
96 - for future in as_completed(future_to_request):  
97 - completed += 1  
98 - if completed % 100 == 0:  
99 - print(f" 已完成 {completed}/{iterations} 次请求...")  
100 -  
101 - try:  
102 - success, latency = future.result()  
103 - if success:  
104 - successes += 1  
105 - latencies.append(latency)  
106 - else:  
107 - failures += 1  
108 - except Exception as e:  
109 - print(f"请求异常: {str(e)}")  
110 - failures += 1  
111 -  
112 - return self._calculate_stats(model, successes, failures, latencies)  
113 -  
114 - def _calculate_stats(self, model: str, successes: int,  
115 - failures: int, latencies: List[float]) -> Dict:  
116 - """计算性能统计信息"""  
117 - if not latencies:  
118 - return {  
119 - "model": model,  
120 - "total_requests": successes + failures,  
121 - "successful_requests": successes,  
122 - "failed_requests": failures,  
123 - "success_rate": 0.0,  
124 - "error": "无成功请求"  
125 - }  
126 -  
127 - stats = {  
128 - "model": model,  
129 - "total_requests": successes + failures,  
130 - "successful_requests": successes,  
131 - "failed_requests": failures,  
132 - "success_rate": successes / (successes + failures) * 100,  
133 - "total_time": sum(latencies),  
134 - "avg_latency": statistics.mean(latencies),  
135 - "min_latency": min(latencies),  
136 - "max_latency": max(latencies),  
137 - "p50_latency": statistics.median(latencies),  
138 - "p95_latency": sorted(latencies)[int(len(latencies) * 0.95)],  
139 - "p99_latency": sorted(latencies)[int(len(latencies) * 0.99)],  
140 - "requests_per_second": len(latencies) / sum(latencies) if sum(latencies) > 0 else 0  
141 - }  
142 -  
143 - # 添加标准差(如果有多于一个样本)  
144 - if len(latencies) > 1:  
145 - stats["std_dev"] = statistics.stdev(latencies)  
146 -  
147 - return stats  
148 -  
149 - def print_results(self, results: Dict):  
150 - """打印测试结果"""  
151 - print("\n" + "="*60)  
152 - print(f"性能测试结果 - {results['model']}")  
153 - print("="*60)  
154 -  
155 - if "error" in results:  
156 - print(f"错误: {results['error']}")  
157 - return  
158 -  
159 - print(f"总请求数: {results['total_requests']}")  
160 - print(f"成功请求: {results['successful_requests']}")  
161 - print(f"失败请求: {results['failed_requests']}")  
162 - print(f"成功率: {results['success_rate']:.2f}%")  
163 - print(f"总耗时: {results['total_time']:.4f}秒")  
164 - print(f"平均延迟: {results['avg_latency']:.4f}秒")  
165 - print(f"最小延迟: {results['min_latency']:.4f}秒")  
166 - print(f"最大延迟: {results['max_latency']:.4f}秒")  
167 - print(f"P50延迟: {results['p50_latency']:.4f}秒")  
168 - print(f"P95延迟: {results['p95_latency']:.4f}秒")  
169 - print(f"P99延迟: {results['p99_latency']:.4f}秒")  
170 -  
171 - if "std_dev" in results:  
172 - print(f"标准差: {results['std_dev']:.4f}秒")  
173 -  
174 - print(f"QPS: {results['requests_per_second']:.2f} 请求/秒")  
175 - print("="*60)  
176 -  
177 - def save_results(self, results_list: List[Dict], filename: str = "performance_results.json"):  
178 - """保存测试结果到JSON文件"""  
179 - with open(filename, 'w', encoding='utf-8') as f:  
180 - json.dump(results_list, f, indent=2, ensure_ascii=False)  
181 - print(f"\n结果已保存到: {filename}")  
182 -  
183 -  
184 -def main():  
185 - """主函数"""  
186 - # 初始化测试器  
187 - tester = EmbeddingPerformanceTester()  
188 -  
189 - # 测试配置  
190 - test_input = ["What is the capital of China?"]  
191 - iterations = 1000  
192 - test_models = ['bge-m3', 'Qwen3-Embedding-0.6B']  
193 -  
194 - print("="*60)  
195 - print("Embedding API 性能测试 (HTTP)")  
196 - print("="*60)  
197 -  
198 - all_results = []  
199 -  
200 - # 测试模式选择  
201 - print("\n选择测试模式:")  
202 - print("1. 顺序测试 (Sequential)")  
203 - print("2. 并发测试 (Concurrent)")  
204 - print("3. 两种模式都测试")  
205 -  
206 - mode = input("请输入选择 (1/2/3, 默认1): ").strip()  
207 -  
208 - for model in test_models:  
209 - print(f"\n{'='*60}")  
210 - print(f"测试模型: {model}")  
211 - print(f"{'='*60}")  
212 -  
213 - if mode in ['2', '3']:  
214 - # 并发测试  
215 - concurrent_results = tester.test_model_concurrent(  
216 - model=model,  
217 - input_text=test_input,  
218 - iterations=iterations,  
219 - max_workers=10 # 可根据需要调整并发数  
220 - )  
221 - tester.print_results(concurrent_results)  
222 - concurrent_results["test_mode"] = "concurrent"  
223 - all_results.append(concurrent_results)  
224 -  
225 - if mode in ['1', '3'] or not mode:  
226 - # 顺序测试  
227 - sequential_results = tester.test_model_sequential(  
228 - model=model,  
229 - input_text=test_input,  
230 - iterations=iterations  
231 - )  
232 - tester.print_results(sequential_results)  
233 - sequential_results["test_mode"] = "sequential"  
234 - all_results.append(sequential_results)  
235 -  
236 - # 保存结果  
237 - tester.save_results(all_results)  
238 -  
239 - # 汇总对比  
240 - print("\n" + "="*60)  
241 - print("性能测试汇总对比")  
242 - print("="*60)  
243 -  
244 - for result in all_results:  
245 - if "error" not in result:  
246 - print(f"\n模型: {result['model']} ({result['test_mode']})")  
247 - print(f" QPS: {result['requests_per_second']:.2f}")  
248 - print(f" 平均延迟: {result['avg_latency']:.4f}秒")  
249 - print(f" 成功率: {result['success_rate']:.2f}%")  
250 -  
251 -  
252 -if __name__ == "__main__":  
253 - # 添加一个简单的健康检查  
254 - try:  
255 - tester = EmbeddingPerformanceTester()  
256 - # 快速测试连接  
257 - test_result = tester.test_single_request('bge-m3', ["test"])  
258 - if test_result[0]:  
259 - print("API连接正常,开始性能测试...")  
260 - main()  
261 - else:  
262 - print("API连接失败,请检查服务是否正常运行")  
263 - except Exception as e:  
264 - print(f"初始化失败: {str(e)}")  
265 - print("请确保 requests 库已安装: pip install requests")  
266 \ No newline at end of file 0 \ No newline at end of file
third-party/xinference/perfermance_test_single.py deleted
@@ -1,108 +0,0 @@ @@ -1,108 +0,0 @@
1 -import openai  
2 -import time  
3 -import requests  
4 -import json  
5 -  
6 -  
7 -client = openai.Client(  
8 - api_key="cannot be empty",  
9 - base_url="http://127.0.0.1:9997/v1"  
10 -)  
11 -  
12 -# 记录开始时间  
13 -start_time = time.time()  
14 -  
15 -a = client.embeddings.create(  
16 - model='bge-m3',  
17 - input=["What is the capital of China?"]  
18 -)  
19 -  
20 -# 记录结束时间  
21 -end_time = time.time()  
22 -  
23 -#print(a)  
24 -print(f"\n耗时: {end_time - start_time:.4f} 秒")  
25 -  
26 -# 记录开始时间  
27 -start_time = time.time()  
28 -  
29 -a = client.embeddings.create(  
30 - model='Qwen3-Embedding-0.6B',  
31 - input=["What is the capital of China?"]  
32 -)  
33 -  
34 -# 记录结束时间  
35 -end_time = time.time()  
36 -  
37 -#print(a)  
38 -print(f"\n耗时: {end_time - start_time:.4f} 秒")  
39 -  
40 -# ========== HTTP API 测试 ==========  
41 -print("\n" + "="*50)  
42 -print("HTTP API 测试")  
43 -print("="*50)  
44 -  
45 -# 配置  
46 -XINFERENCE_HOST = "127.0.0.1"  
47 -XINFERENCE_PORT = "9997"  
48 -base_url = f"http://{XINFERENCE_HOST}:{XINFERENCE_PORT}/v1/embeddings"  
49 -  
50 -# 测试 bge-m3 模型  
51 -print("\n测试模型: bge-m3")  
52 -start_time = time.time()  
53 -  
54 -response = requests.post(  
55 - base_url,  
56 - headers={  
57 - 'accept': 'application/json',  
58 - 'Content-Type': 'application/json'  
59 - },  
60 - json={  
61 - "model": "bge-m3",  
62 - "input": "What is the capital of China?"  
63 - }  
64 -)  
65 -  
66 -end_time = time.time()  
67 -  
68 -if response.status_code == 200:  
69 - result = response.json()  
70 - print(f"状态码: {response.status_code}")  
71 - print(f"模型: {result.get('model', 'N/A')}")  
72 - print(f"使用token数: {result.get('usage', {}).get('total_tokens', 'N/A')}")  
73 - print(f"嵌入向量维度: {len(result.get('data', [{}])[0].get('embedding', []))}")  
74 - print(f"耗时: {end_time - start_time:.4f} 秒")  
75 -else:  
76 - print(f"请求失败,状态码: {response.status_code}")  
77 - print(f"错误信息: {response.text}")  
78 -  
79 -# 测试 Qwen3-Embedding-0.6B 模型  
80 -print("\n测试模型: Qwen3-Embedding-0.6B")  
81 -start_time = time.time()  
82 -  
83 -response = requests.post(  
84 - base_url,  
85 - headers={  
86 - 'accept': 'application/json',  
87 - 'Content-Type': 'application/json'  
88 - },  
89 - json={  
90 - "model": "Qwen3-Embedding-0.6B",  
91 - "input": "What is the capital of China?"  
92 - }  
93 -)  
94 -  
95 -end_time = time.time()  
96 -  
97 -if response.status_code == 200:  
98 - result = response.json()  
99 - print(f"状态码: {response.status_code}")  
100 - print(f"模型: {result.get('model', 'N/A')}")  
101 - print(f"使用token数: {result.get('usage', {}).get('total_tokens', 'N/A')}")  
102 - print(f"嵌入向量维度: {len(result.get('data', [{}])[0].get('embedding', []))}")  
103 - print(f"耗时: {end_time - start_time:.4f} 秒")  
104 -else:  
105 - print(f"请求失败,状态码: {response.status_code}")  
106 - print(f"错误信息: {response.text}")  
107 -  
108 -  
third-party/xinference/stop.sh deleted
@@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
1 -  
2 -xinference terminate --model-uid Qwen3-Reranker-0.6B --model-uid bge-m3 --model-uid Qwen3-Embedding-0.6B  
third-party/xinference/test.sh deleted
@@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
1 -if [ "$CONDA_DEFAULT_ENV" != "tw" ]; then  
2 - echo "当前环境不是 tw,正在激活 tw 环境..."  
3 - CONDA_ROOT="${CONDA_ROOT:-/home/tw/miniconda3}"  
4 - source "$CONDA_ROOT/etc/profile.d/conda.sh"  
5 - conda activate tw  
6 - echo "已激活 tw 环境"  
7 -else  
8 - echo "当前已经在 tw 环境中,无需重复激活"  
9 -fi  
10 -  
11 -python perfermance_test_single.py  
third-party/xinference/测试结果-perfermance_test.txt deleted
@@ -1,166 +0,0 @@ @@ -1,166 +0,0 @@
1 -============================================================  
2 -Embedding API 性能测试  
3 -============================================================  
4 -  
5 -选择测试模式:  
6 -1. 顺序测试 (Sequential)  
7 -2. 并发测试 (Concurrent)  
8 -3. 两种模式都测试  
9 -请输入选择 (1/2/3, 默认1): 3  
10 -  
11 -============================================================  
12 -测试模型: bge-m3  
13 -============================================================  
14 -  
15 -开始并发测试模型: bge-m3  
16 -测试次数: 1000, 并发数: 10  
17 - 已完成 100/1000 次请求...  
18 - 已完成 200/1000 次请求...  
19 - 已完成 300/1000 次请求...  
20 - 已完成 400/1000 次请求...  
21 - 已完成 500/1000 次请求...  
22 - 已完成 600/1000 次请求...  
23 - 已完成 700/1000 次请求...  
24 - 已完成 800/1000 次请求...  
25 - 已完成 900/1000 次请求...  
26 - 已完成 1000/1000 次请求...  
27 -  
28 -============================================================  
29 -性能测试结果 - bge-m3  
30 -============================================================  
31 -总请求数: 1000  
32 -成功请求: 1000  
33 -失败请求: 0  
34 -成功率: 100.00%  
35 -总耗时: 212.9068秒  
36 -平均延迟: 0.2129秒  
37 -最小延迟: 0.0507秒  
38 -最大延迟: 0.6196秒  
39 -P50延迟: 0.0942秒  
40 -P95延迟: 0.5580秒  
41 -P99延迟: 0.5884秒  
42 -标准差: 0.2010秒  
43 -QPS: 4.70 请求/秒  
44 -============================================================  
45 -  
46 -开始顺序测试模型: bge-m3  
47 -测试次数: 1000  
48 - 已完成 100/1000 次请求...  
49 - 已完成 200/1000 次请求...  
50 - 已完成 300/1000 次请求...  
51 - 已完成 400/1000 次请求...  
52 - 已完成 500/1000 次请求...  
53 - 已完成 600/1000 次请求...  
54 - 已完成 700/1000 次请求...  
55 - 已完成 800/1000 次请求...  
56 - 已完成 900/1000 次请求...  
57 -  
58 -============================================================  
59 -性能测试结果 - bge-m3  
60 -============================================================  
61 -总请求数: 1000  
62 -成功请求: 1000  
63 -失败请求: 0  
64 -成功率: 100.00%  
65 -总耗时: 81.7646秒  
66 -平均延迟: 0.0818秒  
67 -最小延迟: 0.0328秒  
68 -最大延迟: 0.5812秒  
69 -P50延迟: 0.0347秒  
70 -P95延迟: 0.4893秒  
71 -P99延迟: 0.5047秒  
72 -标准差: 0.1377秒  
73 -QPS: 12.23 请求/秒  
74 -============================================================  
75 -  
76 -============================================================  
77 -测试模型: Qwen3-Embedding-0.6B  
78 -============================================================  
79 -  
80 -开始并发测试模型: Qwen3-Embedding-0.6B  
81 -测试次数: 1000, 并发数: 10  
82 - 已完成 100/1000 次请求...  
83 - 已完成 200/1000 次请求...  
84 - 已完成 300/1000 次请求...  
85 - 已完成 400/1000 次请求...  
86 - 已完成 500/1000 次请求...  
87 - 已完成 600/1000 次请求...  
88 - 已完成 700/1000 次请求...  
89 - 已完成 800/1000 次请求...  
90 - 已完成 900/1000 次请求...  
91 - 已完成 1000/1000 次请求...  
92 -  
93 -============================================================  
94 -性能测试结果 - Qwen3-Embedding-0.6B  
95 -============================================================  
96 -总请求数: 1000  
97 -成功请求: 1000  
98 -失败请求: 0  
99 -成功率: 100.00%  
100 -总耗时: 210.1917秒  
101 -平均延迟: 0.2102秒  
102 -最小延迟: 0.0651秒  
103 -最大延迟: 0.6659秒  
104 -P50延迟: 0.1123秒  
105 -P95延迟: 0.5845秒  
106 -P99延迟: 0.6210秒  
107 -标准差: 0.1877秒  
108 -QPS: 4.76 请求/秒  
109 -============================================================  
110 -  
111 -开始顺序测试模型: Qwen3-Embedding-0.6B  
112 -测试次数: 1000  
113 - 已完成 100/1000 次请求...  
114 -  
115 - 已完成 200/1000 次请求...  
116 - 已完成 300/1000 次请求...  
117 - 已完成 400/1000 次请求...  
118 - 已完成 500/1000 次请求...  
119 - 已完成 600/1000 次请求...  
120 - 已完成 700/1000 次请求...  
121 - 已完成 800/1000 次请求...  
122 - 已完成 900/1000 次请求...  
123 -  
124 -============================================================  
125 -性能测试结果 - Qwen3-Embedding-0.6B  
126 -============================================================  
127 -总请求数: 1000  
128 -成功请求: 1000  
129 -失败请求: 0  
130 -成功率: 100.00%  
131 -总耗时: 109.9795秒  
132 -平均延迟: 0.1100秒  
133 -最小延迟: 0.0571秒  
134 -最大延迟: 0.5806秒  
135 -P50延迟: 0.0600秒  
136 -P95延迟: 0.5648秒  
137 -P99延迟: 0.5745秒  
138 -标准差: 0.1494秒  
139 -QPS: 9.09 请求/秒  
140 -============================================================  
141 -  
142 -结果已保存到: performance_results.json  
143 -  
144 -============================================================  
145 -性能测试汇总对比  
146 -============================================================  
147 -  
148 -模型: bge-m3 (concurrent)  
149 - QPS: 4.70  
150 - 平均延迟: 0.2129秒  
151 - 成功率: 100.00%  
152 -  
153 -模型: bge-m3 (sequential)  
154 - QPS: 12.23  
155 - 平均延迟: 0.0818秒  
156 - 成功率: 100.00%  
157 -  
158 -模型: Qwen3-Embedding-0.6B (concurrent)  
159 - QPS: 4.76  
160 - 平均延迟: 0.2102秒  
161 - 成功率: 100.00%  
162 -  
163 -模型: Qwen3-Embedding-0.6B (sequential)  
164 - QPS: 9.09  
165 - 平均延迟: 0.1100秒  
166 - 成功率: 100.00%  
third-party/xinference/测试结果-perfermance_test_http.txt deleted
@@ -1,167 +0,0 @@ @@ -1,167 +0,0 @@
1 -$ p perfermance_test_http.py  
2 -API连接正常,开始性能测试...  
3 -============================================================  
4 -Embedding API 性能测试 (HTTP)  
5 -============================================================  
6 -  
7 -选择测试模式:  
8 -1. 顺序测试 (Sequential)  
9 -2. 并发测试 (Concurrent)  
10 -3. 两种模式都测试  
11 -请输入选择 (1/2/3, 默认1): 3  
12 -  
13 -============================================================  
14 -测试模型: bge-m3  
15 -============================================================  
16 -  
17 -开始并发测试模型: bge-m3  
18 -测试次数: 1000, 并发数: 10  
19 - 已完成 100/1000 次请求...  
20 - 已完成 200/1000 次请求...  
21 - 已完成 300/1000 次请求...  
22 - 已完成 400/1000 次请求...  
23 - 已完成 500/1000 次请求...  
24 - 已完成 600/1000 次请求...  
25 - 已完成 700/1000 次请求...  
26 - 已完成 800/1000 次请求...  
27 - 已完成 900/1000 次请求...  
28 - 已完成 1000/1000 次请求...  
29 -  
30 -============================================================  
31 -性能测试结果 - bge-m3  
32 -============================================================  
33 -总请求数: 1000  
34 -成功请求: 1000  
35 -失败请求: 0  
36 -成功率: 100.00%  
37 -总耗时: 145.1439秒  
38 -平均延迟: 0.1451秒  
39 -最小延迟: 0.0311秒  
40 -最大延迟: 0.5770秒  
41 -P50延迟: 0.0599秒  
42 -P95延迟: 0.5151秒  
43 -P99延迟: 0.5704秒  
44 -标准差: 0.1789秒  
45 -QPS: 6.89 请求/秒  
46 -============================================================  
47 -  
48 -开始顺序测试模型: bge-m3  
49 -测试次数: 1000  
50 - 已完成 100/1000 次请求...  
51 - 已完成 200/1000 次请求...  
52 - 已完成 300/1000 次请求...  
53 - 已完成 400/1000 次请求...  
54 - 已完成 500/1000 次请求...  
55 - 已完成 600/1000 次请求...  
56 - 已完成 700/1000 次请求...  
57 - 已完成 800/1000 次请求...  
58 - 已完成 900/1000 次请求...  
59 -  
60 -============================================================  
61 -性能测试结果 - bge-m3  
62 -============================================================  
63 -总请求数: 1000  
64 -成功请求: 1000  
65 -失败请求: 0  
66 -成功率: 100.00%  
67 -总耗时: 74.5284秒  
68 -平均延迟: 0.0745秒  
69 -最小延迟: 0.0271秒  
70 -最大延迟: 0.5767秒  
71 -P50延迟: 0.0286秒  
72 -P95延迟: 0.4797秒  
73 -P99延迟: 0.5037秒  
74 -标准差: 0.1364秒  
75 -QPS: 13.42 请求/秒  
76 -============================================================  
77 -  
78 -============================================================  
79 -测试模型: Qwen3-Embedding-0.6B  
80 -============================================================  
81 -  
82 -开始并发测试模型: Qwen3-Embedding-0.6B  
83 -测试次数: 1000, 并发数: 10  
84 - 已完成 100/1000 次请求...  
85 - 已完成 200/1000 次请求...  
86 - 已完成 300/1000 次请求...  
87 - 已完成 400/1000 次请求...  
88 - 已完成 500/1000 次请求...  
89 - 已完成 600/1000 次请求...  
90 - 已完成 700/1000 次请求...  
91 - 已完成 800/1000 次请求...  
92 - 已完成 900/1000 次请求...  
93 - 已完成 1000/1000 次请求...  
94 -  
95 -============================================================  
96 -性能测试结果 - Qwen3-Embedding-0.6B  
97 -============================================================  
98 -总请求数: 1000  
99 -成功请求: 1000  
100 -失败请求: 0  
101 -成功率: 100.00%  
102 -总耗时: 195.7997秒  
103 -平均延迟: 0.1958秒  
104 -最小延迟: 0.0564秒  
105 -最大延迟: 0.6201秒  
106 -P50延迟: 0.1050秒  
107 -P95延迟: 0.5674秒  
108 -P99延迟: 0.5994秒  
109 -标准差: 0.1829秒  
110 -QPS: 5.11 请求/秒  
111 -============================================================  
112 -  
113 -开始顺序测试模型: Qwen3-Embedding-0.6B  
114 -测试次数: 1000  
115 - 已完成 100/1000 次请求...  
116 - 已完成 200/1000 次请求...  
117 - 已完成 300/1000 次请求...  
118 - 已完成 400/1000 次请求...  
119 - 已完成 500/1000 次请求...  
120 - 已完成 600/1000 次请求...  
121 - 已完成 700/1000 次请求...  
122 - 已完成 800/1000 次请求...  
123 - 已完成 900/1000 次请求...  
124 -  
125 -============================================================  
126 -性能测试结果 - Qwen3-Embedding-0.6B  
127 -============================================================  
128 -总请求数: 1000  
129 -成功请求: 1000  
130 -失败请求: 0  
131 -成功率: 100.00%  
132 -总耗时: 100.2533秒  
133 -平均延迟: 0.1003秒  
134 -最小延迟: 0.0513秒  
135 -最大延迟: 0.6249秒  
136 -P50延迟: 0.0539秒  
137 -P95延迟: 0.4993秒  
138 -P99延迟: 0.5180秒  
139 -标准差: 0.1354秒  
140 -QPS: 9.97 请求/秒  
141 -============================================================  
142 -  
143 -结果已保存到: performance_results.json  
144 -  
145 -============================================================  
146 -性能测试汇总对比  
147 -============================================================  
148 -  
149 -模型: bge-m3 (concurrent)  
150 - QPS: 6.89  
151 - 平均延迟: 0.1451秒  
152 - 成功率: 100.00%  
153 -  
154 -模型: bge-m3 (sequential)  
155 - QPS: 13.42  
156 - 平均延迟: 0.0745秒  
157 - 成功率: 100.00%  
158 -  
159 -模型: Qwen3-Embedding-0.6B (concurrent)  
160 - QPS: 5.11  
161 - 平均延迟: 0.1958秒  
162 - 成功率: 100.00%  
163 -  
164 -模型: Qwen3-Embedding-0.6B (sequential)  
165 - QPS: 9.97  
166 - 平均延迟: 0.1003秒  
167 - 成功率: 100.00%