Commit b73baf85cb520d7316d2514d24edfec5e8334b7a
1 parent
cd3799c6
撰写接口文档
Showing
6 changed files
with
1018 additions
and
110 deletions
Show diff stats
| @@ -0,0 +1,791 @@ | @@ -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
| @@ -218,7 +218,7 @@ from .rerank_engine import RerankEngine # 原 RankingEngine | @@ -218,7 +218,7 @@ from .rerank_engine import RerankEngine # 原 RankingEngine | ||
| 218 | }, | 218 | }, |
| 219 | { | 219 | { |
| 220 | "knn": { | 220 | "knn": { |
| 221 | - "field": "name_embedding", | 221 | + "field": "title_embedding", |
| 222 | "query_vector": [...], | 222 | "query_vector": [...], |
| 223 | "k": 50, | 223 | "k": 50, |
| 224 | "num_candidates": 200 | 224 | "num_candidates": 200 |
HighLevelDesign.md
| @@ -51,112 +51,6 @@ updater varchar(64) | @@ -51,112 +51,6 @@ updater varchar(64) | ||
| 51 | update_time datetime | 51 | update_time datetime |
| 52 | deleted bit(1) | 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 | ES_CONFIG = { | 54 | ES_CONFIG = { |
| 161 | 'host': 'http://localhost:9200', | 55 | 'host': 'http://localhost:9200', |
| 162 | 'username': 'essa', | 56 | 'username': 'essa', |
| @@ -0,0 +1,223 @@ | @@ -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,7 +169,7 @@ fields: | ||
| 169 | return_in_source: true | 169 | return_in_source: true |
| 170 | 170 | ||
| 171 | # 文本嵌入字段(用于语义搜索) | 171 | # 文本嵌入字段(用于语义搜索) |
| 172 | - - name: "name_embedding" | 172 | + - name: "title_embedding" |
| 173 | type: "TEXT_EMBEDDING" | 173 | type: "TEXT_EMBEDDING" |
| 174 | embedding_dims: 1024 | 174 | embedding_dims: 1024 |
| 175 | embedding_similarity: "dot_product" | 175 | embedding_similarity: "dot_product" |
| @@ -296,7 +296,7 @@ query_config: | @@ -296,7 +296,7 @@ query_config: | ||
| 296 | enable_query_rewrite: true | 296 | enable_query_rewrite: true |
| 297 | 297 | ||
| 298 | # Embedding field names (if not set, will auto-detect from fields) | 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 | image_embedding_field: null # Field name for image embeddings (if not set, will auto-detect) | 300 | image_embedding_field: null # Field name for image embeddings (if not set, will auto-detect) |
| 301 | 301 | ||
| 302 | # Translation API (DeepL) | 302 | # Translation API (DeepL) |
config/config_loader.py
| @@ -55,7 +55,7 @@ class QueryConfig: | @@ -55,7 +55,7 @@ class QueryConfig: | ||
| 55 | translation_context: str = "e-commerce product search" # Context hint for translation | 55 | translation_context: str = "e-commerce product search" # Context hint for translation |
| 56 | 56 | ||
| 57 | # Embedding field names - if not set, will auto-detect from fields | 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 | image_embedding_field: Optional[str] = None # Field name for image embeddings (e.g., "image_embedding") | 59 | image_embedding_field: Optional[str] = None # Field name for image embeddings (e.g., "image_embedding") |
| 60 | 60 | ||
| 61 | # ES source fields configuration - fields to return in search results | 61 | # ES source fields configuration - fields to return in search results |