Commit b73baf85cb520d7316d2514d24edfec5e8334b7a
1 parent
cd3799c6
撰写接口文档
Showing
6 changed files
with
1018 additions
and
110 deletions
Show diff stats
| ... | ... | @@ -0,0 +1,791 @@ |
| 1 | +# 搜索API接口对接指南 | |
| 2 | + | |
| 3 | +本文档为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。 | |
| 4 | + | |
| 5 | +## 目录 | |
| 6 | + | |
| 7 | +1. [快速开始](#快速开始) | |
| 8 | +2. [接口概览](#接口概览) | |
| 9 | +3. [文本搜索接口](#文本搜索接口) | |
| 10 | +4. [图片搜索接口](#图片搜索接口) | |
| 11 | +5. [响应格式说明](#响应格式说明) | |
| 12 | +6. [常见场景示例](#常见场景示例) | |
| 13 | +7. [错误处理](#错误处理) | |
| 14 | +8. [最佳实践](#最佳实践) | |
| 15 | + | |
| 16 | +--- | |
| 17 | + | |
| 18 | +## 快速开始 | |
| 19 | + | |
| 20 | +### 基础信息 | |
| 21 | + | |
| 22 | +- **Base URL**: `http://your-domain:6002` 或 `http://120.76.41.98:6002` | |
| 23 | +- **协议**: HTTP/HTTPS | |
| 24 | +- **数据格式**: JSON | |
| 25 | +- **字符编码**: UTF-8 | |
| 26 | +- **请求方法**: POST(搜索接口) | |
| 27 | + | |
| 28 | +### 最简单的搜索请求 | |
| 29 | + | |
| 30 | +```bash | |
| 31 | +curl -X POST "http://localhost:6002/search/" \ | |
| 32 | + -H "Content-Type: application/json" \ | |
| 33 | + -d '{ | |
| 34 | + "query": "芭比娃娃" | |
| 35 | + }' | |
| 36 | +``` | |
| 37 | + | |
| 38 | +### Python示例 | |
| 39 | + | |
| 40 | +```python | |
| 41 | +import requests | |
| 42 | + | |
| 43 | +url = "http://localhost:6002/search/" | |
| 44 | +response = requests.post(url, json={"query": "芭比娃娃"}) | |
| 45 | +data = response.json() | |
| 46 | +print(f"找到 {data['total']} 个结果") | |
| 47 | +``` | |
| 48 | + | |
| 49 | +### JavaScript示例 | |
| 50 | + | |
| 51 | +```javascript | |
| 52 | +const response = await fetch('http://localhost:6002/search/', { | |
| 53 | + method: 'POST', | |
| 54 | + headers: { | |
| 55 | + 'Content-Type': 'application/json' | |
| 56 | + }, | |
| 57 | + body: JSON.stringify({ | |
| 58 | + query: '芭比娃娃' | |
| 59 | + }) | |
| 60 | +}); | |
| 61 | +const data = await response.json(); | |
| 62 | +console.log(`找到 ${data.total} 个结果`); | |
| 63 | +``` | |
| 64 | + | |
| 65 | +--- | |
| 66 | + | |
| 67 | +## 接口概览 | |
| 68 | + | |
| 69 | +| 接口 | 方法 | 路径 | 说明 | | |
| 70 | +|------|------|------|------| | |
| 71 | +| 文本搜索 | POST | `/search/` | 执行文本搜索查询 | | |
| 72 | +| 图片搜索 | POST | `/search/image` | 基于图片相似度搜索 | | |
| 73 | +| 搜索建议 | GET | `/search/suggestions` | 获取搜索建议(框架,暂未实现) | | |
| 74 | +| 获取文档 | GET | `/search/{doc_id}` | 根据ID获取单个文档 | | |
| 75 | +| 健康检查 | GET | `/admin/health` | 检查服务状态 | | |
| 76 | + | |
| 77 | +--- | |
| 78 | + | |
| 79 | +## 文本搜索接口 | |
| 80 | + | |
| 81 | +### 接口信息 | |
| 82 | + | |
| 83 | +- **端点**: `POST /search/` | |
| 84 | +- **描述**: 执行文本搜索查询,支持多语言、布尔表达式、过滤器和分面搜索 | |
| 85 | + | |
| 86 | +### 请求参数 | |
| 87 | + | |
| 88 | +#### 完整请求体结构 | |
| 89 | + | |
| 90 | +```json | |
| 91 | +{ | |
| 92 | + "query": "string (required)", | |
| 93 | + "size": 10, | |
| 94 | + "from": 0, | |
| 95 | + "filters": {}, | |
| 96 | + "range_filters": {}, | |
| 97 | + "facets": [], | |
| 98 | + "sort_by": "string", | |
| 99 | + "sort_order": "desc", | |
| 100 | + "min_score": 0.0, | |
| 101 | + "debug": false, | |
| 102 | + "user_id": "string", | |
| 103 | + "session_id": "string" | |
| 104 | +} | |
| 105 | +``` | |
| 106 | + | |
| 107 | +#### 参数详细说明 | |
| 108 | + | |
| 109 | +| 参数 | 类型 | 必填 | 默认值 | 说明 | | |
| 110 | +|------|------|------|--------|------| | |
| 111 | +| `query` | string | ✅ | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) | | |
| 112 | +| `size` | integer | ❌ | 10 | 返回结果数量(1-100) | | |
| 113 | +| `from` | integer | ❌ | 0 | 分页偏移量(用于分页) | | |
| 114 | +| `filters` | object | ❌ | null | 精确匹配过滤器(见下文) | | |
| 115 | +| `range_filters` | object | ❌ | null | 数值范围过滤器(见下文) | | |
| 116 | +| `facets` | array | ❌ | null | 分面配置(见下文) | | |
| 117 | +| `sort_by` | string | ❌ | null | 排序字段名(如 `min_price`, `max_price`, `title`) | | |
| 118 | +| `sort_order` | string | ❌ | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | | |
| 119 | +| `min_score` | float | ❌ | null | 最小相关性分数阈值 | | |
| 120 | +| `debug` | boolean | ❌ | false | 是否返回调试信息 | | |
| 121 | +| `user_id` | string | ❌ | null | 用户ID(用于个性化,预留) | | |
| 122 | +| `session_id` | string | ❌ | null | 会话ID(用于分析,预留) | | |
| 123 | + | |
| 124 | +### 过滤器详解 | |
| 125 | + | |
| 126 | +#### 1. 精确匹配过滤器 (filters) | |
| 127 | + | |
| 128 | +用于精确匹配或多值匹配(OR 逻辑)。 | |
| 129 | + | |
| 130 | +**格式**: | |
| 131 | +```json | |
| 132 | +{ | |
| 133 | + "filters": { | |
| 134 | + "category_keyword": "玩具", // 单值:精确匹配 | |
| 135 | + "vendor_keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR) | |
| 136 | + "product_type_keyword": "益智玩具" // 单值:精确匹配 | |
| 137 | + } | |
| 138 | +} | |
| 139 | +``` | |
| 140 | + | |
| 141 | +**支持的值类型**: | |
| 142 | +- 字符串:精确匹配 | |
| 143 | +- 整数:精确匹配 | |
| 144 | +- 布尔值:精确匹配 | |
| 145 | +- 数组:匹配任意值(OR 逻辑) | |
| 146 | + | |
| 147 | +**常用过滤字段**: | |
| 148 | +- `category_keyword`: 类目 | |
| 149 | +- `vendor_keyword`: 品牌/供应商 | |
| 150 | +- `product_type_keyword`: 商品类型 | |
| 151 | +- `tags_keyword`: 标签 | |
| 152 | + | |
| 153 | +#### 2. 范围过滤器 (range_filters) | |
| 154 | + | |
| 155 | +用于数值字段的范围过滤。 | |
| 156 | + | |
| 157 | +**格式**: | |
| 158 | +```json | |
| 159 | +{ | |
| 160 | + "range_filters": { | |
| 161 | + "min_price": { | |
| 162 | + "gte": 50, // 大于等于 | |
| 163 | + "lte": 200 // 小于等于 | |
| 164 | + }, | |
| 165 | + "max_price": { | |
| 166 | + "gt": 100 // 大于 | |
| 167 | + }, | |
| 168 | + "create_time": { | |
| 169 | + "gte": "2024-01-01T00:00:00Z" // 日期时间字符串 | |
| 170 | + } | |
| 171 | + } | |
| 172 | +} | |
| 173 | +``` | |
| 174 | + | |
| 175 | +**支持的操作符**: | |
| 176 | +- `gte`: 大于等于 (>=) | |
| 177 | +- `gt`: 大于 (>) | |
| 178 | +- `lte`: 小于等于 (<=) | |
| 179 | +- `lt`: 小于 (<) | |
| 180 | + | |
| 181 | +**注意**: 至少需要指定一个操作符。 | |
| 182 | + | |
| 183 | +**常用范围字段**: | |
| 184 | +- `min_price`: 最低价格 | |
| 185 | +- `max_price`: 最高价格 | |
| 186 | +- `compare_at_price`: 原价 | |
| 187 | +- `create_time`: 创建时间 | |
| 188 | +- `update_time`: 更新时间 | |
| 189 | + | |
| 190 | +#### 3. 分面配置 (facets) | |
| 191 | + | |
| 192 | +用于生成分面统计(分组聚合),常用于构建筛选器UI。 | |
| 193 | + | |
| 194 | +**简单模式**(字符串数组): | |
| 195 | +```json | |
| 196 | +{ | |
| 197 | + "facets": ["category_keyword", "vendor_keyword"] | |
| 198 | +} | |
| 199 | +``` | |
| 200 | + | |
| 201 | +**高级模式**(配置对象数组): | |
| 202 | +```json | |
| 203 | +{ | |
| 204 | + "facets": [ | |
| 205 | + { | |
| 206 | + "field": "category_keyword", | |
| 207 | + "size": 15, | |
| 208 | + "type": "terms" | |
| 209 | + }, | |
| 210 | + { | |
| 211 | + "field": "min_price", | |
| 212 | + "type": "range", | |
| 213 | + "ranges": [ | |
| 214 | + {"key": "0-50", "to": 50}, | |
| 215 | + {"key": "50-100", "from": 50, "to": 100}, | |
| 216 | + {"key": "100-200", "from": 100, "to": 200}, | |
| 217 | + {"key": "200+", "from": 200} | |
| 218 | + ] | |
| 219 | + } | |
| 220 | + ] | |
| 221 | +} | |
| 222 | +``` | |
| 223 | + | |
| 224 | +**分面配置参数**: | |
| 225 | +- `field`: 字段名(必填) | |
| 226 | +- `size`: 返回的分组数量(默认:10,范围:1-100) | |
| 227 | +- `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计) | |
| 228 | +- `ranges`: 范围定义(仅当 type='range' 时需要) | |
| 229 | + | |
| 230 | +### 布尔表达式语法 | |
| 231 | + | |
| 232 | +搜索查询支持布尔表达式,提供更灵活的搜索能力。 | |
| 233 | + | |
| 234 | +**支持的操作符**: | |
| 235 | + | |
| 236 | +| 操作符 | 描述 | 示例 | | |
| 237 | +|--------|------|------| | |
| 238 | +| `AND` | 所有词必须匹配 | `玩具 AND 乐高` | | |
| 239 | +| `OR` | 任意词匹配 | `芭比 OR 娃娃` | | |
| 240 | +| `ANDNOT` | 排除特定词 | `玩具 ANDNOT 电动` | | |
| 241 | +| `RANK` | 排序加权(不强制匹配) | `玩具 RANK 乐高` | | |
| 242 | +| `()` | 分组 | `玩具 AND (乐高 OR 芭比)` | | |
| 243 | + | |
| 244 | +**操作符优先级**(从高到低): | |
| 245 | +1. `()` - 括号 | |
| 246 | +2. `ANDNOT` - 排除 | |
| 247 | +3. `AND` - 与 | |
| 248 | +4. `OR` - 或 | |
| 249 | +5. `RANK` - 排序 | |
| 250 | + | |
| 251 | +**示例**: | |
| 252 | +``` | |
| 253 | +"芭比娃娃" // 简单查询 | |
| 254 | +"玩具 AND 乐高" // AND 查询 | |
| 255 | +"芭比 OR 娃娃" // OR 查询 | |
| 256 | +"玩具 ANDNOT 电动" // 排除查询 | |
| 257 | +"玩具 AND (乐高 OR 芭比)" // 复杂查询 | |
| 258 | +``` | |
| 259 | + | |
| 260 | +--- | |
| 261 | + | |
| 262 | +## 图片搜索接口 | |
| 263 | + | |
| 264 | +### 接口信息 | |
| 265 | + | |
| 266 | +- **端点**: `POST /search/image` | |
| 267 | +- **描述**: 基于图片相似度进行搜索,使用图片向量进行语义匹配 | |
| 268 | + | |
| 269 | +### 请求参数 | |
| 270 | + | |
| 271 | +```json | |
| 272 | +{ | |
| 273 | + "image_url": "string (required)", | |
| 274 | + "size": 10, | |
| 275 | + "filters": {}, | |
| 276 | + "range_filters": {} | |
| 277 | +} | |
| 278 | +``` | |
| 279 | + | |
| 280 | +### 参数说明 | |
| 281 | + | |
| 282 | +| 参数 | 类型 | 必填 | 默认值 | 描述 | | |
| 283 | +|------|------|------|--------|------| | |
| 284 | +| `image_url` | string | ✅ | - | 查询图片的 URL | | |
| 285 | +| `size` | integer | ❌ | 10 | 返回结果数量(1-100) | | |
| 286 | +| `filters` | object | ❌ | null | 精确匹配过滤器 | | |
| 287 | +| `range_filters` | object | ❌ | null | 数值范围过滤器 | | |
| 288 | + | |
| 289 | +### 请求示例 | |
| 290 | + | |
| 291 | +```bash | |
| 292 | +curl -X POST "http://localhost:6002/search/image" \ | |
| 293 | + -H "Content-Type: application/json" \ | |
| 294 | + -d '{ | |
| 295 | + "image_url": "https://example.com/barbie.jpg", | |
| 296 | + "size": 20, | |
| 297 | + "filters": { | |
| 298 | + "category_keyword": "玩具" | |
| 299 | + }, | |
| 300 | + "range_filters": { | |
| 301 | + "min_price": { | |
| 302 | + "lte": 100 | |
| 303 | + } | |
| 304 | + } | |
| 305 | + }' | |
| 306 | +``` | |
| 307 | + | |
| 308 | +--- | |
| 309 | + | |
| 310 | +## 响应格式说明 | |
| 311 | + | |
| 312 | +### 标准响应结构 | |
| 313 | + | |
| 314 | +```json | |
| 315 | +{ | |
| 316 | + "results": [ | |
| 317 | + { | |
| 318 | + "product_id": "12345", | |
| 319 | + "title": "芭比时尚娃娃", | |
| 320 | + "handle": "barbie-doll", | |
| 321 | + "description": "高品质芭比娃娃", | |
| 322 | + "vendor": "美泰", | |
| 323 | + "product_type": "玩具", | |
| 324 | + "tags": "娃娃, 玩具, 女孩", | |
| 325 | + "price": 89.99, | |
| 326 | + "compare_at_price": 129.99, | |
| 327 | + "currency": "USD", | |
| 328 | + "image_url": "https://example.com/image.jpg", | |
| 329 | + "in_stock": true, | |
| 330 | + "variants": [ | |
| 331 | + { | |
| 332 | + "variant_id": "67890", | |
| 333 | + "title": "粉色款", | |
| 334 | + "price": 89.99, | |
| 335 | + "compare_at_price": 129.99, | |
| 336 | + "sku": "BARBIE-001", | |
| 337 | + "stock": 100, | |
| 338 | + "options": { | |
| 339 | + "option1": "粉色", | |
| 340 | + "option2": "标准款" | |
| 341 | + } | |
| 342 | + } | |
| 343 | + ], | |
| 344 | + "relevance_score": 8.5 | |
| 345 | + } | |
| 346 | + ], | |
| 347 | + "total": 118, | |
| 348 | + "max_score": 8.5, | |
| 349 | + "facets": [ | |
| 350 | + { | |
| 351 | + "field": "category_keyword", | |
| 352 | + "label": "category_keyword", | |
| 353 | + "type": "terms", | |
| 354 | + "values": [ | |
| 355 | + { | |
| 356 | + "value": "玩具", | |
| 357 | + "label": "玩具", | |
| 358 | + "count": 85, | |
| 359 | + "selected": false | |
| 360 | + } | |
| 361 | + ] | |
| 362 | + } | |
| 363 | + ], | |
| 364 | + "query_info": { | |
| 365 | + "original_query": "芭比娃娃", | |
| 366 | + "detected_language": "zh", | |
| 367 | + "translations": { | |
| 368 | + "en": "barbie doll" | |
| 369 | + } | |
| 370 | + }, | |
| 371 | + "suggestions": [], | |
| 372 | + "related_searches": [], | |
| 373 | + "took_ms": 45, | |
| 374 | + "performance_info": null, | |
| 375 | + "debug_info": null | |
| 376 | +} | |
| 377 | +``` | |
| 378 | + | |
| 379 | +### 响应字段说明 | |
| 380 | + | |
| 381 | +| 字段 | 类型 | 说明 | | |
| 382 | +|------|------|------| | |
| 383 | +| `results` | array | 搜索结果列表(ProductResult对象数组) | | |
| 384 | +| `results[].product_id` | string | 商品ID | | |
| 385 | +| `results[].title` | string | 商品标题 | | |
| 386 | +| `results[].price` | float | 价格(min_price) | | |
| 387 | +| `results[].variants` | array | 变体列表(SKU列表) | | |
| 388 | +| `results[].relevance_score` | float | 相关性分数 | | |
| 389 | +| `total` | integer | 匹配的总文档数 | | |
| 390 | +| `max_score` | float | 最高相关性分数 | | |
| 391 | +| `facets` | array | 分面统计结果 | | |
| 392 | +| `query_info` | object | 查询处理信息 | | |
| 393 | +| `took_ms` | integer | 搜索耗时(毫秒) | | |
| 394 | + | |
| 395 | +### ProductResult字段说明 | |
| 396 | + | |
| 397 | +| 字段 | 类型 | 说明 | | |
| 398 | +|------|------|------| | |
| 399 | +| `product_id` | string | 商品ID(SPU ID) | | |
| 400 | +| `title` | string | 商品标题 | | |
| 401 | +| `handle` | string | 商品URL handle | | |
| 402 | +| `description` | string | 商品描述 | | |
| 403 | +| `vendor` | string | 供应商/品牌 | | |
| 404 | +| `product_type` | string | 商品类型 | | |
| 405 | +| `tags` | string | 标签 | | |
| 406 | +| `price` | float | 价格(min_price) | | |
| 407 | +| `compare_at_price` | float | 原价 | | |
| 408 | +| `currency` | string | 货币单位(默认USD) | | |
| 409 | +| `image_url` | string | 主图URL | | |
| 410 | +| `in_stock` | boolean | 是否有库存(任意变体有库存即为true) | | |
| 411 | +| `variants` | array | 变体列表 | | |
| 412 | +| `relevance_score` | float | 相关性分数 | | |
| 413 | + | |
| 414 | +### VariantResult字段说明 | |
| 415 | + | |
| 416 | +| 字段 | 类型 | 说明 | | |
| 417 | +|------|------|------| | |
| 418 | +| `variant_id` | string | 变体ID(SKU ID) | | |
| 419 | +| `title` | string | 变体标题 | | |
| 420 | +| `price` | float | 价格 | | |
| 421 | +| `compare_at_price` | float | 原价 | | |
| 422 | +| `sku` | string | SKU编码 | | |
| 423 | +| `stock` | integer | 库存数量 | | |
| 424 | +| `options` | object | 选项(颜色、尺寸等) | | |
| 425 | + | |
| 426 | +--- | |
| 427 | + | |
| 428 | +## 常见场景示例 | |
| 429 | + | |
| 430 | +### 场景1:商品列表页搜索 | |
| 431 | + | |
| 432 | +**需求**: 搜索"玩具",按价格从低到高排序,显示前20个结果 | |
| 433 | + | |
| 434 | +```json | |
| 435 | +{ | |
| 436 | + "query": "玩具", | |
| 437 | + "size": 20, | |
| 438 | + "from": 0, | |
| 439 | + "sort_by": "min_price", | |
| 440 | + "sort_order": "asc" | |
| 441 | +} | |
| 442 | +``` | |
| 443 | + | |
| 444 | +### 场景2:带筛选的商品搜索 | |
| 445 | + | |
| 446 | +**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 | |
| 447 | + | |
| 448 | +```json | |
| 449 | +{ | |
| 450 | + "query": "玩具", | |
| 451 | + "size": 20, | |
| 452 | + "filters": { | |
| 453 | + "category_keyword": "益智玩具" | |
| 454 | + }, | |
| 455 | + "range_filters": { | |
| 456 | + "min_price": { | |
| 457 | + "gte": 50, | |
| 458 | + "lte": 200 | |
| 459 | + } | |
| 460 | + } | |
| 461 | +} | |
| 462 | +``` | |
| 463 | + | |
| 464 | +### 场景3:带分面的商品搜索 | |
| 465 | + | |
| 466 | +**需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器 | |
| 467 | + | |
| 468 | +```json | |
| 469 | +{ | |
| 470 | + "query": "玩具", | |
| 471 | + "size": 20, | |
| 472 | + "facets": [ | |
| 473 | + "category_keyword", | |
| 474 | + "vendor_keyword" | |
| 475 | + ] | |
| 476 | +} | |
| 477 | +``` | |
| 478 | + | |
| 479 | +### 场景4:多条件组合搜索 | |
| 480 | + | |
| 481 | +**需求**: 搜索"玩具",筛选多个品牌,价格范围,并获取分面统计 | |
| 482 | + | |
| 483 | +```json | |
| 484 | +{ | |
| 485 | + "query": "玩具", | |
| 486 | + "size": 20, | |
| 487 | + "filters": { | |
| 488 | + "vendor_keyword": ["乐高", "孩之宝", "美泰"] | |
| 489 | + }, | |
| 490 | + "range_filters": { | |
| 491 | + "min_price": { | |
| 492 | + "gte": 50, | |
| 493 | + "lte": 200 | |
| 494 | + } | |
| 495 | + }, | |
| 496 | + "facets": [ | |
| 497 | + { | |
| 498 | + "field": "category_keyword", | |
| 499 | + "size": 15 | |
| 500 | + }, | |
| 501 | + { | |
| 502 | + "field": "min_price", | |
| 503 | + "type": "range", | |
| 504 | + "ranges": [ | |
| 505 | + {"key": "0-50", "to": 50}, | |
| 506 | + {"key": "50-100", "from": 50, "to": 100}, | |
| 507 | + {"key": "100-200", "from": 100, "to": 200}, | |
| 508 | + {"key": "200+", "from": 200} | |
| 509 | + ] | |
| 510 | + } | |
| 511 | + ], | |
| 512 | + "sort_by": "min_price", | |
| 513 | + "sort_order": "asc" | |
| 514 | +} | |
| 515 | +``` | |
| 516 | + | |
| 517 | +### 场景5:布尔表达式搜索 | |
| 518 | + | |
| 519 | +**需求**: 搜索包含"玩具"和"乐高"的商品,排除"电动" | |
| 520 | + | |
| 521 | +```json | |
| 522 | +{ | |
| 523 | + "query": "玩具 AND 乐高 ANDNOT 电动", | |
| 524 | + "size": 20 | |
| 525 | +} | |
| 526 | +``` | |
| 527 | + | |
| 528 | +### 场景6:分页查询 | |
| 529 | + | |
| 530 | +**需求**: 获取第2页结果(每页20条) | |
| 531 | + | |
| 532 | +```json | |
| 533 | +{ | |
| 534 | + "query": "玩具", | |
| 535 | + "size": 20, | |
| 536 | + "from": 20 | |
| 537 | +} | |
| 538 | +``` | |
| 539 | + | |
| 540 | +--- | |
| 541 | + | |
| 542 | +## 错误处理 | |
| 543 | + | |
| 544 | +### 错误响应格式 | |
| 545 | + | |
| 546 | +```json | |
| 547 | +{ | |
| 548 | + "error": "错误信息", | |
| 549 | + "detail": "详细错误信息(可选)" | |
| 550 | +} | |
| 551 | +``` | |
| 552 | + | |
| 553 | +### 常见错误码 | |
| 554 | + | |
| 555 | +| HTTP状态码 | 说明 | 处理建议 | | |
| 556 | +|-----------|------|---------| | |
| 557 | +| 200 | 成功 | - | | |
| 558 | +| 400 | 请求参数错误 | 检查请求参数格式和必填字段 | | |
| 559 | +| 404 | 接口不存在 | 检查接口路径 | | |
| 560 | +| 500 | 服务器内部错误 | 联系技术支持 | | |
| 561 | + | |
| 562 | +### 错误处理示例 | |
| 563 | + | |
| 564 | +**Python**: | |
| 565 | +```python | |
| 566 | +import requests | |
| 567 | + | |
| 568 | +try: | |
| 569 | + response = requests.post(url, json=payload, timeout=10) | |
| 570 | + response.raise_for_status() | |
| 571 | + data = response.json() | |
| 572 | +except requests.exceptions.HTTPError as e: | |
| 573 | + print(f"HTTP错误: {e}") | |
| 574 | + if response.status_code == 400: | |
| 575 | + error_data = response.json() | |
| 576 | + print(f"错误详情: {error_data.get('detail')}") | |
| 577 | +except requests.exceptions.RequestException as e: | |
| 578 | + print(f"请求异常: {e}") | |
| 579 | +``` | |
| 580 | + | |
| 581 | +**JavaScript**: | |
| 582 | +```javascript | |
| 583 | +try { | |
| 584 | + const response = await fetch(url, { | |
| 585 | + method: 'POST', | |
| 586 | + headers: { 'Content-Type': 'application/json' }, | |
| 587 | + body: JSON.stringify(payload) | |
| 588 | + }); | |
| 589 | + | |
| 590 | + if (!response.ok) { | |
| 591 | + const error = await response.json(); | |
| 592 | + throw new Error(error.error || `HTTP ${response.status}`); | |
| 593 | + } | |
| 594 | + | |
| 595 | + const data = await response.json(); | |
| 596 | +} catch (error) { | |
| 597 | + console.error('搜索失败:', error.message); | |
| 598 | +} | |
| 599 | +``` | |
| 600 | + | |
| 601 | +--- | |
| 602 | + | |
| 603 | +### 5. 代码示例 | |
| 604 | + | |
| 605 | +**完整的搜索函数(Python)**: | |
| 606 | + | |
| 607 | +```python | |
| 608 | +import requests | |
| 609 | +from typing import Dict, Any, Optional, List | |
| 610 | + | |
| 611 | +class SearchClient: | |
| 612 | + def __init__(self, base_url: str = "http://localhost:6002"): | |
| 613 | + self.base_url = base_url | |
| 614 | + self.timeout = 10 | |
| 615 | + | |
| 616 | + def search( | |
| 617 | + self, | |
| 618 | + query: str, | |
| 619 | + size: int = 20, | |
| 620 | + from_: int = 0, | |
| 621 | + filters: Optional[Dict] = None, | |
| 622 | + range_filters: Optional[Dict] = None, | |
| 623 | + facets: Optional[List] = None, | |
| 624 | + sort_by: Optional[str] = None, | |
| 625 | + sort_order: str = "desc" | |
| 626 | + ) -> Dict[str, Any]: | |
| 627 | + """ | |
| 628 | + 执行搜索查询 | |
| 629 | + | |
| 630 | + Args: | |
| 631 | + query: 搜索查询字符串 | |
| 632 | + size: 返回结果数量 | |
| 633 | + from_: 分页偏移量 | |
| 634 | + filters: 精确匹配过滤器 | |
| 635 | + range_filters: 范围过滤器 | |
| 636 | + facets: 分面配置 | |
| 637 | + sort_by: 排序字段 | |
| 638 | + sort_order: 排序方向 | |
| 639 | + | |
| 640 | + Returns: | |
| 641 | + 搜索结果字典 | |
| 642 | + """ | |
| 643 | + url = f"{self.base_url}/search/" | |
| 644 | + payload = { | |
| 645 | + "query": query, | |
| 646 | + "size": size, | |
| 647 | + "from": from_, | |
| 648 | + } | |
| 649 | + | |
| 650 | + if filters: | |
| 651 | + payload["filters"] = filters | |
| 652 | + if range_filters: | |
| 653 | + payload["range_filters"] = range_filters | |
| 654 | + if facets: | |
| 655 | + payload["facets"] = facets | |
| 656 | + if sort_by: | |
| 657 | + payload["sort_by"] = sort_by | |
| 658 | + payload["sort_order"] = sort_order | |
| 659 | + | |
| 660 | + try: | |
| 661 | + response = requests.post( | |
| 662 | + url, | |
| 663 | + json=payload, | |
| 664 | + timeout=self.timeout | |
| 665 | + ) | |
| 666 | + response.raise_for_status() | |
| 667 | + return response.json() | |
| 668 | + except requests.exceptions.RequestException as e: | |
| 669 | + raise Exception(f"搜索请求失败: {e}") | |
| 670 | + | |
| 671 | +# 使用示例 | |
| 672 | +client = SearchClient() | |
| 673 | +result = client.search( | |
| 674 | + query="玩具", | |
| 675 | + size=20, | |
| 676 | + filters={"category_keyword": "益智玩具"}, | |
| 677 | + range_filters={"min_price": {"gte": 50, "lte": 200}}, | |
| 678 | + facets=["category_keyword", "vendor_keyword"], | |
| 679 | + sort_by="min_price", | |
| 680 | + sort_order="asc" | |
| 681 | +) | |
| 682 | + | |
| 683 | +print(f"找到 {result['total']} 个结果") | |
| 684 | +for product in result['results']: | |
| 685 | + print(f"{product['title']} - ¥{product['price']}") | |
| 686 | +``` | |
| 687 | + | |
| 688 | +**完整的搜索函数(JavaScript)**: | |
| 689 | + | |
| 690 | +```javascript | |
| 691 | +class SearchClient { | |
| 692 | + constructor(baseUrl = 'http://localhost:6002') { | |
| 693 | + this.baseUrl = baseUrl; | |
| 694 | + this.timeout = 10000; | |
| 695 | + } | |
| 696 | + | |
| 697 | + async search({ | |
| 698 | + query, | |
| 699 | + size = 20, | |
| 700 | + from = 0, | |
| 701 | + filters = null, | |
| 702 | + rangeFilters = null, | |
| 703 | + facets = null, | |
| 704 | + sortBy = null, | |
| 705 | + sortOrder = 'desc' | |
| 706 | + }) { | |
| 707 | + const url = `${this.baseUrl}/search/`; | |
| 708 | + const payload = { | |
| 709 | + query, | |
| 710 | + size, | |
| 711 | + from, | |
| 712 | + }; | |
| 713 | + | |
| 714 | + if (filters) payload.filters = filters; | |
| 715 | + if (rangeFilters) payload.range_filters = rangeFilters; | |
| 716 | + if (facets) payload.facets = facets; | |
| 717 | + if (sortBy) { | |
| 718 | + payload.sort_by = sortBy; | |
| 719 | + payload.sort_order = sortOrder; | |
| 720 | + } | |
| 721 | + | |
| 722 | + try { | |
| 723 | + const response = await fetch(url, { | |
| 724 | + method: 'POST', | |
| 725 | + headers: { | |
| 726 | + 'Content-Type': 'application/json' | |
| 727 | + }, | |
| 728 | + body: JSON.stringify(payload), | |
| 729 | + signal: AbortSignal.timeout(this.timeout) | |
| 730 | + }); | |
| 731 | + | |
| 732 | + if (!response.ok) { | |
| 733 | + const error = await response.json(); | |
| 734 | + throw new Error(error.error || `HTTP ${response.status}`); | |
| 735 | + } | |
| 736 | + | |
| 737 | + return await response.json(); | |
| 738 | + } catch (error) { | |
| 739 | + throw new Error(`搜索请求失败: ${error.message}`); | |
| 740 | + } | |
| 741 | + } | |
| 742 | +} | |
| 743 | + | |
| 744 | +// 使用示例 | |
| 745 | +const client = new SearchClient(); | |
| 746 | +const result = await client.search({ | |
| 747 | + query: '玩具', | |
| 748 | + size: 20, | |
| 749 | + filters: { category_keyword: '益智玩具' }, | |
| 750 | + rangeFilters: { min_price: { gte: 50, lte: 200 } }, | |
| 751 | + facets: ['category_keyword', 'vendor_keyword'], | |
| 752 | + sortBy: 'min_price', | |
| 753 | + sortOrder: 'asc' | |
| 754 | +}); | |
| 755 | + | |
| 756 | +console.log(`找到 ${result.total} 个结果`); | |
| 757 | +result.results.forEach(product => { | |
| 758 | + console.log(`${product.title} - ¥${product.price}`); | |
| 759 | +}); | |
| 760 | +``` | |
| 761 | + | |
| 762 | +--- | |
| 763 | + | |
| 764 | +## 附录 | |
| 765 | + | |
| 766 | +### 常用字段列表 | |
| 767 | + | |
| 768 | +#### 过滤字段(使用 `*_keyword` 后缀) | |
| 769 | + | |
| 770 | +- `category_keyword`: 类目 | |
| 771 | +- `vendor_keyword`: 品牌/供应商 | |
| 772 | +- `product_type_keyword`: 商品类型 | |
| 773 | +- `tags_keyword`: 标签 | |
| 774 | + | |
| 775 | +#### 范围字段 | |
| 776 | + | |
| 777 | +- `min_price`: 最低价格 | |
| 778 | +- `max_price`: 最高价格 | |
| 779 | +- `compare_at_price`: 原价 | |
| 780 | +- `create_time`: 创建时间 | |
| 781 | +- `update_time`: 更新时间 | |
| 782 | + | |
| 783 | +#### 排序字段 | |
| 784 | + | |
| 785 | +- `min_price`: 最低价格 | |
| 786 | +- `max_price`: 最高价格 | |
| 787 | +- `title`: 标题(字母序) | |
| 788 | +- `create_time`: 创建时间 | |
| 789 | +- `update_time`: 更新时间 | |
| 790 | +- `relevance_score`: 相关性分数(默认) | |
| 791 | + | ... | ... |
ES_QUERY_RESTRUCTURE_COMPLETE.md
HighLevelDesign.md
| ... | ... | @@ -51,112 +51,6 @@ updater varchar(64) |
| 51 | 51 | update_time datetime |
| 52 | 52 | deleted bit(1) |
| 53 | 53 | |
| 54 | -所有租户共用这个主表 | |
| 55 | - | |
| 56 | -### 每个租户的辅表 | |
| 57 | -各个租户,有自己的扩展表。 入索引的时候,商品主表 shoplazza_product_sku 的 id + shopid,拼接租户自己单独的扩展表(比如可以放一些自己的属性体系、各种语言的商品名、品牌名、标签、分类等) | |
| 58 | - | |
| 59 | -但是,各个租户,可能有不一样的业务数据,比如不同租户有不同的属性的体系、不同语言的商品标题(一般至少有中英文两种满足跨境的搜索需求),有不同的权重(提权)字段、业务过滤和聚合字段。 | |
| 60 | -能够统一的 只能是 sku表 按照一套配置规范、做一个配置文件,按照配置文件建设ES mapping结构以及做数据的入库。 | |
| 61 | - | |
| 62 | -## SearchEngine | |
| 63 | - | |
| 64 | -### IndexerConfig | |
| 65 | - @阿里opensearch电商行业.md, 有两套配置 | |
| 66 | -1. 应用结构配置 : 定义了ES的输入数据有哪些字段、关联mysql的哪些字段. | |
| 67 | -2. 索引结构配置 : 定义了ES的字段,每个字段的索引mapping配置,支持各个域的查询,包括默认的域的查询。索引配置预定一号了一堆分析方式 由 @商品数据源入ES配置规范.md 定义。 | |
| 68 | - | |
| 69 | - | |
| 70 | - | |
| 71 | -### 测试数据灌入 | |
| 72 | - | |
| 73 | -灌入数据、mysql到ES的自动同步,不在本项目的范围内,另外有java项目负责。 | |
| 74 | -但是,该项目 为了提供测试数据,需要 构造一个实例 tenant1. | |
| 75 | -我们为他构造一套应用配置和索引配置。 | |
| 76 | -灌入一批测试数据,可以些一个简单的 全量灌入的实现。 | |
| 77 | -数据源地址在:data/tenant1/goods_with_pic.5years_congku.csv.shuf.1w | |
| 78 | -请根据这里面的字段,建设辅助表(注意看哪些字段在主表有,哪些需要放到辅表) | |
| 79 | -然后写一个程序,将数据分别灌入主表和辅表。 | |
| 80 | - | |
| 81 | - | |
| 82 | -### queryParser | |
| 83 | -query分析,做以下几个事情: | |
| 84 | - | |
| 85 | -1. 查询改写。 配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 | |
| 86 | -2. 翻译。配置需要得到的几种目标语言。 在tenant1测试案例中,我们配置 zh en两种语言。先对query做语言检测,如果query是中文那么要翻译一下en,如果是en那么要翻译zh,如果两者都不是那么zh en都需要翻译。 | |
| 87 | -3. 如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 | |
| 88 | -翻译代码参考: | |
| 89 | -``` | |
| 90 | -import requests | |
| 91 | -api_url = "https://api.deepl.com/v2/translate" | |
| 92 | -headers = { | |
| 93 | - "Authorization": "DeepL-Auth-Key YOUR_AUTH_KEY", | |
| 94 | - "Content-Type": "application/json", | |
| 95 | -} | |
| 96 | -payload = { | |
| 97 | - "text": ["要翻译的文本"], | |
| 98 | - "target_lang": "ZH", # 中文 | |
| 99 | -} | |
| 100 | - | |
| 101 | -response = requests.post(api_url, headers=headers, json=payload, timeout=10) | |
| 102 | - | |
| 103 | -if response.status_code == 200: | |
| 104 | - data = response.json() | |
| 105 | - translation = data["translations"][0]["text"] | |
| 106 | - print(translation) | |
| 107 | -``` | |
| 108 | - | |
| 109 | -### searcher | |
| 110 | - | |
| 111 | -支持多种检索表达式: | |
| 112 | -支持多种匹配方式,如AND、OR、RANK、NOTAND以及(),优先级从高到低为(),ANDNOT,AND,OR,RANK。 | |
| 113 | - | |
| 114 | -default域的相关性,是代码里面单独计算,是特定的深度定制优化的,暂时不做配置化。 | |
| 115 | - | |
| 116 | -暂时具体实现为 bm25()+0.2*text_embedding_relevence(也就是knn检索表达式的打分) | |
| 117 | -bm25() 包括多语言的打分:内部需要通过配置翻译为多种语言(配置几种目标语言 默认中文、英文,并且设置对应的检索域),然后分别到对应的字段搜索,中文字段到配置的中文title搜索,英文到对应的英文title搜索。 | |
| 118 | -bm25打分(base_query): | |
| 119 | -"multi_match": { | |
| 120 | - "query": search_query, | |
| 121 | - "fields": match_fields, | |
| 122 | - "minimum_should_match": "67%", | |
| 123 | - "tie_breaker": 0.9, | |
| 124 | - "boost": 1.0, # Low boost for auxiliary keyword query | |
| 125 | - "_name": "base_query" | |
| 126 | -} | |
| 127 | - | |
| 128 | -text_embedding_relevence: | |
| 129 | - knn_query = { | |
| 130 | - "knn": { | |
| 131 | - "field": text_embedding_field, | |
| 132 | - "query_vector": query_vector.tolist(), | |
| 133 | - "k": KNN_K, | |
| 134 | - "num_candidates": KNN_NUM_CANDIDATES | |
| 135 | - } | |
| 136 | - } | |
| 137 | - | |
| 138 | -支持配置化的排序打分: | |
| 139 | -default域 支持配置的排序方式: | |
| 140 | -| 场景 | 表达式 | 含义 | | |
| 141 | -|------|--------|------| | |
| 142 | -| 电商 | `text_re()+general_score*2+timeliness(end_time)` | 文本分、宝贝综合分值、过期时间 | | |
| 143 | - | |
| 144 | - | |
| 145 | -有一个配置,是否按照spu聚合,如果打开spu聚合,那么 要配置spu_id的字段,检索表达上需要加上: | |
| 146 | -es_query["aggs"]["unique_count"] = { | |
| 147 | - "cardinality": { | |
| 148 | - "field": spu_id_field_name | |
| 149 | - } | |
| 150 | -} | |
| 151 | -es_query["collapse"]["inner_hits"] = { | |
| 152 | - "_source": False, | |
| 153 | - "name": "top_docs", | |
| 154 | - "size": INNER_HITS_SIZE | |
| 155 | -} | |
| 156 | - | |
| 157 | - | |
| 158 | -## 相关配置 | |
| 159 | - | |
| 160 | 54 | ES_CONFIG = { |
| 161 | 55 | 'host': 'http://localhost:9200', |
| 162 | 56 | 'username': 'essa', | ... | ... |
| ... | ... | @@ -0,0 +1,223 @@ |
| 1 | +# 索引字段说明文档 | |
| 2 | + | |
| 3 | +本文档详细说明了 Elasticsearch 索引中所有字段的类型、索引方式、数据来源等信息。 | |
| 4 | + | |
| 5 | +## 索引基本信息 | |
| 6 | + | |
| 7 | +- **索引名称**: `search_products` | |
| 8 | +- **索引级别**: SPU级别(商品级别) | |
| 9 | +- **数据结构**: SPU文档包含嵌套的variants(SKU)数组 | |
| 10 | + | |
| 11 | +## 字段说明表 | |
| 12 | + | |
| 13 | +### 基础字段 | |
| 14 | + | |
| 15 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | | |
| 16 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| | |
| 17 | +| tenant_id | KEYWORD | 是 | 精确匹配 | SPU表 | tenant_id | BIGINT | 租户ID,用于多租户隔离 | | |
| 18 | +| product_id | KEYWORD | 是 | 精确匹配 | SPU表 | id | BIGINT | 商品ID(SPU ID) | | |
| 19 | +| handle | KEYWORD | 是 | 精确匹配 | SPU表 | handle | VARCHAR(255) | 商品URL handle | | |
| 20 | + | |
| 21 | +### 文本搜索字段 | |
| 22 | + | |
| 23 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 说明 | | |
| 24 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|------| | |
| 25 | +| title | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | title | VARCHAR(512) | 3.0 | 商品标题,权重最高 | | |
| 26 | +| brief | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | brief | VARCHAR(512) | 1.5 | 商品简介 | | |
| 27 | +| description | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | description | TEXT | 1.0 | 商品详细描述 | | |
| 28 | + | |
| 29 | +### SEO字段 | |
| 30 | + | |
| 31 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 | | |
| 32 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------| | |
| 33 | +| seo_title | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_title | VARCHAR(512) | 2.0 | 否 | SEO标题,用于提升相关性 | | |
| 34 | +| seo_description | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_description | TEXT | 1.5 | 否 | SEO描述 | | |
| 35 | +| seo_keywords | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_keywords | VARCHAR(1024) | 2.0 | 否 | SEO关键词 | | |
| 36 | + | |
| 37 | +### 分类和标签字段 | |
| 38 | + | |
| 39 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 | | |
| 40 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------| | |
| 41 | +| vendor | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | 供应商/品牌(文本搜索) | | |
| 42 | +| vendor_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | vendor | VARCHAR(255) | - | 否 | 供应商/品牌(精确匹配,用于过滤) | | |
| 43 | +| product_type | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 商品类型(文本搜索) | | |
| 44 | +| product_type_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 商品类型(精确匹配,用于过滤) | | |
| 45 | +| tags | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | 标签(文本搜索) | | |
| 46 | +| tags_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | tags | VARCHAR(1024) | - | 否 | 标签(精确匹配,用于过滤) | | |
| 47 | +| category | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 类目(文本搜索) | | |
| 48 | +| category_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 类目(精确匹配,用于过滤) | | |
| 49 | + | |
| 50 | +### 价格字段 | |
| 51 | + | |
| 52 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | | |
| 53 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| | |
| 54 | +| min_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | price | DECIMAL(10,2) | 最低价格(从所有SKU中取最小值) | | |
| 55 | +| max_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | price | DECIMAL(10,2) | 最高价格(从所有SKU中取最大值) | | |
| 56 | +| compare_at_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | compare_at_price | DECIMAL(10,2) | 原价(从所有SKU中取最大值) | | |
| 57 | + | |
| 58 | +**价格计算逻辑**: | |
| 59 | +- `min_price`: 取该SPU下所有SKU的price字段的最小值 | |
| 60 | +- `max_price`: 取该SPU下所有SKU的price字段的最大值 | |
| 61 | +- `compare_at_price`: 取该SPU下所有SKU的compare_at_price字段的最大值(如果存在) | |
| 62 | + | |
| 63 | +### 图片字段 | |
| 64 | + | |
| 65 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | | |
| 66 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| | |
| 67 | +| image_url | KEYWORD | 否 | 不索引 | SPU表 | image_src | VARCHAR(500) | 商品主图URL,仅用于展示 | | |
| 68 | + | |
| 69 | +### 文本嵌入字段 | |
| 70 | + | |
| 71 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | | |
| 72 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| | |
| 73 | +| title_embedding | TEXT_EMBEDDING | 是 | 向量相似度(dot_product) | 计算生成 | title | VARCHAR(512) | 标题的文本向量(1024维),用于语义搜索 | | |
| 74 | + | |
| 75 | +**说明**: | |
| 76 | +- 向量维度:1024 | |
| 77 | +- 相似度算法:dot_product(点积) | |
| 78 | +- 数据来源:基于title字段通过BGE-M3模型生成 | |
| 79 | + | |
| 80 | +### 时间字段 | |
| 81 | + | |
| 82 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 是否返回 | 说明 | | |
| 83 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|---------|------| | |
| 84 | +| create_time | DATE | 是 | 日期范围 | SPU表 | create_time | DATETIME | 是 | 创建时间 | | |
| 85 | +| update_time | DATE | 是 | 日期范围 | SPU表 | update_time | DATETIME | 是 | 更新时间 | | |
| 86 | +| shoplazza_created_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_created_at | DATETIME | 否 | 店匠系统创建时间 | | |
| 87 | +| shoplazza_updated_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_updated_at | DATETIME | 否 | 店匠系统更新时间 | | |
| 88 | + | |
| 89 | +### 嵌套Variants字段(SKU级别) | |
| 90 | + | |
| 91 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | | |
| 92 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| | |
| 93 | +| variants | JSON (nested) | 是 | 嵌套对象 | SKU表 | - | - | 商品变体数组(嵌套结构) | | |
| 94 | + | |
| 95 | +#### Variants子字段 | |
| 96 | + | |
| 97 | +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | | |
| 98 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| | |
| 99 | +| variants.variant_id | keyword | 是 | 精确匹配 | SKU表 | id | BIGINT | 变体ID(SKU ID) | | |
| 100 | +| variants.title | text | 是 | chinese_ecommerce分析器 | SKU表 | title | VARCHAR(500) | 变体标题 | | |
| 101 | +| variants.price | float | 是 | 数值范围 | SKU表 | price | DECIMAL(10,2) | 变体价格 | | |
| 102 | +| variants.compare_at_price | float | 是 | 数值范围 | SKU表 | compare_at_price | DECIMAL(10,2) | 变体原价 | | |
| 103 | +| variants.sku | keyword | 是 | 精确匹配 | SKU表 | sku | VARCHAR(100) | SKU编码 | | |
| 104 | +| variants.stock | long | 是 | 数值范围 | SKU表 | inventory_quantity | INT(11) | 库存数量 | | |
| 105 | +| variants.options | object | 是 | 对象 | SKU表 | option1/option2/option3 | VARCHAR(255) | 选项(颜色、尺寸等) | | |
| 106 | + | |
| 107 | +**Variants结构说明**: | |
| 108 | +- `variants` 是一个嵌套对象数组,每个元素代表一个SKU | |
| 109 | +- 使用ES的nested类型,支持对嵌套字段进行独立查询和过滤 | |
| 110 | +- `options` 对象包含 `option1`、`option2`、`option3` 三个字段,分别对应SKU表中的选项值 | |
| 111 | + | |
| 112 | +## 字段类型说明 | |
| 113 | + | |
| 114 | +### ES字段类型映射 | |
| 115 | + | |
| 116 | +| ES字段类型 | Elasticsearch映射 | 用途 | | |
| 117 | +|-----------|------------------|------| | |
| 118 | +| KEYWORD | keyword | 精确匹配、过滤、聚合、排序 | | |
| 119 | +| TEXT | text | 全文检索(支持分词) | | |
| 120 | +| FLOAT | float | 浮点数(价格、权重等) | | |
| 121 | +| LONG | long | 整数(库存、计数等) | | |
| 122 | +| DATE | date | 日期时间 | | |
| 123 | +| TEXT_EMBEDDING | dense_vector | 文本向量(1024维) | | |
| 124 | +| JSON | object/nested | 嵌套对象 | | |
| 125 | + | |
| 126 | +### 分析器说明 | |
| 127 | + | |
| 128 | +| 分析器名称 | 语言 | 说明 | | |
| 129 | +|-----------|------|------| | |
| 130 | +| chinese_ecommerce | 中文 | Ansj中文分词器(电商优化),用于中文文本的分词和搜索 | | |
| 131 | + | |
| 132 | +## 索引配置 | |
| 133 | + | |
| 134 | +### 索引设置 | |
| 135 | + | |
| 136 | +- **分片数**: 1 | |
| 137 | +- **副本数**: 0 | |
| 138 | +- **刷新间隔**: 30秒 | |
| 139 | + | |
| 140 | +### 查询域(Query Domains) | |
| 141 | + | |
| 142 | +系统定义了多个查询域,用于在不同场景下搜索不同的字段组合: | |
| 143 | + | |
| 144 | +1. **default(默认索引)**: 搜索所有文本字段 | |
| 145 | + - 包含字段:title, brief, description, seo_title, seo_description, seo_keywords, vendor, product_type, tags, category | |
| 146 | + - Boost: 1.0 | |
| 147 | + | |
| 148 | +2. **title(标题索引)**: 仅搜索标题相关字段 | |
| 149 | + - 包含字段:title, seo_title | |
| 150 | + - Boost: 2.0 | |
| 151 | + | |
| 152 | +3. **vendor(品牌索引)**: 仅搜索品牌字段 | |
| 153 | + - 包含字段:vendor | |
| 154 | + - Boost: 1.5 | |
| 155 | + | |
| 156 | +4. **category(类目索引)**: 仅搜索类目字段 | |
| 157 | + - 包含字段:category | |
| 158 | + - Boost: 1.5 | |
| 159 | + | |
| 160 | +5. **tags(标签索引)**: 搜索标签和SEO关键词 | |
| 161 | + - 包含字段:tags, seo_keywords | |
| 162 | + - Boost: 1.0 | |
| 163 | + | |
| 164 | +## 数据转换规则 | |
| 165 | + | |
| 166 | +### 数据类型转换 | |
| 167 | + | |
| 168 | +1. **BIGINT → KEYWORD**: 数字ID转换为字符串(如 `product_id`, `variant_id`) | |
| 169 | +2. **DECIMAL → FLOAT**: 价格字段从DECIMAL转换为FLOAT | |
| 170 | +3. **INT → LONG**: 库存数量从INT转换为LONG | |
| 171 | +4. **DATETIME → DATE**: 时间字段转换为ISO格式字符串 | |
| 172 | + | |
| 173 | +### 特殊处理 | |
| 174 | + | |
| 175 | +1. **价格聚合**: 从多个SKU的价格中计算min_price、max_price、compare_at_price | |
| 176 | +2. **图片URL处理**: 如果image_src不是完整URL,会自动添加协议前缀 | |
| 177 | +3. **选项合并**: 将SKU表的option1、option2、option3合并为options对象 | |
| 178 | + | |
| 179 | +## 注意事项 | |
| 180 | + | |
| 181 | +1. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 | |
| 182 | +2. **嵌套查询**: 查询variants字段时需要使用nested查询语法 | |
| 183 | +3. **字段命名**: 用于过滤的字段应使用 `*_keyword` 后缀的字段 | |
| 184 | +4. **向量搜索**: title_embedding字段用于语义搜索,需要配合文本查询使用 | |
| 185 | +5. **Boost权重**: 不同字段的boost权重影响搜索结果的相关性排序 | |
| 186 | + | |
| 187 | +## 数据来源表结构 | |
| 188 | + | |
| 189 | +### SPU表(shoplazza_product_spu) | |
| 190 | + | |
| 191 | +主要字段: | |
| 192 | +- `id`: BIGINT - 主键ID | |
| 193 | +- `tenant_id`: BIGINT - 租户ID | |
| 194 | +- `handle`: VARCHAR(255) - URL handle | |
| 195 | +- `title`: VARCHAR(512) - 商品标题 | |
| 196 | +- `brief`: VARCHAR(512) - 商品简介 | |
| 197 | +- `description`: TEXT - 商品描述 | |
| 198 | +- `vendor`: VARCHAR(255) - 供应商/品牌 | |
| 199 | +- `category`: VARCHAR(255) - 类目 | |
| 200 | +- `tags`: VARCHAR(1024) - 标签 | |
| 201 | +- `seo_title`: VARCHAR(512) - SEO标题 | |
| 202 | +- `seo_description`: TEXT - SEO描述 | |
| 203 | +- `seo_keywords`: VARCHAR(1024) - SEO关键词 | |
| 204 | +- `image_src`: VARCHAR(500) - 图片URL | |
| 205 | +- `create_time`: DATETIME - 创建时间 | |
| 206 | +- `update_time`: DATETIME - 更新时间 | |
| 207 | +- `shoplazza_created_at`: DATETIME - 店匠创建时间 | |
| 208 | +- `shoplazza_updated_at`: DATETIME - 店匠更新时间 | |
| 209 | + | |
| 210 | +### SKU表(shoplazza_product_sku) | |
| 211 | + | |
| 212 | +主要字段: | |
| 213 | +- `id`: BIGINT - 主键ID(对应variant_id) | |
| 214 | +- `spu_id`: BIGINT - SPU ID(关联字段) | |
| 215 | +- `title`: VARCHAR(500) - 变体标题 | |
| 216 | +- `price`: DECIMAL(10,2) - 价格 | |
| 217 | +- `compare_at_price`: DECIMAL(10,2) - 原价 | |
| 218 | +- `sku`: VARCHAR(100) - SKU编码 | |
| 219 | +- `inventory_quantity`: INT(11) - 库存数量 | |
| 220 | +- `option1`: VARCHAR(255) - 选项1 | |
| 221 | +- `option2`: VARCHAR(255) - 选项2 | |
| 222 | +- `option3`: VARCHAR(255) - 选项3 | |
| 223 | + | ... | ... |
config/config.yaml
| ... | ... | @@ -169,7 +169,7 @@ fields: |
| 169 | 169 | return_in_source: true |
| 170 | 170 | |
| 171 | 171 | # 文本嵌入字段(用于语义搜索) |
| 172 | - - name: "name_embedding" | |
| 172 | + - name: "title_embedding" | |
| 173 | 173 | type: "TEXT_EMBEDDING" |
| 174 | 174 | embedding_dims: 1024 |
| 175 | 175 | embedding_similarity: "dot_product" |
| ... | ... | @@ -296,7 +296,7 @@ query_config: |
| 296 | 296 | enable_query_rewrite: true |
| 297 | 297 | |
| 298 | 298 | # Embedding field names (if not set, will auto-detect from fields) |
| 299 | - text_embedding_field: "name_embedding" # Field name for text embeddings | |
| 299 | + text_embedding_field: "title_embedding" # Field name for text embeddings | |
| 300 | 300 | image_embedding_field: null # Field name for image embeddings (if not set, will auto-detect) |
| 301 | 301 | |
| 302 | 302 | # Translation API (DeepL) | ... | ... |
config/config_loader.py
| ... | ... | @@ -55,7 +55,7 @@ class QueryConfig: |
| 55 | 55 | translation_context: str = "e-commerce product search" # Context hint for translation |
| 56 | 56 | |
| 57 | 57 | # Embedding field names - if not set, will auto-detect from fields |
| 58 | - text_embedding_field: Optional[str] = None # Field name for text embeddings (e.g., "name_embedding") | |
| 58 | + text_embedding_field: Optional[str] = None # Field name for text embeddings (e.g., "title_embedding") | |
| 59 | 59 | image_embedding_field: Optional[str] = None # Field name for image embeddings (e.g., "image_embedding") |
| 60 | 60 | |
| 61 | 61 | # ES source fields configuration - fields to return in search results | ... | ... |