Commit 93be98cb287bd879e5e7e6c495e3d505154d5053
1 parent
7a013ca7
清理过时的文档
Showing
16 changed files
with
174 additions
and
3119 deletions
Show diff stats
config/config.yaml
| ... | ... | @@ -278,7 +278,7 @@ services: |
| 278 | 278 | glossary_id: "" |
| 279 | 279 | use_cache: true |
| 280 | 280 | nllb-200-distilled-600m: |
| 281 | - enabled: false | |
| 281 | + enabled: true | |
| 282 | 282 | backend: "local_nllb" |
| 283 | 283 | model_id: "facebook/nllb-200-distilled-600M" |
| 284 | 284 | model_dir: "./models/translation/facebook/nllb-200-distilled-600M" | ... | ... |
context/request_context.py
| ... | ... | @@ -41,7 +41,6 @@ class QueryAnalysisResult: |
| 41 | 41 | translations: Dict[str, str] = field(default_factory=dict) |
| 42 | 42 | query_vector: Optional[List[float]] = None |
| 43 | 43 | boolean_ast: Optional[str] = None |
| 44 | - is_simple_query: bool = True | |
| 45 | 44 | |
| 46 | 45 | |
| 47 | 46 | @dataclass |
| ... | ... | @@ -280,7 +279,6 @@ class RequestContext: |
| 280 | 279 | 'query_normalized': self.query_analysis.query_normalized, |
| 281 | 280 | 'rewritten_query': self.query_analysis.rewritten_query, |
| 282 | 281 | 'detected_language': self.query_analysis.detected_language, |
| 283 | - 'is_simple_query': self.query_analysis.is_simple_query | |
| 284 | 282 | }, |
| 285 | 283 | 'performance': { |
| 286 | 284 | 'total_duration_ms': round(self.performance_metrics.total_duration, 2), | ... | ... |
docs/亚马逊到店匠格式转换分析.md renamed to data/mai_jia_jing_ling/亚马逊到店匠格式转换分析.md
docs/亚马逊格式数据转店匠商品导入模板.md renamed to data/mai_jia_jing_ling/亚马逊格式数据转店匠商品导入模板.md
docs/Search-API-Examples.md deleted
| ... | ... | @@ -1,1350 +0,0 @@ |
| 1 | -# API 使用示例 | |
| 2 | - | |
| 3 | -本文档提供了搜索引擎 API 的详细使用示例,包括各种常见场景和最佳实践。 | |
| 4 | - | |
| 5 | ---- | |
| 6 | - | |
| 7 | -## 目录 | |
| 8 | - | |
| 9 | -1. [基础搜索](#基础搜索) | |
| 10 | -2. [过滤器使用](#过滤器使用) | |
| 11 | -3. [分面搜索](#分面搜索) | |
| 12 | -4. [排序](#排序) | |
| 13 | -5. [图片搜索](#图片搜索) | |
| 14 | -6. [布尔表达式](#布尔表达式) | |
| 15 | -7. [完整示例](#完整示例) | |
| 16 | - | |
| 17 | ---- | |
| 18 | - | |
| 19 | -## 基础搜索 | |
| 20 | - | |
| 21 | -### 示例 1:最简单的搜索 | |
| 22 | - | |
| 23 | -```bash | |
| 24 | -curl -X POST "http://localhost:6002/search/" \ | |
| 25 | - -H "Content-Type: application/json" \ | |
| 26 | - -H "X-Tenant-ID: 162" \ | |
| 27 | - -d '{ | |
| 28 | - "query": "芭比娃娃" | |
| 29 | - }' | |
| 30 | -``` | |
| 31 | - | |
| 32 | -**响应**: | |
| 33 | -```json | |
| 34 | -{ | |
| 35 | - "hits": [...], | |
| 36 | - "total": 118, | |
| 37 | - "max_score": 8.5, | |
| 38 | - "took_ms": 45, | |
| 39 | - "query_info": { | |
| 40 | - "original_query": "芭比娃娃", | |
| 41 | - "detected_language": "zh", | |
| 42 | - "translations": {"en": "barbie doll"} | |
| 43 | - } | |
| 44 | -} | |
| 45 | -``` | |
| 46 | - | |
| 47 | -### 示例 2:指定返回数量 | |
| 48 | - | |
| 49 | -```bash | |
| 50 | -curl -X POST "http://localhost:6002/search/" \ | |
| 51 | - -H "Content-Type: application/json" \ | |
| 52 | - -H "X-Tenant-ID: 162" \ | |
| 53 | - -d '{ | |
| 54 | - "query": "手机", | |
| 55 | - "language": "zh", | |
| 56 | - "size": 50 | |
| 57 | - }' | |
| 58 | -``` | |
| 59 | - | |
| 60 | -### 示例 3:分页查询 | |
| 61 | - | |
| 62 | -```bash | |
| 63 | -# 第1页(0-19) | |
| 64 | -curl -X POST "http://localhost:6002/search/" \ | |
| 65 | - -H "Content-Type: application/json" \ | |
| 66 | - -H "X-Tenant-ID: 162" \ | |
| 67 | - -d '{ | |
| 68 | - "query": "手机", | |
| 69 | - "language": "zh", | |
| 70 | - "size": 20, | |
| 71 | - "from": 0 | |
| 72 | - }' | |
| 73 | - | |
| 74 | -# 第2页(20-39) | |
| 75 | -curl -X POST "http://localhost:6002/search/" \ | |
| 76 | - -H "Content-Type: application/json" \ | |
| 77 | - -H "X-Tenant-ID: 162" \ | |
| 78 | - -d '{ | |
| 79 | - "query": "手机", | |
| 80 | - "language": "zh", | |
| 81 | - "size": 20, | |
| 82 | - "from": 20 | |
| 83 | - }' | |
| 84 | -``` | |
| 85 | - | |
| 86 | ---- | |
| 87 | - | |
| 88 | -## 过滤器使用 | |
| 89 | - | |
| 90 | -### 精确匹配过滤器 | |
| 91 | - | |
| 92 | -#### 示例 1:单值过滤 | |
| 93 | - | |
| 94 | -```bash | |
| 95 | -curl -X POST "http://localhost:6002/search/" \ | |
| 96 | - -H "Content-Type: application/json" \ | |
| 97 | - -H "X-Tenant-ID: 162" \ | |
| 98 | - -d '{ | |
| 99 | - "query": "手机", | |
| 100 | - "language": "zh", | |
| 101 | - "filters": { | |
| 102 | - "category_name": "手机" | |
| 103 | - } | |
| 104 | - }' | |
| 105 | -``` | |
| 106 | - | |
| 107 | -#### 示例 2:多值过滤(OR 逻辑) | |
| 108 | - | |
| 109 | -```bash | |
| 110 | -curl -X POST "http://localhost:6002/search/" \ | |
| 111 | - -H "Content-Type: application/json" \ | |
| 112 | - -H "X-Tenant-ID: 162" \ | |
| 113 | - -d '{ | |
| 114 | - "query": "手机", | |
| 115 | - "language": "zh", | |
| 116 | - "filters": { | |
| 117 | - "category_name": ["手机", "电子产品"] | |
| 118 | - } | |
| 119 | - }' | |
| 120 | -``` | |
| 121 | - | |
| 122 | -说明:匹配类目为"玩具" **或** "益智玩具" **或** "儿童玩具"的商品。 | |
| 123 | - | |
| 124 | -#### 示例 3:多字段过滤(AND 逻辑) | |
| 125 | - | |
| 126 | -```bash | |
| 127 | -curl -X POST "http://localhost:6002/search/" \ | |
| 128 | - -H "Content-Type: application/json" \ | |
| 129 | - -H "X-Tenant-ID: 162" \ | |
| 130 | - -d '{ | |
| 131 | - "query": "手机", | |
| 132 | - "language": "zh", | |
| 133 | - "filters": { | |
| 134 | - "category_name": "手机", | |
| 135 | - "vendor.zh.keyword": "奇乐" | |
| 136 | - } | |
| 137 | - }' | |
| 138 | -``` | |
| 139 | - | |
| 140 | -说明:必须同时满足"类目=手机" **并且** "品牌=奇乐"。 | |
| 141 | - | |
| 142 | -#### 示例 4:Specifications 嵌套过滤(单个规格) | |
| 143 | - | |
| 144 | -```bash | |
| 145 | -curl -X POST "http://localhost:6002/search/" \ | |
| 146 | - -H "Content-Type: application/json" \ | |
| 147 | - -H "X-Tenant-ID: 162" \ | |
| 148 | - -d '{ | |
| 149 | - "query": "手机", | |
| 150 | - "language": "zh", | |
| 151 | - "filters": { | |
| 152 | - "specifications": { | |
| 153 | - "name": "color", | |
| 154 | - "value": "white" | |
| 155 | - } | |
| 156 | - } | |
| 157 | - }' | |
| 158 | -``` | |
| 159 | - | |
| 160 | -说明:查询规格名称为"color"且值为"white"的商品。 | |
| 161 | - | |
| 162 | -#### 示例 5:Specifications 嵌套过滤(多个规格,OR逻辑) | |
| 163 | - | |
| 164 | -```bash | |
| 165 | -curl -X POST "http://localhost:6002/search/" \ | |
| 166 | - -H "Content-Type: application/json" \ | |
| 167 | - -H "X-Tenant-ID: 162" \ | |
| 168 | - -d '{ | |
| 169 | - "query": "手机", | |
| 170 | - "language": "zh", | |
| 171 | - "filters": { | |
| 172 | - "specifications": [ | |
| 173 | - {"name": "color", "value": "white"}, | |
| 174 | - {"name": "size", "value": "256GB"} | |
| 175 | - ] | |
| 176 | - } | |
| 177 | - }' | |
| 178 | -``` | |
| 179 | - | |
| 180 | -说明:查询满足任意一个规格的商品(color=white **或** size=256GB)。 | |
| 181 | - | |
| 182 | -#### 示例 6:组合过滤(包含 specifications) | |
| 183 | - | |
| 184 | -```bash | |
| 185 | -curl -X POST "http://localhost:6002/search/" \ | |
| 186 | - -H "Content-Type: application/json" \ | |
| 187 | - -H "X-Tenant-ID: 162" \ | |
| 188 | - -d '{ | |
| 189 | - "query": "手机", | |
| 190 | - "language": "zh", | |
| 191 | - "filters": { | |
| 192 | - "category_name": "手机", | |
| 193 | - "specifications": { | |
| 194 | - "name": "color", | |
| 195 | - "value": "white" | |
| 196 | - } | |
| 197 | - } | |
| 198 | - }' | |
| 199 | -``` | |
| 200 | - | |
| 201 | -说明:同时满足类目=手机 **并且** color=white。 | |
| 202 | - | |
| 203 | -### 范围过滤器 | |
| 204 | - | |
| 205 | -#### 示例 1:价格范围 | |
| 206 | - | |
| 207 | -```bash | |
| 208 | -curl -X POST "http://localhost:6002/search/" \ | |
| 209 | - -H "Content-Type: application/json" \ | |
| 210 | - -H "X-Tenant-ID: 162" \ | |
| 211 | - -d '{ | |
| 212 | - "query": "手机", | |
| 213 | - "language": "zh", | |
| 214 | - "range_filters": { | |
| 215 | - "min_price": { | |
| 216 | - "gte": 50, | |
| 217 | - "lte": 200 | |
| 218 | - } | |
| 219 | - } | |
| 220 | - }' | |
| 221 | -``` | |
| 222 | - | |
| 223 | -说明:价格在 50-200 元之间(包含边界)。 | |
| 224 | - | |
| 225 | -#### 示例 2:只设置下限 | |
| 226 | - | |
| 227 | -```bash | |
| 228 | -curl -X POST "http://localhost:6002/search/" \ | |
| 229 | - -H "Content-Type: application/json" \ | |
| 230 | - -H "X-Tenant-ID: 162" \ | |
| 231 | - -d '{ | |
| 232 | - "query": "手机", | |
| 233 | - "language": "zh", | |
| 234 | - "range_filters": { | |
| 235 | - "min_price": { | |
| 236 | - "gte": 100 | |
| 237 | - } | |
| 238 | - } | |
| 239 | - }' | |
| 240 | -``` | |
| 241 | - | |
| 242 | -说明:价格 ≥ 100 元。 | |
| 243 | - | |
| 244 | -#### 示例 3:只设置上限 | |
| 245 | - | |
| 246 | -```bash | |
| 247 | -curl -X POST "http://localhost:6002/search/" \ | |
| 248 | - -H "Content-Type: application/json" \ | |
| 249 | - -H "X-Tenant-ID: 162" \ | |
| 250 | - -d '{ | |
| 251 | - "query": "手机", | |
| 252 | - "language": "zh", | |
| 253 | - "range_filters": { | |
| 254 | - "min_price": { | |
| 255 | - "lt": 50 | |
| 256 | - } | |
| 257 | - } | |
| 258 | - }' | |
| 259 | -``` | |
| 260 | - | |
| 261 | -说明:价格 < 50 元(不包含50)。 | |
| 262 | - | |
| 263 | -#### 示例 4:多字段范围过滤 | |
| 264 | - | |
| 265 | -```bash | |
| 266 | -curl -X POST "http://localhost:6002/search/" \ | |
| 267 | - -H "Content-Type: application/json" \ | |
| 268 | - -H "X-Tenant-ID: 162" \ | |
| 269 | - -d '{ | |
| 270 | - "query": "手机", | |
| 271 | - "language": "zh", | |
| 272 | - "range_filters": { | |
| 273 | - "min_price": { | |
| 274 | - "gte": 50, | |
| 275 | - "lte": 200 | |
| 276 | - }, | |
| 277 | - "days_since_last_update": { | |
| 278 | - "lte": 30 | |
| 279 | - } | |
| 280 | - } | |
| 281 | - }' | |
| 282 | -``` | |
| 283 | - | |
| 284 | -说明:价格在 50-200 元 **并且** 最近30天内更新过。 | |
| 285 | - | |
| 286 | -### 组合过滤器 | |
| 287 | - | |
| 288 | -```bash | |
| 289 | -curl -X POST "http://localhost:6002/search/" \ | |
| 290 | - -H "Content-Type: application/json" \ | |
| 291 | - -H "X-Tenant-ID: 162" \ | |
| 292 | - -d '{ | |
| 293 | - "query": "手机", | |
| 294 | - "language": "zh", | |
| 295 | - "filters": { | |
| 296 | - "category_name": ["手机", "电子产品"], | |
| 297 | - "vendor.zh.keyword": "品牌A" | |
| 298 | - }, | |
| 299 | - "range_filters": { | |
| 300 | - "min_price": { | |
| 301 | - "gte": 50, | |
| 302 | - "lte": 500 | |
| 303 | - } | |
| 304 | - } | |
| 305 | - }' | |
| 306 | -``` | |
| 307 | - | |
| 308 | -说明:类目是"玩具"或"益智玩具" **并且** 品牌是"乐高" **并且** 价格在 50-500 元之间。 | |
| 309 | - | |
| 310 | ---- | |
| 311 | - | |
| 312 | -## 分面搜索 | |
| 313 | - | |
| 314 | -### 简单模式 | |
| 315 | - | |
| 316 | -#### 示例 1:基础分面 | |
| 317 | - | |
| 318 | -```bash | |
| 319 | -curl -X POST "http://localhost:6002/search/" \ | |
| 320 | - -H "Content-Type: application/json" \ | |
| 321 | - -H "X-Tenant-ID: 162" \ | |
| 322 | - -d '{ | |
| 323 | - "query": "手机", | |
| 324 | - "language": "zh", | |
| 325 | - "size": 20, | |
| 326 | - "facets": ["category1_name", "category2_name", "specifications"] | |
| 327 | - }' | |
| 328 | -``` | |
| 329 | - | |
| 330 | -**响应**: | |
| 331 | -```json | |
| 332 | -{ | |
| 333 | - "results": [...], | |
| 334 | - "total": 118, | |
| 335 | - "facets": [ | |
| 336 | - { | |
| 337 | - "field": "category1_name", | |
| 338 | - "label": "category1_name", | |
| 339 | - "type": "terms", | |
| 340 | - "values": [ | |
| 341 | - {"value": "手机", "count": 85, "selected": false}, | |
| 342 | - {"value": "电子产品", "count": 33, "selected": false} | |
| 343 | - ] | |
| 344 | - }, | |
| 345 | - { | |
| 346 | - "field": "specifications.color", | |
| 347 | - "label": "color", | |
| 348 | - "type": "terms", | |
| 349 | - "values": [ | |
| 350 | - {"value": "white", "count": 50, "selected": false}, | |
| 351 | - {"value": "black", "count": 30, "selected": false} | |
| 352 | - ] | |
| 353 | - }, | |
| 354 | - { | |
| 355 | - "field": "specifications.size", | |
| 356 | - "label": "size", | |
| 357 | - "type": "terms", | |
| 358 | - "values": [ | |
| 359 | - {"value": "256GB", "count": 40, "selected": false}, | |
| 360 | - {"value": "512GB", "count": 20, "selected": false} | |
| 361 | - ] | |
| 362 | - } | |
| 363 | - ] | |
| 364 | -} | |
| 365 | -``` | |
| 366 | - | |
| 367 | -#### 示例 2:Specifications 分面(所有规格名称) | |
| 368 | - | |
| 369 | -```bash | |
| 370 | -curl -X POST "http://localhost:6002/search/" \ | |
| 371 | - -H "Content-Type: application/json" \ | |
| 372 | - -H "X-Tenant-ID: 162" \ | |
| 373 | - -d '{ | |
| 374 | - "query": "手机", | |
| 375 | - "language": "zh", | |
| 376 | - "facets": ["specifications"] | |
| 377 | - }' | |
| 378 | -``` | |
| 379 | - | |
| 380 | -说明:返回所有规格名称(name)及其对应的值(value)列表。 | |
| 381 | - | |
| 382 | -#### 示例 3:Specifications 分面(指定规格名称) | |
| 383 | - | |
| 384 | -```bash | |
| 385 | -curl -X POST "http://localhost:6002/search/" \ | |
| 386 | - -H "Content-Type: application/json" \ | |
| 387 | - -H "X-Tenant-ID: 162" \ | |
| 388 | - -d '{ | |
| 389 | - "query": "手机", | |
| 390 | - "language": "zh", | |
| 391 | - "facets": ["specifications.color", "specifications.size"] | |
| 392 | - }' | |
| 393 | -``` | |
| 394 | - | |
| 395 | -说明:只返回指定规格名称的值列表。 | |
| 396 | - | |
| 397 | -### 高级模式 | |
| 398 | - | |
| 399 | -#### 示例 1:自定义分面大小 | |
| 400 | - | |
| 401 | -```bash | |
| 402 | -curl -X POST "http://localhost:6002/search/" \ | |
| 403 | - -H "Content-Type: application/json" \ | |
| 404 | - -H "X-Tenant-ID: 162" \ | |
| 405 | - -d '{ | |
| 406 | - "query": "手机", | |
| 407 | - "language": "zh", | |
| 408 | - "facets": [ | |
| 409 | - { | |
| 410 | - "field": "category1_name", | |
| 411 | - "size": 20, | |
| 412 | - "type": "terms" | |
| 413 | - }, | |
| 414 | - { | |
| 415 | - "field": "category2_name", | |
| 416 | - "size": 30, | |
| 417 | - "type": "terms" | |
| 418 | - } | |
| 419 | - ] | |
| 420 | - }' | |
| 421 | -``` | |
| 422 | - | |
| 423 | -#### 示例 2:范围分面 | |
| 424 | - | |
| 425 | -```bash | |
| 426 | -curl -X POST "http://localhost:6002/search/" \ | |
| 427 | - -H "Content-Type: application/json" \ | |
| 428 | - -H "X-Tenant-ID: 162" \ | |
| 429 | - -d '{ | |
| 430 | - "query": "手机", | |
| 431 | - "language": "zh", | |
| 432 | - "facets": [ | |
| 433 | - { | |
| 434 | - "field": "price", | |
| 435 | - "type": "range", | |
| 436 | - "ranges": [ | |
| 437 | - {"key": "0-50", "to": 50}, | |
| 438 | - {"key": "50-100", "from": 50, "to": 100}, | |
| 439 | - {"key": "100-200", "from": 100, "to": 200}, | |
| 440 | - {"key": "200+", "from": 200} | |
| 441 | - ] | |
| 442 | - } | |
| 443 | - ] | |
| 444 | - }' | |
| 445 | -``` | |
| 446 | - | |
| 447 | -**响应**: | |
| 448 | -```json | |
| 449 | -{ | |
| 450 | - "facets": [ | |
| 451 | - { | |
| 452 | - "field": "price", | |
| 453 | - "label": "price", | |
| 454 | - "type": "range", | |
| 455 | - "values": [ | |
| 456 | - {"value": "0-50", "count": 23, "selected": false}, | |
| 457 | - {"value": "50-100", "count": 45, "selected": false}, | |
| 458 | - {"value": "100-200", "count": 38, "selected": false}, | |
| 459 | - {"value": "200+", "count": 12, "selected": false} | |
| 460 | - ] | |
| 461 | - } | |
| 462 | - ] | |
| 463 | -} | |
| 464 | -``` | |
| 465 | - | |
| 466 | -#### 示例 3:混合分面(Terms + Range) | |
| 467 | - | |
| 468 | -```bash | |
| 469 | -curl -X POST "http://localhost:6002/search/" \ | |
| 470 | - -H "Content-Type: application/json" \ | |
| 471 | - -H "X-Tenant-ID: 162" \ | |
| 472 | - -d '{ | |
| 473 | - "query": "手机", | |
| 474 | - "language": "zh", | |
| 475 | - "facets": [ | |
| 476 | - {"field": "category.keyword", "size": 15}, | |
| 477 | - {"field": "vendor.keyword", "size": 15}, | |
| 478 | - { | |
| 479 | - "field": "price", | |
| 480 | - "type": "range", | |
| 481 | - "ranges": [ | |
| 482 | - {"key": "低价", "to": 50}, | |
| 483 | - {"key": "中价", "from": 50, "to": 200}, | |
| 484 | - {"key": "高价", "from": 200} | |
| 485 | - ] | |
| 486 | - } | |
| 487 | - ] | |
| 488 | - }' | |
| 489 | -``` | |
| 490 | - | |
| 491 | ---- | |
| 492 | - | |
| 493 | -## 排序 | |
| 494 | - | |
| 495 | -### 示例 1:按价格排序(升序) | |
| 496 | - | |
| 497 | -```bash | |
| 498 | -curl -X POST "http://localhost:6002/search/" \ | |
| 499 | - -H "Content-Type: application/json" \ | |
| 500 | - -H "X-Tenant-ID: 162" \ | |
| 501 | - -d '{ | |
| 502 | - "query": "手机", | |
| 503 | - "language": "zh", | |
| 504 | - "size": 20, | |
| 505 | - "sort_by": "min_price", | |
| 506 | - "sort_order": "asc" | |
| 507 | - }' | |
| 508 | -``` | |
| 509 | - | |
| 510 | -### 示例 2:按创建时间排序(降序) | |
| 511 | - | |
| 512 | -```bash | |
| 513 | -curl -X POST "http://localhost:6002/search/" \ | |
| 514 | - -H "Content-Type: application/json" \ | |
| 515 | - -H "X-Tenant-ID: 162" \ | |
| 516 | - -d '{ | |
| 517 | - "query": "手机", | |
| 518 | - "language": "zh", | |
| 519 | - "size": 20, | |
| 520 | - "sort_by": "create_time", | |
| 521 | - "sort_order": "desc" | |
| 522 | - }' | |
| 523 | -``` | |
| 524 | - | |
| 525 | -### 示例 3:排序+过滤 | |
| 526 | - | |
| 527 | -```bash | |
| 528 | -curl -X POST "http://localhost:6002/search/" \ | |
| 529 | - -H "Content-Type: application/json" \ | |
| 530 | - -H "X-Tenant-ID: 162" \ | |
| 531 | - -d '{ | |
| 532 | - "query": "手机", | |
| 533 | - "language": "zh", | |
| 534 | - "filters": { | |
| 535 | - "category.keyword": "益智玩具" | |
| 536 | - }, | |
| 537 | - "sort_by": "min_price", | |
| 538 | - "sort_order": "asc" | |
| 539 | - }' | |
| 540 | -``` | |
| 541 | - | |
| 542 | ---- | |
| 543 | - | |
| 544 | -## 图片搜索 | |
| 545 | - | |
| 546 | -### 示例 1:基础图片搜索 | |
| 547 | - | |
| 548 | -```bash | |
| 549 | -curl -X POST "http://localhost:6002/search/image" \ | |
| 550 | - -H "Content-Type: application/json" \ | |
| 551 | - -H "X-Tenant-ID: 162" \ | |
| 552 | - -d '{ | |
| 553 | - "image_url": "https://example.com/barbie.jpg", | |
| 554 | - "size": 20 | |
| 555 | - }' | |
| 556 | -``` | |
| 557 | - | |
| 558 | -### 示例 2:图片搜索+过滤器 | |
| 559 | - | |
| 560 | -```bash | |
| 561 | -curl -X POST "http://localhost:6002/search/image" \ | |
| 562 | - -H "Content-Type: application/json" \ | |
| 563 | - -H "X-Tenant-ID: 162" \ | |
| 564 | - -d '{ | |
| 565 | - "image_url": "https://example.com/barbie.jpg", | |
| 566 | - "size": 20, | |
| 567 | - "filters": { | |
| 568 | - "category_name": "手机" | |
| 569 | - }, | |
| 570 | - "range_filters": { | |
| 571 | - "min_price": { | |
| 572 | - "lte": 100 | |
| 573 | - } | |
| 574 | - } | |
| 575 | - }' | |
| 576 | -``` | |
| 577 | - | |
| 578 | ---- | |
| 579 | - | |
| 580 | -## 布尔表达式 | |
| 581 | - | |
| 582 | -### 示例 1:AND 查询 | |
| 583 | - | |
| 584 | -```bash | |
| 585 | -curl -X POST "http://localhost:6002/search/" \ | |
| 586 | - -H "Content-Type: application/json" \ | |
| 587 | - -H "X-Tenant-ID: 162" \ | |
| 588 | - -d '{ | |
| 589 | - "query": "玩具 AND 乐高" | |
| 590 | - }' | |
| 591 | -``` | |
| 592 | - | |
| 593 | -说明:必须同时包含"玩具"和"乐高"。 | |
| 594 | - | |
| 595 | -### 示例 2:OR 查询 | |
| 596 | - | |
| 597 | -```bash | |
| 598 | -curl -X POST "http://localhost:6002/search/" \ | |
| 599 | - -H "Content-Type: application/json" \ | |
| 600 | - -H "X-Tenant-ID: 162" \ | |
| 601 | - -d '{ | |
| 602 | - "query": "芭比 OR 娃娃" | |
| 603 | - }' | |
| 604 | -``` | |
| 605 | - | |
| 606 | -说明:包含"芭比"或"娃娃"即可。 | |
| 607 | - | |
| 608 | -### 示例 3:ANDNOT 查询(排除) | |
| 609 | - | |
| 610 | -```bash | |
| 611 | -curl -X POST "http://localhost:6002/search/" \ | |
| 612 | - -H "Content-Type: application/json" \ | |
| 613 | - -H "X-Tenant-ID: 162" \ | |
| 614 | - -d '{ | |
| 615 | - "query": "玩具 ANDNOT 电动" | |
| 616 | - }' | |
| 617 | -``` | |
| 618 | - | |
| 619 | -说明:包含"玩具"但不包含"电动"。 | |
| 620 | - | |
| 621 | -### 示例 4:复杂布尔表达式 | |
| 622 | - | |
| 623 | -```bash | |
| 624 | -curl -X POST "http://localhost:6002/search/" \ | |
| 625 | - -H "Content-Type: application/json" \ | |
| 626 | - -H "X-Tenant-ID: 162" \ | |
| 627 | - -d '{ | |
| 628 | - "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" | |
| 629 | - }' | |
| 630 | -``` | |
| 631 | - | |
| 632 | -说明:必须包含"玩具",并且包含"乐高"或"芭比",但不包含"电动"。 | |
| 633 | - | |
| 634 | -### 示例 5:域查询 | |
| 635 | - | |
| 636 | -```bash | |
| 637 | -curl -X POST "http://localhost:6002/search/" \ | |
| 638 | - -H "Content-Type: application/json" \ | |
| 639 | - -H "X-Tenant-ID: 162" \ | |
| 640 | - -d '{ | |
| 641 | - "query": "brand:乐高" | |
| 642 | - }' | |
| 643 | -``` | |
| 644 | - | |
| 645 | -说明:在品牌域中搜索"乐高"。 | |
| 646 | - | |
| 647 | ---- | |
| 648 | - | |
| 649 | -## 完整示例 | |
| 650 | - | |
| 651 | -### Python 完整示例 | |
| 652 | - | |
| 653 | -```python | |
| 654 | -#!/usr/bin/env python3 | |
| 655 | -import requests | |
| 656 | -import json | |
| 657 | - | |
| 658 | -API_URL = "http://localhost:6002/search/" | |
| 659 | - | |
| 660 | -def search_products( | |
| 661 | - query, | |
| 662 | - size=20, | |
| 663 | - from_=0, | |
| 664 | - filters=None, | |
| 665 | - range_filters=None, | |
| 666 | - facets=None, | |
| 667 | - sort_by=None, | |
| 668 | - sort_order="desc", | |
| 669 | - debug=False | |
| 670 | -): | |
| 671 | - """执行搜索查询""" | |
| 672 | - payload = { | |
| 673 | - "query": query, | |
| 674 | - "size": size, | |
| 675 | - "from": from_ | |
| 676 | - } | |
| 677 | - | |
| 678 | - if filters: | |
| 679 | - payload["filters"] = filters | |
| 680 | - if range_filters: | |
| 681 | - payload["range_filters"] = range_filters | |
| 682 | - if facets: | |
| 683 | - payload["facets"] = facets | |
| 684 | - if sort_by: | |
| 685 | - payload["sort_by"] = sort_by | |
| 686 | - payload["sort_order"] = sort_order | |
| 687 | - if debug: | |
| 688 | - payload["debug"] = debug | |
| 689 | - | |
| 690 | - response = requests.post(API_URL, json=payload) | |
| 691 | - response.raise_for_status() | |
| 692 | - return response.json() | |
| 693 | - | |
| 694 | - | |
| 695 | -# 示例 1:简单搜索 | |
| 696 | -result = search_products("芭比娃娃", size=10) | |
| 697 | -print(f"找到 {result['total']} 个结果") | |
| 698 | -for hit in result['hits'][:3]: | |
| 699 | - product = hit['_source'] | |
| 700 | - print(f" - {product['name']}: ¥{product.get('price', 'N/A')}") | |
| 701 | - | |
| 702 | -# 示例 2:带过滤和分面的搜索 | |
| 703 | -result = search_products( | |
| 704 | - query="手机", | |
| 705 | - size=20, | |
| 706 | - language="zh", | |
| 707 | - filters={ | |
| 708 | - "category_name": "手机", | |
| 709 | - "specifications": {"name": "color", "value": "white"} | |
| 710 | - }, | |
| 711 | - range_filters={ | |
| 712 | - "min_price": {"gte": 50, "lte": 200} | |
| 713 | - }, | |
| 714 | - facets=[ | |
| 715 | - {"field": "category1_name", "size": 15}, | |
| 716 | - {"field": "category2_name", "size": 15}, | |
| 717 | - "specifications.color", | |
| 718 | - "specifications.size", | |
| 719 | - { | |
| 720 | - "field": "min_price", | |
| 721 | - "type": "range", | |
| 722 | - "ranges": [ | |
| 723 | - {"key": "0-50", "to": 50}, | |
| 724 | - {"key": "50-100", "from": 50, "to": 100}, | |
| 725 | - {"key": "100-200", "from": 100, "to": 200}, | |
| 726 | - {"key": "200+", "from": 200} | |
| 727 | - ] | |
| 728 | - } | |
| 729 | - ], | |
| 730 | - sort_by="min_price", | |
| 731 | - sort_order="asc" | |
| 732 | -) | |
| 733 | - | |
| 734 | -# 显示分面结果 | |
| 735 | -print(f"\n分面统计:") | |
| 736 | -for facet in result.get('facets', []): | |
| 737 | - print(f"\n{facet['label']} ({facet['type']}):") | |
| 738 | - for value in facet['values'][:5]: | |
| 739 | - selected_mark = "✓" if value['selected'] else " " | |
| 740 | - print(f" [{selected_mark}] {value['label']}: {value['count']}") | |
| 741 | - | |
| 742 | -# 示例 3:分页查询 | |
| 743 | -page = 1 | |
| 744 | -page_size = 20 | |
| 745 | -total_pages = 5 | |
| 746 | - | |
| 747 | -for page in range(1, total_pages + 1): | |
| 748 | - result = search_products( | |
| 749 | - query="玩具", | |
| 750 | - size=page_size, | |
| 751 | - from_=(page - 1) * page_size | |
| 752 | - ) | |
| 753 | - print(f"\n第 {page} 页:") | |
| 754 | - for hit in result['hits']: | |
| 755 | - product = hit['_source'] | |
| 756 | - print(f" - {product['name']}") | |
| 757 | -``` | |
| 758 | - | |
| 759 | -### JavaScript 完整示例 | |
| 760 | - | |
| 761 | -```javascript | |
| 762 | -// 搜索引擎客户端 | |
| 763 | -class SearchClient { | |
| 764 | - constructor(baseUrl) { | |
| 765 | - this.baseUrl = baseUrl; | |
| 766 | - } | |
| 767 | - | |
| 768 | - async search({ | |
| 769 | - query, | |
| 770 | - size = 20, | |
| 771 | - from = 0, | |
| 772 | - filters = null, | |
| 773 | - rangeFilters = null, | |
| 774 | - facets = null, | |
| 775 | - sortBy = null, | |
| 776 | - sortOrder = 'desc', | |
| 777 | - debug = false | |
| 778 | - }) { | |
| 779 | - const payload = { | |
| 780 | - query, | |
| 781 | - size, | |
| 782 | - from | |
| 783 | - }; | |
| 784 | - | |
| 785 | - if (filters) payload.filters = filters; | |
| 786 | - if (rangeFilters) payload.range_filters = rangeFilters; | |
| 787 | - if (facets) payload.facets = facets; | |
| 788 | - if (sortBy) { | |
| 789 | - payload.sort_by = sortBy; | |
| 790 | - payload.sort_order = sortOrder; | |
| 791 | - } | |
| 792 | - if (debug) payload.debug = debug; | |
| 793 | - | |
| 794 | - const response = await fetch(`${this.baseUrl}/search/`, { | |
| 795 | - method: 'POST', | |
| 796 | - headers: { | |
| 797 | - 'Content-Type': 'application/json' | |
| 798 | - }, | |
| 799 | - body: JSON.stringify(payload) | |
| 800 | - }); | |
| 801 | - | |
| 802 | - if (!response.ok) { | |
| 803 | - throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
| 804 | - } | |
| 805 | - | |
| 806 | - return await response.json(); | |
| 807 | - } | |
| 808 | - | |
| 809 | - async searchByImage(imageUrl, options = {}) { | |
| 810 | - const payload = { | |
| 811 | - image_url: imageUrl, | |
| 812 | - size: options.size || 20, | |
| 813 | - filters: options.filters || null, | |
| 814 | - range_filters: options.rangeFilters || null | |
| 815 | - }; | |
| 816 | - | |
| 817 | - const response = await fetch(`${this.baseUrl}/search/image`, { | |
| 818 | - method: 'POST', | |
| 819 | - headers: { | |
| 820 | - 'Content-Type': 'application/json' | |
| 821 | - }, | |
| 822 | - body: JSON.stringify(payload) | |
| 823 | - }); | |
| 824 | - | |
| 825 | - if (!response.ok) { | |
| 826 | - throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
| 827 | - } | |
| 828 | - | |
| 829 | - return await response.json(); | |
| 830 | - } | |
| 831 | -} | |
| 832 | - | |
| 833 | -// 使用示例 | |
| 834 | -const client = new SearchClient('http://localhost:6002'); | |
| 835 | - | |
| 836 | -// 简单搜索 | |
| 837 | -const result1 = await client.search({ | |
| 838 | - query: "芭比娃娃", | |
| 839 | - size: 20 | |
| 840 | -}); | |
| 841 | -console.log(`找到 ${result1.total} 个结果`); | |
| 842 | - | |
| 843 | -// 带过滤和分面的搜索(包含规格) | |
| 844 | -const result2 = await client.search({ | |
| 845 | - query: "手机", | |
| 846 | - language: "zh", | |
| 847 | - size: 20, | |
| 848 | - filters: { | |
| 849 | - category_name: "手机", | |
| 850 | - specifications: { name: "color", value: "white" } | |
| 851 | - }, | |
| 852 | - rangeFilters: { | |
| 853 | - min_price: { gte: 50, lte: 200 } | |
| 854 | - }, | |
| 855 | - facets: [ | |
| 856 | - "category1_name", | |
| 857 | - "specifications.color", | |
| 858 | - "specifications.size" | |
| 859 | - ], | |
| 860 | - sortBy: "min_price", | |
| 861 | - sortOrder: "asc" | |
| 862 | -}); | |
| 863 | - | |
| 864 | -// 显示分面结果 | |
| 865 | -result2.facets.forEach(facet => { | |
| 866 | - console.log(`\n${facet.label}:`); | |
| 867 | - facet.values.forEach(value => { | |
| 868 | - const selected = value.selected ? '✓' : ' '; | |
| 869 | - console.log(` [${selected}] ${value.label}: ${value.count}`); | |
| 870 | - }); | |
| 871 | -}); | |
| 872 | - | |
| 873 | -// 显示商品 | |
| 874 | -result2.hits.forEach(hit => { | |
| 875 | - const product = hit._source; | |
| 876 | - console.log(`${product.name} - ¥${product.price}`); | |
| 877 | -}); | |
| 878 | -``` | |
| 879 | - | |
| 880 | -### 前端完整示例(Vue.js 风格) | |
| 881 | - | |
| 882 | -```javascript | |
| 883 | -// 搜索组件 | |
| 884 | -const SearchComponent = { | |
| 885 | - data() { | |
| 886 | - return { | |
| 887 | - query: '', | |
| 888 | - results: [], | |
| 889 | - facets: [], | |
| 890 | - filters: {}, | |
| 891 | - rangeFilters: {}, | |
| 892 | - total: 0, | |
| 893 | - currentPage: 1, | |
| 894 | - pageSize: 20 | |
| 895 | - }; | |
| 896 | - }, | |
| 897 | - methods: { | |
| 898 | - async search() { | |
| 899 | - const response = await fetch('http://localhost:6002/search/', { | |
| 900 | - method: 'POST', | |
| 901 | - headers: { 'Content-Type': 'application/json' }, | |
| 902 | - body: JSON.stringify({ | |
| 903 | - query: this.query, | |
| 904 | - size: this.pageSize, | |
| 905 | - from: (this.currentPage - 1) * this.pageSize, | |
| 906 | - filters: this.filters, | |
| 907 | - range_filters: this.rangeFilters, | |
| 908 | - facets: [ | |
| 909 | - { field: 'category.keyword', size: 15 }, | |
| 910 | - { field: 'vendor.keyword', size: 15 } | |
| 911 | - ] | |
| 912 | - }) | |
| 913 | - }); | |
| 914 | - | |
| 915 | - const data = await response.json(); | |
| 916 | - this.results = data.hits; | |
| 917 | - this.facets = data.facets || []; | |
| 918 | - this.total = data.total; | |
| 919 | - }, | |
| 920 | - | |
| 921 | - toggleFilter(field, value) { | |
| 922 | - if (!this.filters[field]) { | |
| 923 | - this.filters[field] = []; | |
| 924 | - } | |
| 925 | - | |
| 926 | - const index = this.filters[field].indexOf(value); | |
| 927 | - if (index > -1) { | |
| 928 | - this.filters[field].splice(index, 1); | |
| 929 | - if (this.filters[field].length === 0) { | |
| 930 | - delete this.filters[field]; | |
| 931 | - } | |
| 932 | - } else { | |
| 933 | - this.filters[field].push(value); | |
| 934 | - } | |
| 935 | - | |
| 936 | - this.currentPage = 1; | |
| 937 | - this.search(); | |
| 938 | - }, | |
| 939 | - | |
| 940 | - setPriceRange(min, max) { | |
| 941 | - if (min !== null || max !== null) { | |
| 942 | - this.rangeFilters.price = {}; | |
| 943 | - if (min !== null) this.rangeFilters.price.gte = min; | |
| 944 | - if (max !== null) this.rangeFilters.price.lte = max; | |
| 945 | - } else { | |
| 946 | - delete this.rangeFilters.price; | |
| 947 | - } | |
| 948 | - this.currentPage = 1; | |
| 949 | - this.search(); | |
| 950 | - } | |
| 951 | - } | |
| 952 | -}; | |
| 953 | -``` | |
| 954 | - | |
| 955 | ---- | |
| 956 | - | |
| 957 | -## 调试与优化 | |
| 958 | - | |
| 959 | -### 启用调试模式 | |
| 960 | - | |
| 961 | -```bash | |
| 962 | -curl -X POST "http://localhost:6002/search/" \ | |
| 963 | - -H "Content-Type: application/json" \ | |
| 964 | - -H "X-Tenant-ID: 162" \ | |
| 965 | - -d '{ | |
| 966 | - "query": "手机", | |
| 967 | - "language": "zh", | |
| 968 | - "debug": true | |
| 969 | - }' | |
| 970 | -``` | |
| 971 | - | |
| 972 | -**响应包含调试信息**: | |
| 973 | -```json | |
| 974 | -{ | |
| 975 | - "hits": [...], | |
| 976 | - "total": 118, | |
| 977 | - "debug_info": { | |
| 978 | - "query_analysis": { | |
| 979 | - "original_query": "玩具", | |
| 980 | - "query_normalized": "玩具", | |
| 981 | - "rewritten_query": "玩具", | |
| 982 | - "detected_language": "zh", | |
| 983 | - "translations": {"en": "toy"} | |
| 984 | - }, | |
| 985 | - "es_query": { | |
| 986 | - "query": {...}, | |
| 987 | - "size": 10 | |
| 988 | - }, | |
| 989 | - "stage_timings": { | |
| 990 | - "query_parsing": 5.3, | |
| 991 | - "elasticsearch_search": 35.1, | |
| 992 | - "result_processing": 4.8 | |
| 993 | - } | |
| 994 | - } | |
| 995 | -} | |
| 996 | -``` | |
| 997 | - | |
| 998 | -### 设置最小分数阈值 | |
| 999 | - | |
| 1000 | -```bash | |
| 1001 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1002 | - -H "Content-Type: application/json" \ | |
| 1003 | - -H "X-Tenant-ID: 162" \ | |
| 1004 | - -d '{ | |
| 1005 | - "query": "手机", | |
| 1006 | - "language": "zh", | |
| 1007 | - "min_score": 5.0 | |
| 1008 | - }' | |
| 1009 | -``` | |
| 1010 | - | |
| 1011 | -说明:只返回相关性分数 ≥ 5.0 的结果。 | |
| 1012 | - | |
| 1013 | ---- | |
| 1014 | - | |
| 1015 | -## 常见使用场景 | |
| 1016 | - | |
| 1017 | -### 场景 1:电商分类页 | |
| 1018 | - | |
| 1019 | -```bash | |
| 1020 | -# 显示某个类目下的所有商品,按价格排序,提供品牌筛选 | |
| 1021 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1022 | - -H "Content-Type: application/json" \ | |
| 1023 | - -H "X-Tenant-ID: 162" \ | |
| 1024 | - -d '{ | |
| 1025 | - "query": "*", | |
| 1026 | - "filters": { | |
| 1027 | - "category_name": "手机" | |
| 1028 | - }, | |
| 1029 | - "facets": [ | |
| 1030 | - {"field": "vendor.keyword", "size": 20}, | |
| 1031 | - { | |
| 1032 | - "field": "price", | |
| 1033 | - "type": "range", | |
| 1034 | - "ranges": [ | |
| 1035 | - {"key": "0-50", "to": 50}, | |
| 1036 | - {"key": "50-100", "from": 50, "to": 100}, | |
| 1037 | - {"key": "100-200", "from": 100, "to": 200}, | |
| 1038 | - {"key": "200+", "from": 200} | |
| 1039 | - ] | |
| 1040 | - } | |
| 1041 | - ], | |
| 1042 | - "sort_by": "min_price", | |
| 1043 | - "sort_order": "asc", | |
| 1044 | - "size": 24 | |
| 1045 | - }' | |
| 1046 | -``` | |
| 1047 | - | |
| 1048 | -### 场景 2:搜索结果页 | |
| 1049 | - | |
| 1050 | -```bash | |
| 1051 | -# 用户搜索关键词,提供筛选和排序(包含规格分面) | |
| 1052 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1053 | - -H "Content-Type: application/json" \ | |
| 1054 | - -H "X-Tenant-ID: 162" \ | |
| 1055 | - -d '{ | |
| 1056 | - "query": "手机", | |
| 1057 | - "language": "zh", | |
| 1058 | - "facets": [ | |
| 1059 | - {"field": "category1_name", "size": 10}, | |
| 1060 | - {"field": "category2_name", "size": 10}, | |
| 1061 | - "specifications.color", | |
| 1062 | - "specifications.size", | |
| 1063 | - { | |
| 1064 | - "field": "min_price", | |
| 1065 | - "type": "range", | |
| 1066 | - "ranges": [ | |
| 1067 | - {"key": "0-50", "to": 50}, | |
| 1068 | - {"key": "50-100", "from": 50, "to": 100}, | |
| 1069 | - {"key": "100+", "from": 100} | |
| 1070 | - ] | |
| 1071 | - } | |
| 1072 | - ], | |
| 1073 | - "size": 20 | |
| 1074 | - }' | |
| 1075 | -``` | |
| 1076 | - | |
| 1077 | -### 场景 2.1:带规格过滤的搜索结果页 | |
| 1078 | - | |
| 1079 | -```bash | |
| 1080 | -# 用户搜索并选择了规格筛选条件 | |
| 1081 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1082 | - -H "Content-Type: application/json" \ | |
| 1083 | - -H "X-Tenant-ID: 162" \ | |
| 1084 | - -d '{ | |
| 1085 | - "query": "手机", | |
| 1086 | - "language": "zh", | |
| 1087 | - "filters": { | |
| 1088 | - "category_name": "手机", | |
| 1089 | - "specifications": { | |
| 1090 | - "name": "color", | |
| 1091 | - "value": "white" | |
| 1092 | - } | |
| 1093 | - }, | |
| 1094 | - "facets": [ | |
| 1095 | - "category1_name", | |
| 1096 | - "specifications.color", | |
| 1097 | - "specifications.size" | |
| 1098 | - ], | |
| 1099 | - "size": 20 | |
| 1100 | - }' | |
| 1101 | -``` | |
| 1102 | - | |
| 1103 | -### 场景 3:促销专区 | |
| 1104 | - | |
| 1105 | -```bash | |
| 1106 | -# 显示特定价格区间的商品 | |
| 1107 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1108 | - -H "Content-Type: application/json" \ | |
| 1109 | - -H "X-Tenant-ID: 162" \ | |
| 1110 | - -d '{ | |
| 1111 | - "query": "*", | |
| 1112 | - "range_filters": { | |
| 1113 | - "min_price": { | |
| 1114 | - "gte": 50, | |
| 1115 | - "lte": 100 | |
| 1116 | - } | |
| 1117 | - }, | |
| 1118 | - "facets": ["category1_name", "category2_name", "specifications"], | |
| 1119 | - "sort_by": "min_price", | |
| 1120 | - "sort_order": "asc", | |
| 1121 | - "size": 50 | |
| 1122 | - }' | |
| 1123 | -``` | |
| 1124 | - | |
| 1125 | -### 场景 4:新品推荐 | |
| 1126 | - | |
| 1127 | -```bash | |
| 1128 | -# 最近更新的商品 | |
| 1129 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1130 | - -H "Content-Type: application/json" \ | |
| 1131 | - -H "X-Tenant-ID: 162" \ | |
| 1132 | - -d '{ | |
| 1133 | - "query": "*", | |
| 1134 | - "range_filters": { | |
| 1135 | - "days_since_last_update": { | |
| 1136 | - "lte": 7 | |
| 1137 | - } | |
| 1138 | - }, | |
| 1139 | - "sort_by": "create_time", | |
| 1140 | - "sort_order": "desc", | |
| 1141 | - "size": 20 | |
| 1142 | - }' | |
| 1143 | -``` | |
| 1144 | - | |
| 1145 | ---- | |
| 1146 | - | |
| 1147 | -## 错误处理 | |
| 1148 | - | |
| 1149 | -### 示例 1:参数错误 | |
| 1150 | - | |
| 1151 | -```bash | |
| 1152 | -# 错误:range_filters 缺少操作符 | |
| 1153 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1154 | - -H "Content-Type: application/json" \ | |
| 1155 | - -H "X-Tenant-ID: 162" \ | |
| 1156 | - -d '{ | |
| 1157 | - "query": "手机", | |
| 1158 | - "language": "zh", | |
| 1159 | - "range_filters": { | |
| 1160 | - "min_price": {} | |
| 1161 | - } | |
| 1162 | - }' | |
| 1163 | -``` | |
| 1164 | - | |
| 1165 | -**响应**: | |
| 1166 | -```json | |
| 1167 | -{ | |
| 1168 | - "error": "Validation error", | |
| 1169 | - "detail": "至少需要指定一个范围边界(gte, gt, lte, lt)", | |
| 1170 | - "timestamp": 1699800000 | |
| 1171 | -} | |
| 1172 | -``` | |
| 1173 | - | |
| 1174 | -### 示例 2:空查询 | |
| 1175 | - | |
| 1176 | -```bash | |
| 1177 | -# 错误:query 为空 | |
| 1178 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1179 | - -H "Content-Type: application/json" \ | |
| 1180 | - -H "X-Tenant-ID: 162" \ | |
| 1181 | - -d '{ | |
| 1182 | - "query": "" | |
| 1183 | - }' | |
| 1184 | -``` | |
| 1185 | - | |
| 1186 | -**响应**: | |
| 1187 | -```json | |
| 1188 | -{ | |
| 1189 | - "error": "Validation error", | |
| 1190 | - "detail": "query field required", | |
| 1191 | - "timestamp": 1699800000 | |
| 1192 | -} | |
| 1193 | -``` | |
| 1194 | - | |
| 1195 | ---- | |
| 1196 | - | |
| 1197 | -## 性能优化建议 | |
| 1198 | - | |
| 1199 | -### 1. 合理使用分面 | |
| 1200 | - | |
| 1201 | -```bash | |
| 1202 | -# ❌ 不推荐:请求太多分面 | |
| 1203 | -{ | |
| 1204 | - "facets": [ | |
| 1205 | - {"field": "field1", "size": 100}, | |
| 1206 | - {"field": "field2", "size": 100}, | |
| 1207 | - {"field": "field3", "size": 100}, | |
| 1208 | - // ... 10+ facets | |
| 1209 | - ] | |
| 1210 | -} | |
| 1211 | - | |
| 1212 | -# ✅ 推荐:只请求必要的分面 | |
| 1213 | -{ | |
| 1214 | - "facets": [ | |
| 1215 | - {"field": "category.keyword", "size": 15}, | |
| 1216 | - {"field": "vendor.keyword", "size": 15} | |
| 1217 | - ] | |
| 1218 | -} | |
| 1219 | -``` | |
| 1220 | - | |
| 1221 | -### 2. 控制返回数量 | |
| 1222 | - | |
| 1223 | -```bash | |
| 1224 | -# ❌ 不推荐:一次返回太多 | |
| 1225 | -{ | |
| 1226 | - "size": 100 | |
| 1227 | -} | |
| 1228 | - | |
| 1229 | -# ✅ 推荐:分页查询 | |
| 1230 | -{ | |
| 1231 | - "size": 20, | |
| 1232 | - "from": 0 | |
| 1233 | -} | |
| 1234 | -``` | |
| 1235 | - | |
| 1236 | -### 3. 使用适当的过滤器 | |
| 1237 | - | |
| 1238 | -```bash | |
| 1239 | -# ✅ 推荐:先过滤后搜索 | |
| 1240 | -{ | |
| 1241 | - "query": "玩具", | |
| 1242 | - "filters": { | |
| 1243 | - "category.keyword": "玩具" | |
| 1244 | - } | |
| 1245 | -} | |
| 1246 | -``` | |
| 1247 | - | |
| 1248 | ---- | |
| 1249 | - | |
| 1250 | -## 高级技巧 | |
| 1251 | - | |
| 1252 | -### 技巧 1:获取所有类目 | |
| 1253 | - | |
| 1254 | -```bash | |
| 1255 | -# 使用通配符查询 + 分面 | |
| 1256 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1257 | - -H "Content-Type: application/json" \ | |
| 1258 | - -H "X-Tenant-ID: 162" \ | |
| 1259 | - -d '{ | |
| 1260 | - "query": "*", | |
| 1261 | - "size": 0, | |
| 1262 | - "facets": [ | |
| 1263 | - {"field": "category.keyword", "size": 100} | |
| 1264 | - ] | |
| 1265 | - }' | |
| 1266 | -``` | |
| 1267 | - | |
| 1268 | -### 技巧 2:价格分布统计 | |
| 1269 | - | |
| 1270 | -```bash | |
| 1271 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1272 | - -H "Content-Type: application/json" \ | |
| 1273 | - -H "X-Tenant-ID: 162" \ | |
| 1274 | - -d '{ | |
| 1275 | - "query": "手机", | |
| 1276 | - "language": "zh", | |
| 1277 | - "size": 0, | |
| 1278 | - "facets": [ | |
| 1279 | - { | |
| 1280 | - "field": "price", | |
| 1281 | - "type": "range", | |
| 1282 | - "ranges": [ | |
| 1283 | - {"key": "0-50", "to": 50}, | |
| 1284 | - {"key": "50-100", "from": 50, "to": 100}, | |
| 1285 | - {"key": "100-200", "from": 100, "to": 200}, | |
| 1286 | - {"key": "200-500", "from": 200, "to": 500}, | |
| 1287 | - {"key": "500+", "from": 500} | |
| 1288 | - ] | |
| 1289 | - } | |
| 1290 | - ] | |
| 1291 | - }' | |
| 1292 | -``` | |
| 1293 | - | |
| 1294 | -### 技巧 3:组合多种查询类型 | |
| 1295 | - | |
| 1296 | -```bash | |
| 1297 | -# 布尔表达式 + 过滤器 + 分面 + 排序 | |
| 1298 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1299 | - -H "Content-Type: application/json" \ | |
| 1300 | - -H "X-Tenant-ID: 162" \ | |
| 1301 | - -d '{ | |
| 1302 | - "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", | |
| 1303 | - "filters": { | |
| 1304 | - "category.keyword": ["玩具", "益智玩具"] | |
| 1305 | - }, | |
| 1306 | - "range_filters": { | |
| 1307 | - "min_price": {"gte": 20, "lte": 100}, | |
| 1308 | - "days_since_last_update": {"lte": 30} | |
| 1309 | - }, | |
| 1310 | - "facets": [ | |
| 1311 | - {"field": "vendor.keyword", "size": 20} | |
| 1312 | - ], | |
| 1313 | - "sort_by": "min_price", | |
| 1314 | - "sort_order": "asc", | |
| 1315 | - "size": 20 | |
| 1316 | - }' | |
| 1317 | -``` | |
| 1318 | - | |
| 1319 | ---- | |
| 1320 | - | |
| 1321 | -## 测试数据 | |
| 1322 | - | |
| 1323 | -如果你需要测试数据,可以使用以下查询: | |
| 1324 | - | |
| 1325 | -```bash | |
| 1326 | -# 测试类目:玩具 | |
| 1327 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1328 | - -H "Content-Type: application/json" \ | |
| 1329 | - -H "X-Tenant-ID: 162" \ | |
| 1330 | - -d '{"query": "玩具", "size": 5}' | |
| 1331 | - | |
| 1332 | -# 测试品牌:乐高 | |
| 1333 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1334 | - -H "Content-Type: application/json" \ | |
| 1335 | - -H "X-Tenant-ID: 162" \ | |
| 1336 | - -d '{"query": "brand:乐高", "size": 5}' | |
| 1337 | - | |
| 1338 | -# 测试布尔表达式 | |
| 1339 | -curl -X POST "http://localhost:6002/search/" \ | |
| 1340 | - -H "Content-Type: application/json" \ | |
| 1341 | - -H "X-Tenant-ID: 162" \ | |
| 1342 | - -d '{"query": "玩具 AND 乐高", "size": 5}' | |
| 1343 | -``` | |
| 1344 | - | |
| 1345 | ---- | |
| 1346 | - | |
| 1347 | -**文档版本**: 3.0 | |
| 1348 | -**最后更新**: 2024-11-12 | |
| 1349 | -**相关文档**: `API_DOCUMENTATION.md` | |
| 1350 | - |
docs/TODO-ES能力提升.md
docs/TODO-prompts.md deleted
| ... | ... | @@ -1,39 +0,0 @@ |
| 1 | - | |
| 2 | -1. debug信息展示方面 | |
| 3 | -在这次重构过程中,你基本上了解了从query析到 ES 搜索,再到重排的全流程。 | |
| 4 | -在这些环节中,有哪些重要的信息对搜索结果产生较大影响,先回顾,适当的完善、优化日志。 | |
| 5 | -debug_info也给前端更充分的调试信息,包括,丰富每条结果的ranking debug。目前只有这些 | |
| 6 | -Ranking Debug | |
| 7 | -spu_id | |
| 8 | -ES score | |
| 9 | -ES normalized | |
| 10 | -ES norm (rerank input): | |
| 11 | -Rerank score | |
| 12 | -Fused score | |
| 13 | -title.en | |
| 14 | -title.zh | |
| 15 | - | |
| 16 | -还不够,需要详细思考,设计一个更加完整的展示的体系 | |
| 17 | -比如: | |
| 18 | -position(重排前的,重排后最终的) | |
| 19 | -ES打分,从ES检索原始打分、相关的因子(minmax归一化的因子。到底用了哪些因子,你自己关注下,用到的要体现出来,让人知道从原始打分怎么得到的ES norm (rerank input))。另外ES normalized,ES norm,要梳理清楚,如果代码不清晰则改代码,展示不清晰则要梳理展示。 | |
| 20 | -Rerank这一块,除了rerank_score,输入的信息比如输入的doc模板,被选中的sku及其用于辅助reranker相关性的、加到title后面的后缀 | |
| 21 | -融合公式这一块,除了fused_score,还要包含rerank_factor, text_factor, knn_factor等因子,以及text_score, knn_score, text_source_score, text_translation_score, … 等变量,要清晰的展示。 | |
| 22 | -证据 matched_queries | |
| 23 | - | |
| 24 | - | |
| 25 | -耗时记录方面: | |
| 26 | -1. Stage Timings: 为每个阶段耗时补充起止时间戳。 | |
| 27 | -2, 我看到total_search大幅度大于上面各个Stage 的总和,可能漏了一些重要的stage,比如「款式意图 SKU 预筛选(StyleSkuSelector.prepare_hits)」,补上这个stage | |
| 28 | - | |
| 29 | -但是也要注意,不要影响不带debug_info的请求的性能。 | |
| 30 | - | |
| 31 | - | |
| 32 | -2. 日志方面也需要完善 | |
| 33 | -从 query 到 ES 再到重排,对结果影响大的信息,语种检测 + index_languages,Query 向量,ES查询的构建过程中所使用的关键的变量、关键的中间结果,rerank相关信息(intput/window,query/doc 模板,融合公式的详细信息和关键的因子、输入原始值以及中间变量、到最终的融合公式的因子,Page fill,sku_filter_dimension等 | |
| 34 | - | |
| 35 | - | |
| 36 | -3. 测试用例方面 | |
| 37 | -子串匹配(Direct 阶段)是否可能产生误判 — 与尺码/短词强相关 | |
| 38 | -_is_direct_match 使用 normalized_value in query_text(见 sku_intent_selector.py)。 | |
| 39 | -词表里 l、m、s、xl,会在常见英文 query 里产生字符级子串命中,例如"l" 是否会匹配 "large …" 里的 l,注意要用分词匹配,检查分词匹配逻辑是否容易产生badcase。 |
docs/TODO-意图判断-2.md deleted
| ... | ... | @@ -1,40 +0,0 @@ |
| 1 | - | |
| 2 | -一、 增加款式意图识别模块 | |
| 3 | -意图类型: 颜色,尺码(目前只需要支持这两种) | |
| 4 | - | |
| 5 | - | |
| 6 | -二、 意图判断 | |
| 7 | -- 意图召回层: | |
| 8 | -每种意图,有一个召回词集合 | |
| 9 | -对query(包括原始query、各种翻译query 都做匹配) | |
| 10 | -- 以颜色意图为例: | |
| 11 | -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 | |
| 12 | -query匹配了其中任何一个词,都认为,具有颜色意图 | |
| 13 | -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 | |
| 14 | - | |
| 15 | - | |
| 16 | -三、 意图使用: | |
| 17 | - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 | |
| 18 | -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter | |
| 19 | - 要改为: | |
| 20 | - 1. 有款式意图的时候,才做sku筛选 | |
| 21 | - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 | |
| 22 | - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 | |
| 23 | - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: | |
| 24 | - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url | |
| 25 | - 2. 用来做rerank doc的title补充,从而参与rerank | |
| 26 | - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) | |
| 27 | - | |
| 28 | -- sku筛选的规则也要优化: | |
| 29 | -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 | |
| 30 | -改为: | |
| 31 | - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 | |
| 32 | - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 | |
| 33 | - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 | |
| 34 | - 这个sku筛选也需要提取为一个独立的模块。 | |
| 35 | - | |
| 36 | -细节备注: | |
| 37 | -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name | |
| 38 | - | |
| 39 | -5. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*_name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 sku_filter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。) | |
| 40 | - |
docs/TODO-意图判断.md renamed to docs/TODO-意图判断-done.md
| ... | ... | @@ -97,3 +97,172 @@ ption_value的匹é…。æ„图检测的时候,有匹é…çš„queryä¸çš„命ä¸çš„è |
| 97 | 97 | 验è¯è¿‡ï¼š |
| 98 | 98 | - `pytest tests/test_search_rerank_window.py -q` 通过 |
| 99 | 99 | - å˜æ›´æ–‡ä»¶ lint æ— æŠ¥é”™ |
| 100 | + | |
| 101 | + | |
| 102 | +------------------------------ | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | +--- | |
| 107 | + | |
| 108 | +## 1. 现状(与需求的差è·ï¼‰ | |
| 109 | + | |
| 110 | +**æµæ°´çº¿**(`search/searcher.py`)大致是: | |
| 111 | + | |
| 112 | +1. `QueryParser.parse` → `ParsedQuery`ï¼ˆå« `translations`ã€`query_tokens` ç‰ï¼‰ | |
| 113 | +2. 组 ES æŸ¥è¯¢ï¼›è‹¥åœ¨é‡æŽ’çª—å£å†…,第一次查询把 `_source` è£æˆã€Œé‡æŽ’æ¨¡æ¿æ‰€éœ€å—段ã€ï¼ˆ`_resolve_rerank_source_filter`) | |
| 114 | +3. ES æœç´¢ → `run_rerank`(`rerank_client.build_docs_from_hits` 用 `{title}` ç‰æ‹¼ doc) | |
| 115 | +4. 按 `from/size` 切片 → **page fill** 用 `ids` 查询把当å‰é¡µ `_source` 补全 | |
| 116 | +5. `_apply_sku_sorting_for_page_hits`(仅 **option1**,先å串包å«å‘½ä¸ç¬¬ä¸€ä¸ªï¼Œå¦åˆ™å…¨é‡ option1 embedding) | |
| 117 | +6. `ResultFormatter`(`sku_filter_dimension` åªåš**展示层**按维度折å SKU,与置顶逻辑独立) | |
| 118 | + | |
| 119 | + | |
| 120 | +**与需求冲çªä½†å¿…须一起解决的一点**:page fill 会用 ES 拉回æ¥çš„ `_source`**整份覆盖**å½“å‰ hit(约 841–842 行)。若在 rerank **之å‰**åªæ”¹å†…å˜é‡Œçš„ `skus` 顺åº/`image_url`,**ä¸**在 fill åŽå†å¤„ç†ä¸€æ¬¡ï¼Œæœ€ç»ˆå“åº”ä¼šè¢«è¦†ç›–æŽ‰ã€‚å› æ¤ã€Œrerank å‰å¯¹æ‰€æœ‰ window 内 hit åš SKU 决ç–ã€å’Œã€Œç”¨æˆ·çœ‹åˆ°çš„æœ€ç»ˆåˆ—表ã€ä¹‹é—´ï¼Œå¿…须有一æ¡**明确的数æ®å¥‘约**(è§ä¸‹æ–‡ §4)。 | |
| 121 | + | |
| 122 | +--- | |
| 123 | + | |
| 124 | +## 2. 模å—划分(建议:`intent` + `sku_intent` 两层) | |
| 125 | + | |
| 126 | +é¿å…ç»§ç»åœ¨ `Searcher` é‡Œå †æ–¹æ³•ï¼Œå»ºè®®æ–°å»ºå°åŒ…,èŒè´£æ¸…æ™°ã€ç”± `Searcher` 编排。 | |
| 127 | + | |
| 128 | +| æ¨¡å— | èŒè´£ | | |
| 129 | +|------|------| | |
| 130 | +| **`query/intent/`**(或 `search/intent/`ï¼ŒäºŒé€‰ä¸€ä»¥ã€Œç¦»è°æ›´è¿‘ã€ä¸ºå‡†ï¼›æ›´æŽ¨è **`query/intent`**ï¼Œå› ä¸ºè¾“å…¥å®Œå…¨æ˜¯ query 侧事实) | åŠ è½½è¯è¡¨ã€**æ„图å¬å›ž**ã€å¤š query å˜ä½“ + 粗细分è¯ã€è¾“出结构化 **`IntentProfile`** | | |
| 131 | +| **`search/sku_intent/`**(或 `intent/sku_selection.py`) | æ ¹æ® `IntentProfile` è§£æž **option1/2/3** 哪一维ã€ç”Ÿæˆæ¯ SKU çš„**åŒ¹é…æ–‡æœ¬**ã€ä¸‰è½®åŒ¹é…规则ã€embedding 批处ç†ã€å¯¹ `_source` åš **promote + image_url** | | |
| 132 | +| **`search/rerank_client.py`(薄扩展)** | 支æŒã€Œæ¯æ¡ hit çš„ doc 文本ã€ï¼šæ¨¡æ¿æ‰©å±•或 **显å¼ä¼ å…¥ per-hit å—符串列表**,é¿å…把业务塞进 format å—符串 | | |
| 133 | + | |
| 134 | +**`IntentProfile`(概念模型)建议包å«**: | |
| 135 | + | |
| 136 | +- `active_intents: Set[Literal["color","size"]]`ï¼ˆå¯æ‰©å±•) | |
| 137 | +- æ¯ç§æ„图:`canonical_terms`(命ä¸è¡Œçš„æ ‡å‡†è¯ï¼‰ã€`matched_surface_forms`(å¯é€‰ï¼Œç”¨äºŽ debug) | |
| 138 | +- **维度别å**:如 color → `{"color","颜色","colour",...}`(é…置或独立å°è¯è¡¨ï¼‰ | |
| 139 | +- 原始用于匹é…çš„ token 集åˆï¼šæ¯ä¸ª query å˜ä½“ ×(细粒度 | ç²—ç²’åº¦ï¼‰ï¼Œä¾¿äºŽæ—¥å¿—ä¸Žå•æµ‹ | |
| 140 | + | |
| 141 | +**è¯è¡¨**: | |
| 142 | + | |
| 143 | +- **æ„图å¬å›žè¡¨**:æ¯è¡Œé€—å·åˆ†éš”åŒä¹‰è¯ï¼Œé¦–è¯æ ‡å‡†åŒ–;颜色ã€å°ºç å„一份(路径放 `config/` 或 `resources/intent/` + `config.yaml` 指路径)。 | |
| 144 | +- **SKU 第二轮「泛化ã€è¡¨**(对 **option å–值** åšåŒä¹‰æ‰©å±•):与æ„图å¬å›žè¡¨åˆ†å¼€ï¼Œé¿å…è¯ä¹‰æ··åœ¨ä¸€èµ·ã€‚ | |
| 145 | + | |
| 146 | +--- | |
| 147 | + | |
| 148 | +## 3. æ„图判æ–(与 `QueryParser` 的衔接) | |
| 149 | + | |
| 150 | +需求:对 **原始 query + å„类翻译** 都åšåŒ¹é…ï¼›**细粒度 + 粗粒度** 分è¯ã€‚ | |
| 151 | + | |
| 152 | +现状: | |
| 153 | + | |
| 154 | +- `ParsedQuery` 里 **`query_tokens` åªå¯¹ rewritten åŽçš„ `query_text` 跑了一次 HanLP**(`query_parser.py` 269–274 行附近),**没有**对 `original_query`ã€å„ `translations` çš„ token 缓å˜ã€‚ | |
| 155 | +- 已有 **`simple_tokenize_query`**(粗粒度)在 `query_parser.py`。 | |
| 156 | + | |
| 157 | +**建议**: | |
| 158 | + | |
| 159 | +- 在 **`IntentDetector.detect(parsed_query, tokenizer_fn)`** 内统一生æˆã€Œquery å˜ä½“列表ã€ï¼šè‡³å°‘åŒ…å« `original_query`ã€`query_normalized`ã€`rewritten_query`ã€`translations` çš„å€¼ï¼ˆä¸Žå½“å‰ `_build_sku_query_texts` æ€è·¯ä¸€è‡´ï¼Œä½†å‡çº§ä¸º**结构化**)。 | |
| 160 | +- 细粒度:å¤ç”¨ `QueryParser._get_query_tokens`(需把该方法暴露为公开 API 或注入åŒä¸€ HanLP callable),对æ¯ä¸ªå˜ä½“å—符串调用。 | |
| 161 | +- 粗粒度:对æ¯ä¸ªå˜ä½“调用 `simple_tokenize_query`。 | |
| 162 | +- 匹é…逻辑:**ä»»æ„å˜ä½“ × ä»»æ„粒度** çš„ token è½åœ¨ã€Œæ ‡å‡†åŒ– → åŒä¹‰è¯é—包ã€ä¸Šå³è§†ä¸ºå‘½ä¸è¯¥æ„å›¾ï¼ˆä¸Žä½ æè¿°çš„行内åŒä¹‰ä¸€è‡´ï¼‰ã€‚ | |
| 163 | + | |
| 164 | +**å¯é€‰ä¼˜åŒ–**:在 `parse()` 里顺带产出 `intent_profile`,å‡å°‘一次é历;但为控制 `QueryParser` 体积,更稳妥的是 **parse 之åŽ**å•独调 `IntentDetector`,ä¾èµ–清晰。 | |
| 165 | + | |
| 166 | +--- | |
| 167 | + | |
| 168 | +## 4. æµæ°´çº¿æ”¹é€ (与 page fill 的契约) | |
| 169 | + | |
| 170 | +ç›®æ ‡é¡ºåºå˜ä¸ºï¼š | |
| 171 | + | |
| 172 | +`ES(window)`(有æ„图时 `_source` å« `skus` + `option*_name`) | |
| 173 | +→ **对æ¯ä¸ª hit:SKU å†³ç– + ç”Ÿæˆ rerank 用åŽç¼€/全文** | |
| 174 | +→ `run_rerank`(doc = æ ‡é¢˜ + åŽç¼€ï¼‰ | |
| 175 | +→ 切片 | |
| 176 | +→ page fill | |
| 177 | +→ **最终å“应å‰å†åº”用一次 SKU 决ç–(或与 prefetch 结果åˆå¹¶ï¼‰** | |
| 178 | +→ `ResultFormatter` | |
| 179 | + | |
| 180 | +**为何最åŽè¿˜è¦ä¸€æ¬¡ï¼Ÿ** å› ä¸º page fill 会覆盖 `_source`,rerank å‰å†…å˜é‡Œçš„ `skus` 顺åºä¸èƒ½å½“作最终真相。 | |
| 181 | + | |
| 182 | +**推è契约(é™ä½Žå¤æ‚度)**: | |
| 183 | + | |
| 184 | +1. **Rerank å‰**:对 window 内æ¯ä¸ª hit 计算 `SkuIntentDecision`(至少包å«ï¼š`option_slot` 1/2/3ã€`candidate_sku_index` 或 `sku_id`ã€`rerank_suffix` å—ç¬¦ä¸²ï¼‰ã€‚å¯æŒ‚在 hit çš„**éž ES å—æ®µ**上,例如 `hit["_intent_sku"] = {...}`(或åªå˜ `rerank_doc_text` 全文)。 | |
| 185 | +2. **`run_rerank`**:`build_docs_from_hits` è‹¥å‘现 hit 上已有 `rerank_doc_text`(或 `style_suffix` + 模æ¿ï¼‰ï¼Œåˆ™ä¼˜å…ˆä½¿ç”¨ï¼Œå¦åˆ™èµ°åŽŸæ¨¡æ¿ã€‚ | |
| 186 | +3. **Page fill 之åŽ**:对**当å‰é¡µ** hit å†è°ƒç”¨**åŒä¸€** `SkuIntentSelector.apply(source, parsed_query, intent_profile)`ï¼ˆæˆ–æ ¹æ® `_id` åˆå¹¶ prefetch 决ç–ï¼‰ã€‚è¿™æ ·æœ€ç»ˆ `image_url` / SKU 顺åºä¸Ž rerank 一致,且ä¸è¢« fill 冲掉。 | |
| 187 | + | |
| 188 | +若担心算两次 embedding:**第一次**在 window å…¨é‡ä¸Šç®— query å‘é‡ + option å‘é‡ï¼›ç¬¬äºŒæ¬¡ä»…对当å‰é¡µä¸”å¯å¸¦ç¼“å˜ï¼ˆæŒ‰ `embed_key` 去é‡ï¼‰ï¼Œä¸€èˆ¬é‡å¾ˆå°ã€‚ | |
| 189 | + | |
| 190 | +**ä¸åœ¨é‡æŽ’窗å£å†…**:没有「rerank å‰å…¨ windowã€è¿™ä¸€æ¥ï¼›å¯åœ¨ **ResultFormatter å‰**对当å‰é¡µ `es_hits` 用åŒä¸€ `SkuIntentSelector`(仅当有æ„图时),与「有æ„图æ‰åš SKU ç›é€‰ã€ä¸€è‡´ã€‚ | |
| 191 | + | |
| 192 | +--- | |
| 193 | + | |
| 194 | +## 5. `_resolve_rerank_source_filter` 与 ES å—æ®µ | |
| 195 | + | |
| 196 | +需求:有æ„图时预å–éœ€åŒ…å« `skus`ã€`option1_name`ã€`option2_name`ã€`option3_name`。 | |
| 197 | + | |
| 198 | +建议ç¾å扩展为: | |
| 199 | + | |
| 200 | +`_resolve_rerank_source_filter(doc_template, intent_profile: Optional[IntentProfile])` | |
| 201 | + | |
| 202 | +- è‹¥ `intent_profile` éžç©ºä¸”å« color/size(或任æ„ã€Œæ¬¾å¼æ„图ã€ï¼‰ï¼Œåœ¨ `includes` ä¸**åˆå¹¶**ä¸Šè¿°å—æ®µï¼ˆå¹¶ä¸Žæ¨¡æ¿è§£æžå‡ºçš„ `title` ç‰å–并集)。 | |
| 203 | +- 注æ„与全局 `source_fields` çš„ tri-state è¯ä¹‰ï¼ˆ`_apply_source_filter`)是å¦å†²çªï¼šè‹¥ç§Ÿæˆ·é…ç½® `_source` 白åå•且ä¸å« `skus`,需定义优先级——**建议**ï¼šã€Œæ¬¾å¼æ„å›¾æ‰€éœ€å—æ®µã€ä½œä¸º**最低ä¿è¯**åˆå¹¶è¿›æœ¬æ¬¡è¯·æ±‚çš„ fetch includes,或在文档ä¸å†™æ˜Žé™åˆ¶ã€‚ | |
| 204 | + | |
| 205 | +--- | |
| 206 | + | |
| 207 | +## 6. 多维度 option 与「未匹é…维度å〠| |
| 208 | + | |
| 209 | +需求逻辑å¯è½åˆ°çº¯å‡½æ•°ï¼š | |
| 210 | + | |
| 211 | +1. 对æ¯ä¸ªæ„图类型,有 **维度别å集åˆ**(如 color)。 | |
| 212 | +2. 便¬¡ä¸Ž `option1_name`ã€`option2_name`ã€`option3_name`(å—符串,注æ„多è¯è¨€ï¼šä¸Ž indexer 一致,å¯èƒ½æ˜¯çº¯è‹±æ–‡æˆ–䏿–‡ï¼‰åš **casefold / 规范化** åŽåŒ¹é…别å表。 | |
| 213 | +3. 命ä¸åˆ™è¯¥ SKU 行的匹é…å—æ®µä¸º `option{k}_value`;用于 embedding key æ—¶ç»§ç»ç”¨ `name:value` å½¢å¼ï¼ˆæ²¿ç”¨çŽ°æœ‰ `_sku_option1_embedding_key` æ€è·¯ï¼Œæ³›åŒ–为 `option_slot`)。 | |
| 214 | +4. **若三个 name 都ä¸åŒ¹é…æ„图维度**:用 `option1_value`ã€`option2_value`ã€`option3_value` **ç©ºæ ¼æ‹¼æŽ¥**æˆä¸€æ¡ã€Œå…œåº•æè¿°å—符串ã€ï¼Œä¾›ï¼š | |
| 215 | + - 与 query 的包å«/泛化/embedding 比较; | |
| 216 | + - 作为 `rerank_suffix` çš„ä¸€éƒ¨åˆ†ï¼ˆè‹¥ä½ å¸Œæœ›æ— æ˜Žç¡®ç»´åº¦æ—¶ä»åŠ å¼º rerank)。 | |
| 217 | + | |
| 218 | +**多æ„å›¾åŒæ—¶å˜åœ¨**ï¼ˆå¦‚åŒæ—¶é¢œè‰²+å°ºç ):需è¦åœ¨äº§å“层定规则,例如: | |
| 219 | + | |
| 220 | +- åªå¯¹ã€Œä¸»æ„å›¾ã€æŽ’åºï¼ˆé…置优先级 color > size),或 | |
| 221 | +- è¦æ±‚两个维度都满足的 SKU 优先,å¦åˆ™é€€åŒ–ä¸ºå•æ„图。 | |
| 222 | + | |
| 223 | +实现上å¯åœ¨ `SkuIntentSelector` 输入 `List[IntentType]` 与ç–略枚举,é¿å…å†™æ» if-else æ•£è½ã€‚ | |
| 224 | + | |
| 225 | +--- | |
| 226 | + | |
| 227 | +## 7. 三轮 SKU 匹é…规则(独立模å—内) | |
| 228 | + | |
| 229 | +从当å‰ã€Œç¬¬ä¸€ä¸ªåŒ…å«å°±è¿”å›žã€æ”¹ä¸ºï¼š | |
| 230 | + | |
| 231 | +1. **第一轮**:统计「option åŒ¹é…æ–‡æœ¬è¢« **æ•´æ¡ query 文本** **包å«**ã€çš„ SKU(或对æ¯ä¸ª query å˜ä½“分别计,å†åˆå¹¶â€”â€”å»ºè®®ä¸Žä½ çŽ°æœ‰ `_build_sku_query_texts` 对é½ï¼‰ï¼›**è‹¥æ°å¥½ 1 个** → 选ä¸ã€‚ | |
| 232 | +2. **第二轮**:若 0 个,对æ¯ä¸ª SKU 的候选è¯èµ° **å–值泛化表**(åŒä¹‰è¯è¡Œï¼‰ï¼Œå†è·‘包å«åˆ¤æ–ï¼›ä»ç»Ÿè®¡ã€Œå¤šä¸ª / 零个ã€ã€‚ | |
| 233 | +3. **第三轮**: | |
| 234 | + - è‹¥ **多个** 满足包å«ï¼ˆç¬¬ä¸€è½®æˆ–第二轮)→ 仅在这多个上算 embedding,å–相似度最高; | |
| 235 | + - è‹¥ **ä» 0 个** → 对 **全部** SKU ç®— embeddingï¼Œå–æœ€é«˜ã€‚ | |
| 236 | + | |
| 237 | +å®žçŽ°ä¸Šä¿æŒ **æ‰¹é‡ encode**ï¼ˆä¸Žå½“å‰ `option1_values_to_encode` 去é‡é€»è¾‘ç±»ä¼¼ï¼‰ï¼Œåªæ˜¯æŠŠã€Œembed_keyã€ä»Žå›ºå®š option1 改为按 slot 动æ€ç”Ÿæˆã€‚ | |
| 238 | + | |
| 239 | +--- | |
| 240 | + | |
| 241 | +## 8. `sku_filter_dimension`(API)与æ„图的关系 | |
| 242 | + | |
| 243 | +- **`sku_filter_dimension`**:客户端指定「结果里 SKU 列表如何按维度折å ã€ï¼Œåœ¨ `ResultFormatter._filter_skus_by_dimensions` ä¸å®žçŽ°ã€‚ | |
| 244 | +- **æ„图 SKU 置顶**:æœåŠ¡ç«¯æ ¹æ® query 推æ–维度与å–值,改顺åºä¸Žä¸»å›¾ã€‚ | |
| 245 | + | |
| 246 | +建议约定: | |
| 247 | + | |
| 248 | +- **置顶 / æ¢å›¾**仅在æ„图开坿—¶æ‰§è¡Œï¼› | |
| 249 | +- **`sku_filter_dimension` ä»åªå½±å“返回 SKU æ¡æ•°ç»“æž„**;若与æ„图维度冲çªï¼ˆä¾‹å¦‚æ„å›¾å‘½ä¸ colorï¼Œå®¢æˆ·ç«¯åªæŒ‰ size 折å ),应用**文档说明优先级**:常è§åšæ³•æ˜¯ **å…ˆæ„å›¾ç½®é¡¶ï¼Œå† filter**(或相å,需在 PRD 写清)。 | |
| 250 | + | |
| 251 | +é¿å…在 `ResultFormatter` 里å†çŒœæ„图;æ„å›¾ç»“è®ºç”±ä¸Šæ¸¸ä¼ å…¥æˆ–åœ¨ Formatter å‰å·²å®Œæˆ `_source` 调整。 | |
| 252 | + | |
| 253 | +--- | |
| 254 | + | |
| 255 | +## 9. é…置与观测 | |
| 256 | + | |
| 257 | +- `config.yaml`:`intent.enabled`ã€`intent.lexicon_paths`ã€`intent.dimension_aliases`(或按类型分å—)。 | |
| 258 | +- `RequestContext` / `debug`:写入 `intent_profile`ã€`sku_intent_decision`ã€rerank 用的 doc 摘è¦ï¼Œä¾¿äºŽä¸Ž `docs/TODO-æ„图判æ–.md` 对é½ã€‚ | |
| 259 | + | |
| 260 | +--- | |
| 261 | + | |
| 262 | +## 10. å°ç»“ | |
| 263 | + | |
| 264 | +- **æ ¸å¿ƒæž¶æž„**:**`IntentDetector`(query 侧)** + **`SkuIntentSelector`(search 侧)** + **`run_rerank` çš„ per-hit doc 覆盖** + **`_resolve_rerank_source_filter` æ¡ä»¶ includes**。 | |
| 265 | +- **å¿…é¡»å¤„ç† page fill 覆盖 `_source`**:rerank å‰å†³ç–与 **fill åŽå† apply 一次**(或ç‰ä»·åˆå¹¶ç–略),å¦åˆ™ä¼šå‡ºçŽ°ã€Œé‡æŽ’ç”¨äº†å¸¦åŽç¼€çš„ docã€è¿”å›žç»“æžœå´æ˜¯æœªç½®é¡¶ SKUã€çš„ä¸ä¸€è‡´ã€‚ | |
| 266 | +- **与现有系统èžåˆç‚¹**:`ParsedQuery` å˜ä½“列表ã€HanLP + `simple_tokenize_query`ã€`TextEmbeddingEncoder`ã€`ResultFormatter` / `sku_filter_dimension` 的边界清晰,é¿å…把æ„图逻辑å¤åˆ¶åˆ° `api/` 层。 | |
| 267 | + | |
| 268 | +è‹¥ä½ åŽç»å¸Œæœ›æŠŠã€Œå¤šæ„å›¾ä¼˜å…ˆçº§ã€æˆ–「rerank åŽç¼€æ ¼å¼ã€å®šæˆå”¯ä¸€äº§å“规则,å¯ä»¥åœ¨å®žçްå‰å†™è¿›åŒä¸€ä»½ specï¼Œæ¨¡å—æŽ¥å£ä¼šå¾ˆå¥½ç¨³å®šä¸‹æ¥ã€‚ | |
| 100 | 269 | \ No newline at end of file | ... | ... |
docs/Untitled deleted
| ... | ... | @@ -1,38 +0,0 @@ |
| 1 | - | |
| 2 | -一、 增加款式意图识别模块 | |
| 3 | -意图类型: 颜色,尺码(目前只需要支持这两种) | |
| 4 | - | |
| 5 | - | |
| 6 | -二、 意图判断 | |
| 7 | -- 意图召回层: | |
| 8 | -每种意图,有一个召回词集合 | |
| 9 | -对query(包括原始query、各种翻译query 都做匹配) | |
| 10 | -- 以颜色意图为例: | |
| 11 | -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 | |
| 12 | -query匹配了其中任何一个词,都认为,具有颜色意图 | |
| 13 | -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 | |
| 14 | - | |
| 15 | - | |
| 16 | -三、 意图使用: | |
| 17 | - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 | |
| 18 | -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter | |
| 19 | - 要改为: | |
| 20 | - 1. 有款式意图的时候,才做sku筛选 | |
| 21 | - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 | |
| 22 | - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 | |
| 23 | - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: | |
| 24 | - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url | |
| 25 | - 2. 用来做rerank doc的title补充,从而参与rerank | |
| 26 | - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) | |
| 27 | - | |
| 28 | -- sku筛选的规则也要优化: | |
| 29 | -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 | |
| 30 | -改为: | |
| 31 | - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 | |
| 32 | - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 | |
| 33 | - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 | |
| 34 | - 这个sku筛选也需要提取为一个独立的模块。 | |
| 35 | - | |
| 36 | -细节备注: | |
| 37 | -intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 | |
| 38 | -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name |
docs/系统设计文档.md deleted
| ... | ... | @@ -1,894 +0,0 @@ |
| 1 | -# 搜索引擎通用化开发进度 | |
| 2 | - | |
| 3 | -## 项目概述 | |
| 4 | - | |
| 5 | -对后端搜索技术 做通用化。 | |
| 6 | -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 | |
| 7 | - | |
| 8 | - | |
| 9 | -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 | |
| 10 | - | |
| 11 | ---- | |
| 12 | - | |
| 13 | -## 1. 原始数据层的约定 | |
| 14 | - | |
| 15 | -### 1.1 店匠主表 | |
| 16 | - | |
| 17 | -所有租户共用以下主表: | |
| 18 | -- `shoplazza_product_sku` - SKU级别商品数据 | |
| 19 | -- `shoplazza_product_spu` - SPU级别商品数据 | |
| 20 | - | |
| 21 | -### 1.2 索引结构(SPU维度) | |
| 22 | - | |
| 23 | -**索引架构**: | |
| 24 | -- 每个租户使用独立的 Elasticsearch 索引(索引名称由 `get_tenant_index_name(tenant_id)` 动态生成) | |
| 25 | -- 索引粒度:SPU级别(每个文档代表一个SPU) | |
| 26 | -- 数据隔离:通过“分索引 + `tenant_id` 字段”双重隔离(索引级 + 文档级) | |
| 27 | -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组 | |
| 28 | - | |
| 29 | -**索引文档结构**: | |
| 30 | -```json | |
| 31 | -{ | |
| 32 | - "tenant_id": "1", | |
| 33 | - "spu_id": "123", | |
| 34 | - "title.zh": "蓝牙耳机", | |
| 35 | - "title.en": "Bluetooth Headphones", | |
| 36 | - "brief.zh": "高品质蓝牙耳机", | |
| 37 | - "brief.en": "High-quality Bluetooth headphones", | |
| 38 | - "category_name": "电子产品", | |
| 39 | - "category_path.zh": "电子产品/音频设备/耳机", | |
| 40 | - "category_path.en": "Electronics/Audio/Headphones", | |
| 41 | - "category1_name": "电子产品", | |
| 42 | - "category2_name": "音频设备", | |
| 43 | - "category3_name": "耳机", | |
| 44 | - "vendor.zh": "品牌A", | |
| 45 | - "vendor.en": "Brand A", | |
| 46 | - "min_price": 199.99, | |
| 47 | - "max_price": 299.99, | |
| 48 | - "option1_name": "color", | |
| 49 | - "option2_name": "size", | |
| 50 | - "specifications": [ | |
| 51 | - { | |
| 52 | - "sku_id": "456", | |
| 53 | - "name": "color", | |
| 54 | - "value": "black" | |
| 55 | - }, | |
| 56 | - { | |
| 57 | - "sku_id": "456", | |
| 58 | - "name": "size", | |
| 59 | - "value": "large" | |
| 60 | - } | |
| 61 | - ], | |
| 62 | - "skus": [ | |
| 63 | - { | |
| 64 | - "sku_id": "456", | |
| 65 | - "price": 199.99, | |
| 66 | - "compare_at_price": 249.99, | |
| 67 | - "sku_code": "SKU-123-1", | |
| 68 | - "stock": 50, | |
| 69 | - "weight": 0.2, | |
| 70 | - "weight_unit": "kg", | |
| 71 | - "option1_value": "black", | |
| 72 | - "option2_value": "large", | |
| 73 | - "option3_value": null, | |
| 74 | - "image_src": "https://example.com/image.jpg" | |
| 75 | - } | |
| 76 | - ], | |
| 77 | - "title_embedding": [0.1, 0.2, ...], // 1024维向量 | |
| 78 | - "image_embedding": [ | |
| 79 | - { | |
| 80 | - "vector": [0.1, 0.2, ...], // 1024维向量 | |
| 81 | - "url": "https://example.com/image.jpg" | |
| 82 | - } | |
| 83 | - ] | |
| 84 | -} | |
| 85 | -``` | |
| 86 | - | |
| 87 | -### 1.3 索引结构简化方案 | |
| 88 | - | |
| 89 | -**简化原则**: | |
| 90 | -- **硬编码映射**:ES mapping 结构直接定义在 JSON 文件中(`mappings/search_products.json`),所有租户索引共享相同结构 | |
| 91 | -- **分索引 + 公共结构**:每个租户拥有独立索引,但索引结构一致 | |
| 92 | -- **数据源统一**:所有租户使用相同的 MySQL 表结构(店匠标准表) | |
| 93 | -- **查询配置集中**:查询相关配置(字段 boost、查询域等)集中在配置文件中管理 | |
| 94 | - | |
| 95 | -**索引结构特点**: | |
| 96 | -1. **多语言字段**:所有文本字段支持中英文(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`) | |
| 97 | -2. **嵌套字段**: | |
| 98 | - - `skus`: SKU 嵌套数组(包含价格、库存、选项值等) | |
| 99 | - - `specifications`: 规格嵌套数组(包含 name、value、sku_id) | |
| 100 | - - `image_embedding`: 图片向量嵌套数组 | |
| 101 | -3. **扁平化字段**:`sku_prices`, `sku_weights`, `total_inventory` 等用于过滤和排序 | |
| 102 | -4. **向量字段**:`title_embedding`(1024维)用于语义搜索 | |
| 103 | - | |
| 104 | -**实现文件**: | |
| 105 | -- `mappings/search_products.json` - ES mapping 定义(硬编码) | |
| 106 | -- `search/query_config.py` - 查询配置(硬编码) | |
| 107 | -- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引 | |
| 108 | - | |
| 109 | ---- | |
| 110 | - | |
| 111 | -## 2. 索引结构实现 | |
| 112 | - | |
| 113 | -### 2.1 硬编码映射方案 | |
| 114 | - | |
| 115 | -**实现方式**: | |
| 116 | -- ES mapping 直接定义在 `mappings/search_products.json` 文件中 | |
| 117 | -- 所有租户索引共享相同的 mapping 结构 | |
| 118 | -- 查询配置集中在配置系统中(如 `config/config.yaml` 等) | |
| 119 | - | |
| 120 | -**索引字段结构**: | |
| 121 | - | |
| 122 | -#### 基础字段 | |
| 123 | -- `tenant_id` (keyword): 租户ID,用于数据隔离 | |
| 124 | -- `spu_id` (keyword): SPU唯一标识 | |
| 125 | -- `create_time`, `update_time` (date): 创建和更新时间 | |
| 126 | - | |
| 127 | -#### 多语言文本字段 | |
| 128 | -- `title.zh/en` (text): 标题(中英文) | |
| 129 | -- `brief.zh/en` (text): 短描述(中英文) | |
| 130 | -- `description.zh/en` (text): 详细描述(中英文) | |
| 131 | -- `vendor.zh/en` (text): 供应商/品牌(中英文) | |
| 132 | -- `category_path.zh/en` (text): 类目路径(中英文) | |
| 133 | -- `category_name_text.zh/en` (text): 类目名称(中英文) | |
| 134 | - | |
| 135 | -**分析器配置**: | |
| 136 | -- 中文字段:`hanlp_index`(索引时)/ `hanlp_standard`(查询时) | |
| 137 | -- 英文字段:`english` | |
| 138 | -- `vendor` 字段包含 `keyword` 子字段(normalizer: lowercase) | |
| 139 | - | |
| 140 | -#### 分类字段 | |
| 141 | -- `category_id` (keyword): 类目ID | |
| 142 | -- `category_name` (keyword): 类目名称 | |
| 143 | -- `category_level` (integer): 类目层级 | |
| 144 | -- `category1_name`, `category2_name`, `category3_name` (keyword): 多级类目名称 | |
| 145 | - | |
| 146 | -#### 规格字段(Specifications) | |
| 147 | -- `specifications` (nested): 规格嵌套数组 | |
| 148 | - - `sku_id` (keyword): SKU ID | |
| 149 | - - `name` (keyword): 规格名称(如 "color", "size") | |
| 150 | - - `value` (keyword): 规格值(如 "white", "256GB") | |
| 151 | - | |
| 152 | -**用途**: | |
| 153 | -- 支持按规格过滤:`{"specifications": {"name": "color", "value": "white"}}` | |
| 154 | -- 支持规格分面:`["specifications"]` 或 `["specifications.color"]` | |
| 155 | - | |
| 156 | -#### SKU嵌套字段 | |
| 157 | -- `skus` (nested): SKU嵌套数组 | |
| 158 | - - `sku_id`, `price`, `compare_at_price`, `sku_code` | |
| 159 | - - `stock`, `weight`, `weight_unit` | |
| 160 | - - `option1_value`, `option2_value`, `option3_value` | |
| 161 | - - `image_src` (index: false) | |
| 162 | - | |
| 163 | -#### 选项名称字段 | |
| 164 | -- `option1_name`, `option2_name`, `option3_name` (keyword): 选项名称(如 "color", "size") | |
| 165 | - | |
| 166 | -#### 扁平化字段 | |
| 167 | -- `min_price`, `max_price`, `compare_at_price` (float): 价格字段 | |
| 168 | -- `sku_prices` (float[]): 所有SKU价格数组 | |
| 169 | -- `sku_weights` (long[]): 所有SKU重量数组 | |
| 170 | -- `total_inventory` (long): 总库存 | |
| 171 | - | |
| 172 | -#### 向量字段 | |
| 173 | -- `title_embedding` (dense_vector, 1024维): 标题向量,用于语义搜索 | |
| 174 | -- `image_embedding` (nested): 图片向量数组 | |
| 175 | - - `vector` (dense_vector, 1024维) | |
| 176 | - - `url` (text) | |
| 177 | - | |
| 178 | -**实现模块**: | |
| 179 | -- `mappings/search_products.json` - ES mapping 定义 | |
| 180 | -- `indexer/mapping_generator.py` - 加载 JSON mapping 并创建索引 | |
| 181 | -- `search/query_config.py` - 查询配置(字段 boost、查询域等) | |
| 182 | - | |
| 183 | -### 2.2 索引结构配置(查询域配置) | |
| 184 | - | |
| 185 | -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 | |
| 186 | - | |
| 187 | -**实现情况**: | |
| 188 | - | |
| 189 | -#### 域(Domain)配置 | |
| 190 | -每个域定义了: | |
| 191 | -- 域名称(如 `default`, `title`, `category`, `brand`) | |
| 192 | -- 域标签(中文描述) | |
| 193 | -- 搜索字段列表 | |
| 194 | -- 默认分析器 | |
| 195 | -- 权重(boost) | |
| 196 | -- **多语言字段映射**(`language_field_mapping`) | |
| 197 | - | |
| 198 | -#### 多语言字段映射 | |
| 199 | - | |
| 200 | -支持将不同语言的查询路由到对应的字段: | |
| 201 | - | |
| 202 | -```yaml | |
| 203 | -indexes: | |
| 204 | - - name: "default" | |
| 205 | - label: "默认索引" | |
| 206 | - fields: | |
| 207 | - - "name" | |
| 208 | - - "enSpuName" | |
| 209 | - - "ruSkuName" | |
| 210 | - - "categoryName" | |
| 211 | - - "brandName" | |
| 212 | - analyzer: "chinese_ecommerce" | |
| 213 | - boost: 1.0 | |
| 214 | - language_field_mapping: | |
| 215 | - zh: | |
| 216 | - - "name" | |
| 217 | - - "categoryName" | |
| 218 | - - "brandName" | |
| 219 | - en: | |
| 220 | - - "enSpuName" | |
| 221 | - ru: | |
| 222 | - - "ruSkuName" | |
| 223 | - | |
| 224 | - - name: "title" | |
| 225 | - label: "标题索引" | |
| 226 | - fields: | |
| 227 | - - "name" | |
| 228 | - - "enSpuName" | |
| 229 | - - "ruSkuName" | |
| 230 | - analyzer: "chinese_ecommerce" | |
| 231 | - boost: 2.0 | |
| 232 | - language_field_mapping: | |
| 233 | - zh: | |
| 234 | - - "name" | |
| 235 | - en: | |
| 236 | - - "enSpuName" | |
| 237 | - ru: | |
| 238 | - - "ruSkuName" | |
| 239 | -``` | |
| 240 | - | |
| 241 | -**工作原理**: | |
| 242 | -1. 检测查询语言(中文、英文、俄文等) | |
| 243 | -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 | |
| 244 | -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 | |
| 245 | -4. 组合多个语言查询的结果,提高召回率 | |
| 246 | - | |
| 247 | -**实现模块**: | |
| 248 | -- `search/es_query_builder.py` - ES 查询构建器(单层架构) | |
| 249 | -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) | |
| 250 | -- `search/query_config.py` - 查询配置(字段 boost、查询域等) | |
| 251 | - | |
| 252 | ---- | |
| 253 | - | |
| 254 | -## 3. 数据导入流程 | |
| 255 | - | |
| 256 | -### 3.1 数据源 | |
| 257 | - | |
| 258 | -**店匠标准表**(Base配置使用): | |
| 259 | -- `shoplazza_product_spu` - SPU级别商品数据 | |
| 260 | -- `shoplazza_product_sku` - SKU级别商品数据 | |
| 261 | - | |
| 262 | -**其他客户表**(tenant1等): | |
| 263 | -- 使用各自的数据源表和扩展表 | |
| 264 | - | |
| 265 | -### 3.2 数据导入方式 | |
| 266 | - | |
| 267 | -**数据源统一**: | |
| 268 | -- 所有租户使用相同的MySQL表结构(店匠标准表) | |
| 269 | -- 数据转换逻辑写死在转换器代码中 | |
| 270 | -- 索引结构硬编码,不依赖配置 | |
| 271 | - | |
| 272 | -#### 数据导入流程(店匠通用) | |
| 273 | - | |
| 274 | -**脚本**:`scripts/ingest_shoplazza.py` | |
| 275 | - | |
| 276 | -**数据流程**: | |
| 277 | -1. **数据加载**: | |
| 278 | - - 从MySQL读取`shoplazza_product_spu`表(SPU数据) | |
| 279 | - - 从MySQL读取`shoplazza_product_sku`表(SKU数据) | |
| 280 | - - 从MySQL读取`shoplazza_product_option`表(选项定义) | |
| 281 | - | |
| 282 | -2. **数据转换**(`indexer/spu_transformer.py`): | |
| 283 | - - 按`spu_id`和`tenant_id`关联SPU和SKU数据 | |
| 284 | - - **多语言字段映射**: | |
| 285 | - - MySQL的`title` → ES的`title.zh`(英文字段设为空) | |
| 286 | - - 其他文本字段类似处理 | |
| 287 | - - **分类字段映射**: | |
| 288 | - - 从SPU表的`category_path`解析多级类目(`category1_name`, `category2_name`, `category3_name`) | |
| 289 | - - 映射`category_id`, `category_name`, `category_level` | |
| 290 | - - **规格字段构建**(`specifications`): | |
| 291 | - - 从`shoplazza_product_option`表获取选项名称(`name`) | |
| 292 | - - 从SKU的`option1/2/3`字段获取选项值(`value`) | |
| 293 | - - 构建嵌套数组:`[{"sku_id": "...", "name": "color", "value": "white"}, ...]` | |
| 294 | - - **选项名称映射**: | |
| 295 | - - 从`shoplazza_product_option`表获取`option1_name`, `option2_name`, `option3_name` | |
| 296 | - - **SKU嵌套数组构建**: | |
| 297 | - - 包含所有SKU字段(价格、库存、选项值、图片等) | |
| 298 | - - **扁平化字段计算**: | |
| 299 | - - `min_price`, `max_price`: 从所有SKU价格计算 | |
| 300 | - - `sku_prices`: 所有SKU价格数组 | |
| 301 | - - `total_inventory`: SKU库存总和 | |
| 302 | - - 注入`tenant_id`字段 | |
| 303 | - | |
| 304 | -3. **索引创建**: | |
| 305 | - - 从`mappings/search_products.json`加载ES mapping | |
| 306 | - - 创建或更新`search_products`索引 | |
| 307 | - | |
| 308 | -4. **批量入库**: | |
| 309 | - - 批量写入ES(默认每批500条) | |
| 310 | - - 错误处理和重试机制 | |
| 311 | - | |
| 312 | -**命令行工具**: | |
| 313 | -```bash | |
| 314 | -python scripts/ingest_shoplazza.py \ | |
| 315 | - --db-host localhost \ | |
| 316 | - --db-port 3306 \ | |
| 317 | - --db-database saas \ | |
| 318 | - --db-username root \ | |
| 319 | - --db-password password \ | |
| 320 | - --tenant-id "1" \ | |
| 321 | - --config base \ | |
| 322 | - --es-host http://localhost:9200 \ | |
| 323 | - --recreate \ | |
| 324 | - --batch-size 500 | |
| 325 | -``` | |
| 326 | - | |
| 327 | -#### 其他客户数据导入 | |
| 328 | - | |
| 329 | -- 使用各自的数据转换器(如`indexer/data_transformer.py`) | |
| 330 | -- 数据源映射逻辑写死在各自的转换器中 | |
| 331 | -- 共享`search_products`索引,通过`tenant_id`隔离 | |
| 332 | - | |
| 333 | -**实现模块**: | |
| 334 | -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) | |
| 335 | -- `indexer/data_transformer.py` - 通用数据转换器(其他客户) | |
| 336 | -- `indexer/bulk_indexer.py` - 批量索引器 | |
| 337 | -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 | |
| 338 | - | |
| 339 | ---- | |
| 340 | - | |
| 341 | -## 4. QueryParser 实现 | |
| 342 | - | |
| 343 | - | |
| 344 | -### 4.1 查询改写(Query Rewriting) | |
| 345 | - | |
| 346 | -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 | |
| 347 | -**实现情况**: | |
| 348 | - | |
| 349 | -#### 配置方式 | |
| 350 | -在 `query_config.rewrite_dictionary` 中配置查询改写规则: | |
| 351 | - | |
| 352 | -```yaml | |
| 353 | -query_config: | |
| 354 | - enable_query_rewrite: true | |
| 355 | - rewrite_dictionary: | |
| 356 | - "芭比": "brand:芭比 OR name:芭比娃娃" | |
| 357 | - "玩具": "category:玩具" | |
| 358 | - "消防": "category:消防 OR name:消防" | |
| 359 | -``` | |
| 360 | - | |
| 361 | -#### 功能特性 | |
| 362 | -- **精确匹配**:查询完全匹配词典 key 时,替换为 value | |
| 363 | -- **部分匹配**:查询包含词典 key 时,替换该部分 | |
| 364 | -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) | |
| 365 | - | |
| 366 | -#### 实现模块 | |
| 367 | -- `query/query_rewriter.py` - 查询改写器 | |
| 368 | -- `query/query_parser.py` - 查询解析器(集成改写功能) | |
| 369 | - | |
| 370 | -### 4.2 翻译(Translation) | |
| 371 | - | |
| 372 | -**实现情况**: | |
| 373 | - | |
| 374 | -#### 配置方式(示意) | |
| 375 | -翻译能力通过统一的 provider 配置管理,支持多种后端实现(如本地服务或外部 API): | |
| 376 | -```yaml | |
| 377 | -query_config: | |
| 378 | - supported_languages: | |
| 379 | - - "zh" | |
| 380 | - - "en" | |
| 381 | - default_language: "en" | |
| 382 | - # 实际翻译 provider 与模型在通用 services 配置中定义 | |
| 383 | -``` | |
| 384 | - | |
| 385 | -实际代码中,翻译已改为统一的 translator service 架构:业务侧通过 `translation.create_translation_client()` 访问 6006,由 `translation/service.py` 在服务内按 `model + scene` 路由到具体 backend。scene 集合、语言码映射、LLM prompt 模板、本地模型方向约束等翻译域知识位于 `translation/` 内部,不再通过外部 provider 抽象分散管理。 | |
| 386 | - | |
| 387 | -#### 功能特性 | |
| 388 | -1. **语言检测**:自动检测查询语言 | |
| 389 | -2. **智能翻译**: | |
| 390 | - - 将源语言 query 翻译到当前租户配置的索引语言集合 | |
| 391 | -3. **域感知翻译**: | |
| 392 | - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 | |
| 393 | - - 避免不必要的翻译,提高效率 | |
| 394 | -4. **翻译缓存**:缓存翻译结果,避免重复调用翻译后端 | |
| 395 | - | |
| 396 | -#### 工作流程 | |
| 397 | -``` | |
| 398 | -查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall)) | |
| 399 | -``` | |
| 400 | - | |
| 401 | -#### 实现模块 | |
| 402 | -- `query/language_detector.py` - 语言检测器 | |
| 403 | -- `query/translator.py` - 翻译 provider 抽象与调用 | |
| 404 | -- `query/query_parser.py` - 查询解析器(集成翻译功能) | |
| 405 | - | |
| 406 | -### 4.3 文本向量化(Text Embedding) | |
| 407 | - | |
| 408 | -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 | |
| 409 | - | |
| 410 | -**实现情况**: | |
| 411 | - | |
| 412 | -#### 配置方式 | |
| 413 | -```yaml | |
| 414 | -query_config: | |
| 415 | - enable_text_embedding: true | |
| 416 | -``` | |
| 417 | - | |
| 418 | -#### 功能特性 | |
| 419 | -1. **条件生成**: | |
| 420 | - - 仅当 `enable_text_embedding=true` 时生成向量 | |
| 421 | - - 仅对 `default` 域查询生成向量 | |
| 422 | -2. **向量模型**:使用可配置的文本向量模型(例如 1024 维通用语义 embedding),具体模型通过配置与 embedding 服务选择,不在文档中固定写死 | |
| 423 | -3. **用途**:用于语义搜索(KNN 检索) | |
| 424 | - | |
| 425 | -#### 实现模块 | |
| 426 | -- `embeddings/text_encoder.py` + `embeddings/server.py` - 文本向量服务客户端与服务端实现(支持可配置的模型后端) | |
| 427 | -- `query/query_parser.py` - 查询解析器(集成向量生成) | |
| 428 | - | |
| 429 | ---- | |
| 430 | - | |
| 431 | -## 5. Searcher 实现 | |
| 432 | - | |
| 433 | -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 | |
| 434 | -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 | |
| 435 | - | |
| 436 | -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 | |
| 437 | - | |
| 438 | -### 5.1 布尔表达式解析 | |
| 439 | - | |
| 440 | -**实现情况**: | |
| 441 | - | |
| 442 | -#### 支持的运算符 | |
| 443 | -- **AND**:所有项必须匹配 | |
| 444 | -- **OR**:任意项匹配 | |
| 445 | -- **RANK**:排序增强(类似 OR 但影响排序) | |
| 446 | -- **ANDNOT**:排除(第一项匹配,第二项不匹配) | |
| 447 | -- **()**:括号分组 | |
| 448 | - | |
| 449 | -#### 优先级(从高到低) | |
| 450 | -1. `()` - 括号 | |
| 451 | -2. `ANDNOT` - 排除 | |
| 452 | -3. `AND` - 与 | |
| 453 | -4. `OR` - 或 | |
| 454 | -5. `RANK` - 排序 | |
| 455 | - | |
| 456 | -#### 示例 | |
| 457 | -``` | |
| 458 | -laptop AND (gaming OR professional) ANDNOT cheap | |
| 459 | -``` | |
| 460 | - | |
| 461 | -#### 实现模块 | |
| 462 | -- `search/boolean_parser.py` - 布尔表达式解析器 | |
| 463 | -- `search/searcher.py` - 搜索器(集成布尔解析) | |
| 464 | - | |
| 465 | -### 5.2 多语言搜索 | |
| 466 | - | |
| 467 | -**实现情况**: | |
| 468 | - | |
| 469 | -#### 工作原理 | |
| 470 | -1. **查询解析**: | |
| 471 | - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) | |
| 472 | - - 检测查询语言 | |
| 473 | - - 生成翻译 | |
| 474 | -2. **查询构建**(简化架构): | |
| 475 | - - **结构**: `filters AND (text_recall OR embedding_recall)` | |
| 476 | - - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) | |
| 477 | - - 普通字段过滤:`{"category_name": "手机"}` | |
| 478 | - - 范围过滤:`{"min_price": {"gte": 50, "lte": 200}}` | |
| 479 | - - **Specifications嵌套过滤**: | |
| 480 | - - 单个规格:`{"specifications": {"name": "color", "value": "white"}}` | |
| 481 | - - 多个规格:`{"specifications": [{"name": "color", "value": "white"}, {"name": "size", "value": "256GB"}]}` | |
| 482 | - - 过滤逻辑:不同维度(不同name)是AND关系,相同维度(相同name)的多个值是OR关系 | |
| 483 | - - 使用ES的`nested`查询实现 | |
| 484 | - - **text_recall**: 文本相关性召回 | |
| 485 | - - 同时搜索中英文字段(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`, `tags`) | |
| 486 | - - 使用 `multi_match` 查询,支持字段 boost | |
| 487 | - - 中文字段使用中文分词器,英文字段使用英文分析器 | |
| 488 | - - **embedding_recall**: 向量召回(KNN) | |
| 489 | - - 使用 `title_embedding` 字段进行 KNN 搜索 | |
| 490 | - - ES 自动与文本召回合并(OR逻辑) | |
| 491 | - - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) | |
| 492 | - | |
| 493 | -#### 查询结构示例 | |
| 494 | -```json | |
| 495 | -{ | |
| 496 | - "query": { | |
| 497 | - "bool": { | |
| 498 | - "must": [ | |
| 499 | - { | |
| 500 | - "function_score": { | |
| 501 | - "query": { | |
| 502 | - "multi_match": { | |
| 503 | - "query": "手机", | |
| 504 | - "fields": [ | |
| 505 | - "title.zh^3.0", "title.en^3.0", | |
| 506 | - "brief.zh^1.5", "brief.en^1.5", | |
| 507 | - ... | |
| 508 | - ] | |
| 509 | - } | |
| 510 | - }, | |
| 511 | - "functions": [...] | |
| 512 | - } | |
| 513 | - } | |
| 514 | - ], | |
| 515 | - "filter": [ | |
| 516 | - {"term": {"tenant_id": "2"}}, | |
| 517 | - {"term": {"category_name": "手机"}} | |
| 518 | - ] | |
| 519 | - } | |
| 520 | - }, | |
| 521 | - "knn": { | |
| 522 | - "field": "title_embedding", | |
| 523 | - "query_vector": [...], | |
| 524 | - "k": 50, | |
| 525 | - "boost": 0.2 | |
| 526 | - } | |
| 527 | -} | |
| 528 | -``` | |
| 529 | -> **KNN 自适应策略**:`k`、`num_candidates`、`boost` 会根据 `query_tokens` 动态调整:短查询(≤2 token)减少召回和权重,长查询(≥5 token)增加召回和权重。详见 `docs/相关性检索优化说明.md` 3.6 节。 | |
| 530 | - | |
| 531 | -#### 实现模块 | |
| 532 | -- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法) | |
| 533 | -- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`) | |
| 534 | - | |
| 535 | -### 5.3 相关性计算(Ranking) | |
| 536 | - | |
| 537 | -**实现情况**: | |
| 538 | - | |
| 539 | -#### 当前实现 | |
| 540 | -**公式**:`bm25() + 0.2 * text_embedding_relevance()` | |
| 541 | - | |
| 542 | -- **bm25()**:BM25 文本相关性得分 | |
| 543 | - - 包括多语言打分 | |
| 544 | - - 内部通过配置翻译为多种语言 | |
| 545 | - - 分别到对应的字段搜索 | |
| 546 | - - 中文字段使用中文分词器,英文字段使用英文分词器 | |
| 547 | -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) | |
| 548 | - - 权重:0.2 | |
| 549 | - | |
| 550 | -#### 配置方式 | |
| 551 | -```yaml | |
| 552 | -ranking: | |
| 553 | - expression: "bm25() + 0.2*text_embedding_relevance()" | |
| 554 | - description: "BM25 text relevance combined with semantic embedding similarity" | |
| 555 | -``` | |
| 556 | - | |
| 557 | -#### 扩展性 | |
| 558 | -- 支持表达式配置(未来可扩展) | |
| 559 | -- 支持自定义函数(如 `timeliness()`, `field_value()`) | |
| 560 | - | |
| 561 | -#### 实现模块 | |
| 562 | -- `search/ranking_engine.py` - 排序引擎 | |
| 563 | -- `search/searcher.py` - 搜索器(集成排序功能) | |
| 564 | - | |
| 565 | ---- | |
| 566 | - | |
| 567 | -## 6. 已完成功能总结 | |
| 568 | - | |
| 569 | -### 6.1 配置系统 | |
| 570 | -- ✅ 字段定义配置(类型、分析器、来源表/列) | |
| 571 | -- ✅ 索引域配置(多域查询、多语言映射) | |
| 572 | -- ✅ 查询配置(改写词典、翻译配置) | |
| 573 | -- ✅ 排序配置(表达式配置) | |
| 574 | -- ✅ 配置验证(字段存在性、类型检查、分析器匹配) | |
| 575 | - | |
| 576 | -### 6.2 数据索引 | |
| 577 | -- ✅ 数据转换(字段映射、类型转换) | |
| 578 | -- ✅ 向量生成(文本向量、图片向量) | |
| 579 | -- ✅ 向量缓存(避免重复计算) | |
| 580 | -- ✅ 批量索引(错误处理、重试机制) | |
| 581 | -- ✅ ES mapping 自动生成 | |
| 582 | - | |
| 583 | -### 6.3 查询处理 | |
| 584 | -- ✅ 查询改写(词典配置) | |
| 585 | -- ✅ 语言检测 | |
| 586 | -- ✅ 多语言翻译(DeepL API) | |
| 587 | -- ✅ 文本向量化(Qwen3-Embedding-0.6B) | |
| 588 | -- ✅ 域提取(支持 `domain:query` 语法) | |
| 589 | - | |
| 590 | -### 6.4 搜索功能 | |
| 591 | -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) | |
| 592 | -- ✅ 多语言查询构建(同时搜索中英文字段) | |
| 593 | -- ✅ 语义搜索(KNN 检索) | |
| 594 | -- ✅ 相关性排序(BM25 + 向量相似度) | |
| 595 | -- ✅ 结果聚合(Faceted Search) | |
| 596 | -- ✅ Specifications嵌套过滤(单个和多个规格,按维度分组:不同维度AND,相同维度OR) | |
| 597 | -- ✅ Specifications嵌套分面(所有规格名称和指定规格名称) | |
| 598 | -- ✅ SKU筛选(按维度过滤,应用层实现) | |
| 599 | - | |
| 600 | -### 6.5 API 服务 | |
| 601 | -- ✅ RESTful API(FastAPI) | |
| 602 | -- ✅ 搜索接口(文本搜索、图片搜索) | |
| 603 | -- ✅ 文档查询接口 | |
| 604 | -- ✅ 前端界面(HTML + JavaScript) | |
| 605 | -- ✅ 租户隔离(tenant_id过滤) | |
| 606 | - | |
| 607 | -### 6.6 索引结构(店匠通用) | |
| 608 | -- ✅ SPU级别索引结构 | |
| 609 | -- ✅ 多语言字段支持(中英文) | |
| 610 | -- ✅ 嵌套字段(skus, specifications, image_embedding) | |
| 611 | -- ✅ 规格字段(specifications)支持过滤和分面 | |
| 612 | -- ✅ 扁平化字段(价格、库存等)用于过滤和排序 | |
| 613 | -- ✅ 按租户分索引(索引名通过 `get_tenant_index_name` 生成),各租户索引结构一致 | |
| 614 | -- ✅ 文档内仍包含 `tenant_id` 字段,可用于额外校验或跨索引场景 | |
| 615 | -- ✅ 硬编码映射(mappings/search_products.json) | |
| 616 | -- ✅ 集中查询配置(config/config.yaml 等) | |
| 617 | - | |
| 618 | ---- | |
| 619 | - | |
| 620 | -## 7. 技术栈 | |
| 621 | - | |
| 622 | -- **后端**:Python 3.6+ | |
| 623 | -- **搜索引擎**:Elasticsearch | |
| 624 | -- **数据库**:MySQL(Shoplazza) | |
| 625 | -- **向量服务**:可配置的文本/图像向量模型(通过 embedding 服务与配置选择具体模型) | |
| 626 | -- **翻译服务**:可配置的翻译 provider(通过 translation 服务与配置选择具体后端与模型) | |
| 627 | -- **API 框架**:FastAPI | |
| 628 | -- **前端**:HTML + JavaScript | |
| 629 | - | |
| 630 | ---- | |
| 631 | - | |
| 632 | -## 8. API响应格式 | |
| 633 | - | |
| 634 | -### 8.1 外部友好格式 | |
| 635 | - | |
| 636 | -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: | |
| 637 | - | |
| 638 | -**响应结构**: | |
| 639 | -```json | |
| 640 | -{ | |
| 641 | - "results": [ | |
| 642 | - { | |
| 643 | - "spu_id": "123", | |
| 644 | - "title": "蓝牙耳机", | |
| 645 | - "skus": [ | |
| 646 | - { | |
| 647 | - "sku_id": "456", | |
| 648 | - "price": 199.99, | |
| 649 | - "sku": "SKU-123-1", | |
| 650 | - "stock": 50 | |
| 651 | - } | |
| 652 | - ], | |
| 653 | - "relevance_score": 0.95 | |
| 654 | - } | |
| 655 | - ], | |
| 656 | - "total": 10, | |
| 657 | - "facets": [...], | |
| 658 | - "suggestions": [], | |
| 659 | - "related_searches": [] | |
| 660 | -} | |
| 661 | -``` | |
| 662 | - | |
| 663 | -**主要变化**: | |
| 664 | -- 结构化结果(`SpuResult`和`SkuResult`) | |
| 665 | -- 嵌套skus数组 | |
| 666 | -- 无ES内部字段 | |
| 667 | - | |
| 668 | -### 8.2 租户隔离 | |
| 669 | - | |
| 670 | -所有API请求必须提供`tenant_id`: | |
| 671 | -- 请求头:`X-Tenant-ID: 1` | |
| 672 | -- 或查询参数:`?tenant_id=1` | |
| 673 | - | |
| 674 | -搜索时自动添加`tenant_id`过滤,确保数据隔离。 | |
| 675 | - | |
| 676 | -### 8.3 数据接口约定 | |
| 677 | - | |
| 678 | -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 | |
| 679 | - | |
| 680 | -#### 8.3.1 数据流模式 | |
| 681 | - | |
| 682 | -系统采用统一的数据流模式,确保数据在各层之间的一致性: | |
| 683 | - | |
| 684 | -**数据流转路径**: | |
| 685 | -``` | |
| 686 | -API Request (JSON) | |
| 687 | - ↓ | |
| 688 | -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) | |
| 689 | - ↓ | |
| 690 | -Searcher(透传) | |
| 691 | - ↓ | |
| 692 | -ES Query Builder → model_dump() 转换为字典 | |
| 693 | - ↓ | |
| 694 | -ES Query (字典) | |
| 695 | - ↓ | |
| 696 | -Elasticsearch | |
| 697 | -``` | |
| 698 | - | |
| 699 | -#### 8.3.2 Facets 配置数据流 | |
| 700 | - | |
| 701 | -**输入格式**:`List[FacetConfig]` | |
| 702 | - | |
| 703 | -**配置对象列表**:所有分面配置必须使用 FacetConfig 对象 | |
| 704 | -```json | |
| 705 | -[ | |
| 706 | - { | |
| 707 | - "field": "category1_name", | |
| 708 | - "size": 15, | |
| 709 | - "type": "terms" | |
| 710 | - }, | |
| 711 | - { | |
| 712 | - "field": "specifications.color", | |
| 713 | - "size": 20, | |
| 714 | - "type": "terms" | |
| 715 | - }, | |
| 716 | - { | |
| 717 | - "field": "min_price", | |
| 718 | - "type": "range", | |
| 719 | - "ranges": [ | |
| 720 | - {"key": "0-50", "to": 50}, | |
| 721 | - {"key": "50-100", "from": 50, "to": 100} | |
| 722 | - ] | |
| 723 | - } | |
| 724 | -] | |
| 725 | -``` | |
| 726 | - | |
| 727 | -**Specifications 分面支持**: | |
| 728 | -- 所有规格名称:`field: "specifications"` - 返回所有 name 及其 value 列表 | |
| 729 | -- 指定规格名称:`field: "specifications.color"` - 只返回指定 name 的 value 列表 | |
| 730 | - | |
| 731 | -**数据流**: | |
| 732 | -1. API 层:接收 `List[FacetConfig]`,Pydantic 验证参数 | |
| 733 | -2. Searcher 层:透传 FacetConfig 对象列表 | |
| 734 | -3. ES Query Builder:解析 FacetConfig 对象 | |
| 735 | - - 检测 `"specifications"` 或 `"specifications.{name}"` 格式 | |
| 736 | - - 构建对应的嵌套聚合查询或普通聚合查询 | |
| 737 | -4. 输出:转换为 ES 聚合查询(包括 specifications 嵌套聚合) | |
| 738 | -5. Result Formatter:格式化 ES 聚合结果,处理 specifications 嵌套结构 | |
| 739 | - | |
| 740 | -#### 8.3.3 Range Filters 数据流 | |
| 741 | - | |
| 742 | -**输入格式**:`Dict[str, RangeFilter]` | |
| 743 | - | |
| 744 | -**RangeFilter 模型**: | |
| 745 | -```python | |
| 746 | -class RangeFilter(BaseModel): | |
| 747 | - gte: Optional[Union[float, str]] # 大于等于 | |
| 748 | - gt: Optional[Union[float, str]] # 大于 | |
| 749 | - lte: Optional[Union[float, str]] # 小于等于 | |
| 750 | - lt: Optional[Union[float, str]] # 小于 | |
| 751 | -``` | |
| 752 | - | |
| 753 | -**示例**: | |
| 754 | -```json | |
| 755 | -{ | |
| 756 | - "min_price": {"gte": 50, "lte": 200}, | |
| 757 | - "create_time": {"gte": "2023-01-01T00:00:00Z"} | |
| 758 | -} | |
| 759 | -``` | |
| 760 | - | |
| 761 | -**数据流**: | |
| 762 | -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 | |
| 763 | -2. Searcher 层:透传 `Dict[str, RangeFilter]` | |
| 764 | -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 | |
| 765 | -4. 输出:ES range 查询(支持数值和日期) | |
| 766 | - | |
| 767 | -**特性**: | |
| 768 | -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) | |
| 769 | -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) | |
| 770 | -- 统一约定:所有范围过滤都使用 RangeFilter 模型 | |
| 771 | - | |
| 772 | -#### 8.3.3.1 Specifications 过滤数据流 | |
| 773 | - | |
| 774 | -**输入格式**:`Dict[str, Union[Dict[str, str], List[Dict[str, str]]]]` | |
| 775 | - | |
| 776 | -**单个规格过滤**: | |
| 777 | -```json | |
| 778 | -{ | |
| 779 | - "specifications": { | |
| 780 | - "name": "color", | |
| 781 | - "value": "white" | |
| 782 | - } | |
| 783 | -} | |
| 784 | -``` | |
| 785 | - | |
| 786 | -**多个规格过滤(按维度分组)**: | |
| 787 | -```json | |
| 788 | -{ | |
| 789 | - "specifications": [ | |
| 790 | - {"name": "color", "value": "white"}, | |
| 791 | - {"name": "size", "value": "256GB"} | |
| 792 | - ] | |
| 793 | -} | |
| 794 | -``` | |
| 795 | - | |
| 796 | -**数据流**: | |
| 797 | -1. API 层:接收 `filters` 字典,检测 `specifications` 键 | |
| 798 | -2. Searcher 层:透传 `filters` 字典 | |
| 799 | -3. ES Query Builder:检测 `specifications` 键,构建ES `nested` 查询 | |
| 800 | - - 单个规格:构建单个 `nested` 查询 | |
| 801 | - - 多个规格:按 name 维度分组,相同维度内使用 `should` 组合(OR逻辑),不同维度之间使用 `must` 组合(AND逻辑) | |
| 802 | -4. 输出:ES nested 查询(`nested.path=specifications` + `bool.must=[term(name), term(value)]`) | |
| 803 | - | |
| 804 | -#### 8.3.4 响应 Facets 数据流 | |
| 805 | - | |
| 806 | -**输出格式**:`List[FacetResult]` | |
| 807 | - | |
| 808 | -**FacetResult 模型**: | |
| 809 | -```python | |
| 810 | -class FacetResult(BaseModel): | |
| 811 | - field: str # 字段名 | |
| 812 | - label: str # 显示标签 | |
| 813 | - type: Literal["terms", "range"] # 分面类型 | |
| 814 | - values: List[FacetValue] # 分面值列表 | |
| 815 | - total_count: Optional[int] # 总文档数 | |
| 816 | -``` | |
| 817 | - | |
| 818 | -**数据流**: | |
| 819 | -1. ES Response:返回聚合结果(字典格式,包括specifications嵌套聚合) | |
| 820 | -2. Result Formatter:格式化ES聚合结果 | |
| 821 | - - 处理普通terms聚合 | |
| 822 | - - 处理range聚合 | |
| 823 | - - **处理specifications嵌套聚合**: | |
| 824 | - - 所有规格名称:解析 `by_name` 聚合结构 | |
| 825 | - - 指定规格名称:解析 `filter_by_name` 聚合结构 | |
| 826 | -3. Searcher 层:构建 `List[FacetResult]` 对象 | |
| 827 | -4. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) | |
| 828 | - | |
| 829 | -**优势**: | |
| 830 | -- 类型安全:使用 Pydantic 模型确保数据结构一致性 | |
| 831 | -- 自动序列化:模型自动转换为 JSON,无需手动处理 | |
| 832 | -- 统一约定:所有响应都使用标准化的 Pydantic 模型 | |
| 833 | - | |
| 834 | -#### 8.3.5 SKU筛选数据流 | |
| 835 | - | |
| 836 | -**输入格式**:`Optional[str]` | |
| 837 | - | |
| 838 | -**支持的维度值**: | |
| 839 | -- `option1`, `option2`, `option3`: 直接使用选项字段 | |
| 840 | -- 规格名称(如 `color`, `size`): 通过 `option1_name`、`option2_name`、`option3_name` 匹配 | |
| 841 | - | |
| 842 | -**示例**: | |
| 843 | -```json | |
| 844 | -{ | |
| 845 | - "query": "手机", | |
| 846 | - "sku_filter_dimension": "color" | |
| 847 | -} | |
| 848 | -``` | |
| 849 | - | |
| 850 | -**数据流**: | |
| 851 | -1. API 层:接收 `sku_filter_dimension` 字符串参数 | |
| 852 | -2. Searcher 层:透传到 Result Formatter | |
| 853 | -3. Result Formatter:在格式化结果时,按指定维度对SKU进行分组 | |
| 854 | - - 如果维度是 `option1/2/3`,直接使用对应的 `option1_value/2/3` 字段 | |
| 855 | - - 如果维度是规格名称,通过 `option1_name/2/3` 匹配找到对应的 `option1_value/2/3` | |
| 856 | - - 每个分组选择第一个SKU返回 | |
| 857 | -4. 输出:过滤后的SKU列表(每个维度值一个SKU) | |
| 858 | - | |
| 859 | -**工作原理**: | |
| 860 | -1. 系统从ES返回所有SKU(不改变ES查询,保持性能) | |
| 861 | -2. 在结果格式化阶段,按指定维度对SKU进行分组 | |
| 862 | -3. 每个分组选择第一个SKU返回 | |
| 863 | -4. 如果维度不匹配或未找到,返回所有SKU(不进行过滤) | |
| 864 | - | |
| 865 | -**性能说明**: | |
| 866 | -- ✅ **推荐方案**: 在应用层过滤(当前实现) | |
| 867 | - - ES查询简单,不需要nested查询和join | |
| 868 | - - 只对返回的结果(通常10-20个SPU)进行过滤,数据量小 | |
| 869 | - - 实现简单,性能开销小 | |
| 870 | -- ❌ **不推荐**: 在ES查询时过滤 | |
| 871 | - - 需要nested查询和join,性能开销大 | |
| 872 | - - 实现复杂 | |
| 873 | - - 只对返回的结果需要过滤,不需要在ES层面过滤 | |
| 874 | - | |
| 875 | -#### 8.3.6 统一约定的好处 | |
| 876 | - | |
| 877 | -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 | |
| 878 | -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 | |
| 879 | -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) | |
| 880 | -4. **易于维护**:修改数据结构只需更新模型定义 | |
| 881 | -5. **数据验证**:自动验证输入数据,减少错误处理代码 | |
| 882 | - | |
| 883 | -**实现模块**: | |
| 884 | -- `api/models.py` - 所有 Pydantic 模型定义(包括 `SearchRequest`, `FacetConfig`, `RangeFilter` 等) | |
| 885 | -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型,包括specifications分面处理和SKU筛选) | |
| 886 | -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询,包括specifications过滤和分面) | |
| 887 | - | |
| 888 | -## 9. 索引结构文件 | |
| 889 | - | |
| 890 | -**硬编码映射**(店匠通用):`mappings/search_products.json` | |
| 891 | - | |
| 892 | -**查询配置**(硬编码):`search/query_config.py` | |
| 893 | - | |
| 894 | ---- |
docs/系统设计文档v1.md deleted
| ... | ... | @@ -1,743 +0,0 @@ |
| 1 | -# 搜索引擎通用化开发进度 | |
| 2 | - | |
| 3 | -> 历史版本说明:本文件为 v1 归档文档,部分模型与实现细节(如 BGE-M3)已过时。当前以 `docs/系统设计文档.md` 与 `docs/QUICKSTART.md` 为准。 | |
| 4 | - | |
| 5 | -## 项目概述 | |
| 6 | - | |
| 7 | -对后端搜索技术 做通用化。 | |
| 8 | -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 | |
| 9 | - | |
| 10 | - | |
| 11 | -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 | |
| 12 | - | |
| 13 | ---- | |
| 14 | - | |
| 15 | -## 1. 原始数据层的约定 | |
| 16 | - | |
| 17 | -### 1.1 店匠主表 | |
| 18 | - | |
| 19 | -所有租户共用以下主表: | |
| 20 | -- `shoplazza_product_sku` - SKU级别商品数据 | |
| 21 | -- `shoplazza_product_spu` - SPU级别商品数据 | |
| 22 | - | |
| 23 | -### 1.2 索引结构(SPU维度) | |
| 24 | - | |
| 25 | -**统一索引架构**: | |
| 26 | -- 所有客户共享同一个Elasticsearch索引:`search_products` | |
| 27 | -- 索引粒度:SPU级别(每个文档代表一个SPU) | |
| 28 | -- 数据隔离:通过`tenant_id`字段实现租户隔离 | |
| 29 | -- 嵌套结构:每个SPU文档包含嵌套的`skus`数组 | |
| 30 | - | |
| 31 | -**索引文档结构**: | |
| 32 | -```json | |
| 33 | -{ | |
| 34 | - "tenant_id": "1", | |
| 35 | - "spu_id": "123", | |
| 36 | - "title": "蓝牙耳机", | |
| 37 | - "skus": [ | |
| 38 | - { | |
| 39 | - "sku_id": "456", | |
| 40 | - "title": "黑色", | |
| 41 | - "price": 199.99, | |
| 42 | - "sku": "SKU-123-1", | |
| 43 | - "stock": 50 | |
| 44 | - } | |
| 45 | - ], | |
| 46 | - "min_price": 199.99, | |
| 47 | - "max_price": 299.99 | |
| 48 | -} | |
| 49 | -``` | |
| 50 | - | |
| 51 | -### 1.3 配置化方案 | |
| 52 | - | |
| 53 | -**配置分离原则**: | |
| 54 | -- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置 | |
| 55 | -- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定 | |
| 56 | -- **数据导入流程**:写死的脚本,不依赖配置 | |
| 57 | - | |
| 58 | -统一通过配置文件定义: | |
| 59 | -1. ES 字段定义(字段类型、分析器、boost等) | |
| 60 | -2. ES mapping 结构生成 | |
| 61 | -3. 查询域配置(indexes) | |
| 62 | -4. 排序和打分配置(function_score) | |
| 63 | - | |
| 64 | -**注意**:配置中**不包含**以下内容: | |
| 65 | -- `mysql_config` - MySQL数据库配置 | |
| 66 | -- `main_table` / `extension_table` - 数据表配置 | |
| 67 | -- `source_table` / `source_column` - 字段数据源映射 | |
| 68 | - | |
| 69 | ---- | |
| 70 | - | |
| 71 | -## 2. 配置系统实现 | |
| 72 | - | |
| 73 | -### 2.1 应用结构配置(字段定义) | |
| 74 | - | |
| 75 | -**配置文件位置**:`config/schema/{tenant_id}_config.yaml` | |
| 76 | - | |
| 77 | -**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。 | |
| 78 | - | |
| 79 | -**实现情况**: | |
| 80 | - | |
| 81 | -#### 字段类型支持 | |
| 82 | -- **TEXT**:文本字段,支持多语言分析器 | |
| 83 | -- **KEYWORD**:关键词字段,用于精确匹配和聚合 | |
| 84 | -- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度) | |
| 85 | -- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度) | |
| 86 | -- **INT/LONG**:整数类型 | |
| 87 | -- **FLOAT/DOUBLE**:浮点数类型 | |
| 88 | -- **DATE**:日期类型 | |
| 89 | -- **BOOLEAN**:布尔类型 | |
| 90 | - | |
| 91 | -#### 分析器支持 | |
| 92 | -- **chinese_ecommerce**:中文电商分词器(index_ik/query_ik) | |
| 93 | -- **english**:英文分析器 | |
| 94 | -- **russian**:俄文分析器 | |
| 95 | -- **arabic**:阿拉伯文分析器 | |
| 96 | -- **spanish**:西班牙文分析器 | |
| 97 | -- **japanese**:日文分析器 | |
| 98 | -- **standard**:标准分析器 | |
| 99 | -- **keyword**:关键词分析器 | |
| 100 | - | |
| 101 | -#### 字段配置示例(Base配置) | |
| 102 | - | |
| 103 | -```yaml | |
| 104 | -fields: | |
| 105 | - # 租户隔离字段(必需) | |
| 106 | - - name: "tenant_id" | |
| 107 | - type: "KEYWORD" | |
| 108 | - required: true | |
| 109 | - index: true | |
| 110 | - store: true | |
| 111 | - | |
| 112 | - # 商品标识字段 | |
| 113 | - - name: "spu_id" | |
| 114 | - type: "KEYWORD" | |
| 115 | - required: true | |
| 116 | - index: true | |
| 117 | - store: true | |
| 118 | - | |
| 119 | - # 文本搜索字段 | |
| 120 | - - name: "title" | |
| 121 | - type: "TEXT" | |
| 122 | - analyzer: "chinese_ecommerce" | |
| 123 | - boost: 3.0 | |
| 124 | - index: true | |
| 125 | - store: true | |
| 126 | - | |
| 127 | - - name: "seo_keywords" | |
| 128 | - type: "TEXT" | |
| 129 | - analyzer: "chinese_ecommerce" | |
| 130 | - boost: 2.0 | |
| 131 | - index: true | |
| 132 | - store: true | |
| 133 | - | |
| 134 | - # 嵌套skus字段 | |
| 135 | - - name: "skus" | |
| 136 | - type: "JSON" | |
| 137 | - nested: true | |
| 138 | - nested_properties: | |
| 139 | - sku_id: | |
| 140 | - type: "keyword" | |
| 141 | - price: | |
| 142 | - type: "float" | |
| 143 | - sku: | |
| 144 | - type: "keyword" | |
| 145 | -``` | |
| 146 | - | |
| 147 | -**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。 | |
| 148 | - | |
| 149 | -**实现模块**: | |
| 150 | -- `config/config_loader.py` - 配置加载器 | |
| 151 | -- `config/field_types.py` - 字段类型定义 | |
| 152 | -- `indexer/mapping_generator.py` - ES mapping 生成器 | |
| 153 | -- `indexer/data_transformer.py` - 数据转换器 | |
| 154 | - | |
| 155 | -### 2.2 索引结构配置(查询域配置) | |
| 156 | - | |
| 157 | -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 | |
| 158 | - | |
| 159 | -**实现情况**: | |
| 160 | - | |
| 161 | -#### 域(Domain)配置 | |
| 162 | -每个域定义了: | |
| 163 | -- 域名称(如 `default`, `title`, `category`, `brand`) | |
| 164 | -- 域标签(中文描述) | |
| 165 | -- 搜索字段列表 | |
| 166 | -- 默认分析器 | |
| 167 | -- 权重(boost) | |
| 168 | -- **多语言字段映射**(`language_field_mapping`) | |
| 169 | - | |
| 170 | -#### 多语言字段映射 | |
| 171 | - | |
| 172 | -支持将不同语言的查询路由到对应的字段: | |
| 173 | - | |
| 174 | -```yaml | |
| 175 | -indexes: | |
| 176 | - - name: "default" | |
| 177 | - label: "默认索引" | |
| 178 | - fields: | |
| 179 | - - "name" | |
| 180 | - - "enSpuName" | |
| 181 | - - "ruSkuName" | |
| 182 | - - "categoryName" | |
| 183 | - - "brandName" | |
| 184 | - analyzer: "chinese_ecommerce" | |
| 185 | - boost: 1.0 | |
| 186 | - language_field_mapping: | |
| 187 | - zh: | |
| 188 | - - "name" | |
| 189 | - - "categoryName" | |
| 190 | - - "brandName" | |
| 191 | - en: | |
| 192 | - - "enSpuName" | |
| 193 | - ru: | |
| 194 | - - "ruSkuName" | |
| 195 | - | |
| 196 | - - name: "title" | |
| 197 | - label: "标题索引" | |
| 198 | - fields: | |
| 199 | - - "name" | |
| 200 | - - "enSpuName" | |
| 201 | - - "ruSkuName" | |
| 202 | - analyzer: "chinese_ecommerce" | |
| 203 | - boost: 2.0 | |
| 204 | - language_field_mapping: | |
| 205 | - zh: | |
| 206 | - - "name" | |
| 207 | - en: | |
| 208 | - - "enSpuName" | |
| 209 | - ru: | |
| 210 | - - "ruSkuName" | |
| 211 | -``` | |
| 212 | - | |
| 213 | -**工作原理**: | |
| 214 | -1. 检测查询语言(中文、英文、俄文等) | |
| 215 | -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 | |
| 216 | -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 | |
| 217 | -4. 组合多个语言查询的结果,提高召回率 | |
| 218 | - | |
| 219 | -**实现模块**: | |
| 220 | -- `search/es_query_builder.py` - ES 查询构建器(单层架构) | |
| 221 | -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) | |
| 222 | - | |
| 223 | ---- | |
| 224 | - | |
| 225 | -## 3. 数据导入流程 | |
| 226 | - | |
| 227 | -### 3.1 数据源 | |
| 228 | - | |
| 229 | -**店匠标准表**(Base配置使用): | |
| 230 | -- `shoplazza_product_spu` - SPU级别商品数据 | |
| 231 | -- `shoplazza_product_sku` - SKU级别商品数据 | |
| 232 | - | |
| 233 | -**其他客户表**(tenant1等): | |
| 234 | -- 使用各自的数据源表和扩展表 | |
| 235 | - | |
| 236 | -### 3.2 数据导入方式 | |
| 237 | - | |
| 238 | -**Pipeline层决定数据源**: | |
| 239 | -- 数据导入流程是写死的脚本,不依赖配置 | |
| 240 | -- 配置只关注ES搜索相关的内容 | |
| 241 | -- 数据源映射逻辑写死在转换器代码中 | |
| 242 | - | |
| 243 | -#### Base配置数据导入(店匠通用) | |
| 244 | - | |
| 245 | -**脚本**:`scripts/ingest_shoplazza.py` | |
| 246 | - | |
| 247 | -**数据流程**: | |
| 248 | -1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表 | |
| 249 | -2. **数据转换**(`indexer/spu_transformer.py`): | |
| 250 | - - 按`spu_id`和`tenant_id`关联SPU和SKU数据 | |
| 251 | - - 将SKU数据聚合为嵌套的`skus`数组 | |
| 252 | - - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`) | |
| 253 | - - 字段映射(写死在代码中,不依赖配置) | |
| 254 | - - 注入`tenant_id`字段 | |
| 255 | -3. **索引创建**: | |
| 256 | - - 根据配置生成ES mapping | |
| 257 | - - 创建或更新`search_products`索引 | |
| 258 | -4. **批量入库**: | |
| 259 | - - 批量写入ES(默认每批500条) | |
| 260 | - - 错误处理和重试机制 | |
| 261 | - | |
| 262 | -**命令行工具**: | |
| 263 | -```bash | |
| 264 | -python scripts/ingest_shoplazza.py \ | |
| 265 | - --db-host localhost \ | |
| 266 | - --db-port 3306 \ | |
| 267 | - --db-database saas \ | |
| 268 | - --db-username root \ | |
| 269 | - --db-password password \ | |
| 270 | - --tenant-id "1" \ | |
| 271 | - --config base \ | |
| 272 | - --es-host http://localhost:9200 \ | |
| 273 | - --recreate \ | |
| 274 | - --batch-size 500 | |
| 275 | -``` | |
| 276 | - | |
| 277 | -#### 其他客户数据导入 | |
| 278 | - | |
| 279 | -- 使用各自的数据转换器(如`indexer/data_transformer.py`) | |
| 280 | -- 数据源映射逻辑写死在各自的转换器中 | |
| 281 | -- 共享`search_products`索引,通过`tenant_id`隔离 | |
| 282 | - | |
| 283 | -**实现模块**: | |
| 284 | -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) | |
| 285 | -- `indexer/data_transformer.py` - 通用数据转换器(其他客户) | |
| 286 | -- `indexer/bulk_indexer.py` - 批量索引器 | |
| 287 | -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 | |
| 288 | - | |
| 289 | ---- | |
| 290 | - | |
| 291 | -## 4. QueryParser 实现 | |
| 292 | - | |
| 293 | - | |
| 294 | -### 4.1 查询改写(Query Rewriting) | |
| 295 | - | |
| 296 | -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 | |
| 297 | -**实现情况**: | |
| 298 | - | |
| 299 | -#### 配置方式 | |
| 300 | -在 `query_config.rewrite_dictionary` 中配置查询改写规则: | |
| 301 | - | |
| 302 | -```yaml | |
| 303 | -query_config: | |
| 304 | - enable_query_rewrite: true | |
| 305 | - rewrite_dictionary: | |
| 306 | - "芭比": "brand:芭比 OR name:芭比娃娃" | |
| 307 | - "玩具": "category:玩具" | |
| 308 | - "消防": "category:消防 OR name:消防" | |
| 309 | -``` | |
| 310 | - | |
| 311 | -#### 功能特性 | |
| 312 | -- **精确匹配**:查询完全匹配词典 key 时,替换为 value | |
| 313 | -- **部分匹配**:查询包含词典 key 时,替换该部分 | |
| 314 | -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) | |
| 315 | - | |
| 316 | -#### 实现模块 | |
| 317 | -- `query/query_rewriter.py` - 查询改写器 | |
| 318 | -- `query/query_parser.py` - 查询解析器(集成改写功能) | |
| 319 | - | |
| 320 | -### 4.2 翻译(Translation) | |
| 321 | - | |
| 322 | -**实现情况**: | |
| 323 | - | |
| 324 | -#### 配置方式 | |
| 325 | -```yaml | |
| 326 | -query_config: | |
| 327 | - supported_languages: | |
| 328 | - - "zh" | |
| 329 | - - "en" | |
| 330 | - - "ru" | |
| 331 | - default_language: "zh" | |
| 332 | - translation_service: "deepl" | |
| 333 | - translation_api_key: null # 通过环境变量设置 | |
| 334 | -``` | |
| 335 | - | |
| 336 | -#### 功能特性 | |
| 337 | -1. **语言检测**:自动检测查询语言 | |
| 338 | -2. **智能翻译**: | |
| 339 | - - 如果查询是中文,翻译为英文、俄文 | |
| 340 | - - 如果查询是英文,翻译为中文、俄文 | |
| 341 | - - 如果查询是其他语言,翻译为所有支持的语言 | |
| 342 | -3. **域感知翻译**: | |
| 343 | - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 | |
| 344 | - - 避免不必要的翻译,提高效率 | |
| 345 | -4. **翻译缓存**:缓存翻译结果,避免重复调用 API | |
| 346 | - | |
| 347 | -#### 工作流程 | |
| 348 | -``` | |
| 349 | -查询输入 → 语言检测 → 翻译 → 查询构建(filters and (text_recall or embedding_recall)) | |
| 350 | -``` | |
| 351 | - | |
| 352 | -#### 实现模块 | |
| 353 | -- `query/language_detector.py` - 语言检测器 | |
| 354 | -- `query/translator.py` - 翻译器(DeepL API) | |
| 355 | -- `query/query_parser.py` - 查询解析器(集成翻译功能) | |
| 356 | - | |
| 357 | -### 4.3 文本向量化(Text Embedding) | |
| 358 | - | |
| 359 | -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 | |
| 360 | - | |
| 361 | -**实现情况**: | |
| 362 | - | |
| 363 | -#### 配置方式 | |
| 364 | -```yaml | |
| 365 | -query_config: | |
| 366 | - enable_text_embedding: true | |
| 367 | -``` | |
| 368 | - | |
| 369 | -#### 功能特性 | |
| 370 | -1. **条件生成**: | |
| 371 | - - 仅当 `enable_text_embedding=true` 时生成向量 | |
| 372 | - - 仅对 `default` 域查询生成向量 | |
| 373 | -2. **向量模型**:BGE-M3 模型(1024维向量) | |
| 374 | -3. **用途**:用于语义搜索(KNN 检索) | |
| 375 | - | |
| 376 | -#### 实现模块 | |
| 377 | -- `embeddings/bge_encoder.py` - BGE 文本编码器 | |
| 378 | -- `query/query_parser.py` - 查询解析器(集成向量生成) | |
| 379 | - | |
| 380 | ---- | |
| 381 | - | |
| 382 | -## 5. Searcher 实现 | |
| 383 | - | |
| 384 | -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 | |
| 385 | -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 | |
| 386 | - | |
| 387 | -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 | |
| 388 | - | |
| 389 | -### 5.1 布尔表达式解析 | |
| 390 | - | |
| 391 | -**实现情况**: | |
| 392 | - | |
| 393 | -#### 支持的运算符 | |
| 394 | -- **AND**:所有项必须匹配 | |
| 395 | -- **OR**:任意项匹配 | |
| 396 | -- **RANK**:排序增强(类似 OR 但影响排序) | |
| 397 | -- **ANDNOT**:排除(第一项匹配,第二项不匹配) | |
| 398 | -- **()**:括号分组 | |
| 399 | - | |
| 400 | -#### 优先级(从高到低) | |
| 401 | -1. `()` - 括号 | |
| 402 | -2. `ANDNOT` - 排除 | |
| 403 | -3. `AND` - 与 | |
| 404 | -4. `OR` - 或 | |
| 405 | -5. `RANK` - 排序 | |
| 406 | - | |
| 407 | -#### 示例 | |
| 408 | -``` | |
| 409 | -laptop AND (gaming OR professional) ANDNOT cheap | |
| 410 | -``` | |
| 411 | - | |
| 412 | -#### 实现模块 | |
| 413 | -- `search/boolean_parser.py` - 布尔表达式解析器 | |
| 414 | -- `search/searcher.py` - 搜索器(集成布尔解析) | |
| 415 | - | |
| 416 | -### 5.2 多语言搜索 | |
| 417 | - | |
| 418 | -**实现情况**: | |
| 419 | - | |
| 420 | -#### 工作原理 | |
| 421 | -1. **查询解析**: | |
| 422 | - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) | |
| 423 | - - 检测查询语言 | |
| 424 | - - 生成翻译 | |
| 425 | -2. **查询构建**(简化架构): | |
| 426 | - - **结构**: `filters AND (text_recall OR embedding_recall)` | |
| 427 | - - **filters**: 前端传递的过滤条件(永远起作用,放在 `filter` 中) | |
| 428 | - - **text_recall**: 文本相关性召回 | |
| 429 | - - 同时搜索中英文字段(`title.zh/en`, `brief.zh/en`, `description.zh/en`, `vendor.zh/en`, `category_path.zh/en`, `category_name_text.zh/en`, `tags`) | |
| 430 | - - 使用 `multi_match` 查询,支持字段 boost | |
| 431 | - - **embedding_recall**: 向量召回(KNN) | |
| 432 | - - 使用 `title_embedding` 字段进行 KNN 搜索 | |
| 433 | - - ES 自动与文本召回合并 | |
| 434 | - - **function_score**: 包装召回部分,支持提权字段(新鲜度、销量等) | |
| 435 | - | |
| 436 | -#### 查询结构示例 | |
| 437 | -```json | |
| 438 | -{ | |
| 439 | - "query": { | |
| 440 | - "bool": { | |
| 441 | - "must": [ | |
| 442 | - { | |
| 443 | - "function_score": { | |
| 444 | - "query": { | |
| 445 | - "multi_match": { | |
| 446 | - "query": "手机", | |
| 447 | - "fields": [ | |
| 448 | - "title.zh^3.0", "title.en^3.0", | |
| 449 | - "brief.zh^1.5", "brief.en^1.5", | |
| 450 | - ... | |
| 451 | - ] | |
| 452 | - } | |
| 453 | - }, | |
| 454 | - "functions": [...] | |
| 455 | - } | |
| 456 | - } | |
| 457 | - ], | |
| 458 | - "filter": [ | |
| 459 | - {"term": {"tenant_id": "2"}}, | |
| 460 | - {"term": {"category_name": "手机"}} | |
| 461 | - ] | |
| 462 | - } | |
| 463 | - }, | |
| 464 | - "knn": { | |
| 465 | - "field": "title_embedding", | |
| 466 | - "query_vector": [...], | |
| 467 | - "k": 50, | |
| 468 | - "boost": 0.2 | |
| 469 | - } | |
| 470 | -} | |
| 471 | -``` | |
| 472 | - | |
| 473 | -#### 实现模块 | |
| 474 | -- `search/es_query_builder.py` - ES 查询构建器(单层架构,`build_query` 方法) | |
| 475 | -- `search/searcher.py` - 搜索器(使用 `ESQueryBuilder`) | |
| 476 | - | |
| 477 | -### 5.3 相关性计算(Ranking) | |
| 478 | - | |
| 479 | -**实现情况**: | |
| 480 | - | |
| 481 | -#### 当前实现 | |
| 482 | -**公式**:`bm25() + 0.2 * text_embedding_relevance()` | |
| 483 | - | |
| 484 | -- **bm25()**:BM25 文本相关性得分 | |
| 485 | - - 包括多语言打分 | |
| 486 | - - 内部通过配置翻译为多种语言 | |
| 487 | - - 分别到对应的字段搜索 | |
| 488 | - - 中文字段使用中文分词器,英文字段使用英文分词器 | |
| 489 | -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) | |
| 490 | - - 权重:0.2 | |
| 491 | - | |
| 492 | -#### 配置方式 | |
| 493 | -```yaml | |
| 494 | -ranking: | |
| 495 | - expression: "bm25() + 0.2*text_embedding_relevance()" | |
| 496 | - description: "BM25 text relevance combined with semantic embedding similarity" | |
| 497 | -``` | |
| 498 | - | |
| 499 | -#### 扩展性 | |
| 500 | -- 支持表达式配置(未来可扩展) | |
| 501 | -- 支持自定义函数(如 `timeliness()`, `field_value()`) | |
| 502 | - | |
| 503 | -#### 实现模块 | |
| 504 | -- `search/ranking_engine.py` - 排序引擎 | |
| 505 | -- `search/searcher.py` - 搜索器(集成排序功能) | |
| 506 | - | |
| 507 | ---- | |
| 508 | - | |
| 509 | -## 6. 已完成功能总结 | |
| 510 | - | |
| 511 | -### 6.1 配置系统 | |
| 512 | -- ✅ 字段定义配置(类型、分析器、来源表/列) | |
| 513 | -- ✅ 索引域配置(多域查询、多语言映射) | |
| 514 | -- ✅ 查询配置(改写词典、翻译配置) | |
| 515 | -- ✅ 排序配置(表达式配置) | |
| 516 | -- ✅ 配置验证(字段存在性、类型检查、分析器匹配) | |
| 517 | - | |
| 518 | -### 6.2 数据索引 | |
| 519 | -- ✅ 数据转换(字段映射、类型转换) | |
| 520 | -- ✅ 向量生成(文本向量、图片向量) | |
| 521 | -- ✅ 向量缓存(避免重复计算) | |
| 522 | -- ✅ 批量索引(错误处理、重试机制) | |
| 523 | -- ✅ ES mapping 自动生成 | |
| 524 | - | |
| 525 | -### 6.3 查询处理 | |
| 526 | -- ✅ 查询改写(词典配置) | |
| 527 | -- ✅ 语言检测 | |
| 528 | -- ✅ 多语言翻译(DeepL API) | |
| 529 | -- ✅ 文本向量化(BGE-M3) | |
| 530 | -- ✅ 域提取(支持 `domain:query` 语法) | |
| 531 | - | |
| 532 | -### 6.4 搜索功能 | |
| 533 | -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) | |
| 534 | -- ✅ 多语言查询构建(语言路由、字段映射) | |
| 535 | -- ✅ 语义搜索(KNN 检索) | |
| 536 | -- ✅ 相关性排序(BM25 + 向量相似度) | |
| 537 | -- ✅ 结果聚合(Faceted Search) | |
| 538 | - | |
| 539 | -### 6.5 API 服务 | |
| 540 | -- ✅ RESTful API(FastAPI) | |
| 541 | -- ✅ 搜索接口(文本搜索、图片搜索) | |
| 542 | -- ✅ 文档查询接口 | |
| 543 | -- ✅ 前端界面(HTML + JavaScript) | |
| 544 | -- ✅ 租户隔离(tenant_id过滤) | |
| 545 | - | |
| 546 | -### 6.6 Base配置(店匠通用) | |
| 547 | -- ✅ SPU级别索引结构 | |
| 548 | -- ✅ 嵌套skus字段 | |
| 549 | -- ✅ 统一索引(search_products) | |
| 550 | -- ✅ 租户隔离(tenant_id) | |
| 551 | -- ✅ 配置简化(移除MySQL相关配置) | |
| 552 | - | |
| 553 | ---- | |
| 554 | - | |
| 555 | -## 7. 技术栈 | |
| 556 | - | |
| 557 | -- **后端**:Python 3.6+ | |
| 558 | -- **搜索引擎**:Elasticsearch | |
| 559 | -- **数据库**:MySQL(Shoplazza) | |
| 560 | -- **向量模型**:BGE-M3(文本)、CN-CLIP(图片) | |
| 561 | -- **翻译服务**:DeepL API | |
| 562 | -- **API 框架**:FastAPI | |
| 563 | -- **前端**:HTML + JavaScript | |
| 564 | - | |
| 565 | ---- | |
| 566 | - | |
| 567 | -## 8. API响应格式 | |
| 568 | - | |
| 569 | -### 8.1 外部友好格式 | |
| 570 | - | |
| 571 | -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: | |
| 572 | - | |
| 573 | -**响应结构**: | |
| 574 | -```json | |
| 575 | -{ | |
| 576 | - "results": [ | |
| 577 | - { | |
| 578 | - "spu_id": "123", | |
| 579 | - "title": "蓝牙耳机", | |
| 580 | - "skus": [ | |
| 581 | - { | |
| 582 | - "sku_id": "456", | |
| 583 | - "price": 199.99, | |
| 584 | - "sku": "SKU-123-1", | |
| 585 | - "stock": 50 | |
| 586 | - } | |
| 587 | - ], | |
| 588 | - "relevance_score": 0.95 | |
| 589 | - } | |
| 590 | - ], | |
| 591 | - "total": 10, | |
| 592 | - "facets": [...], | |
| 593 | - "suggestions": [], | |
| 594 | - "related_searches": [] | |
| 595 | -} | |
| 596 | -``` | |
| 597 | - | |
| 598 | -**主要变化**: | |
| 599 | -- 结构化结果(`SpuResult`和`SkuResult`) | |
| 600 | -- 嵌套skus数组 | |
| 601 | -- 无ES内部字段 | |
| 602 | - | |
| 603 | -### 8.2 租户隔离 | |
| 604 | - | |
| 605 | -所有API请求必须提供`tenant_id`: | |
| 606 | -- 请求头:`X-Tenant-ID: 1` | |
| 607 | -- 或查询参数:`?tenant_id=1` | |
| 608 | - | |
| 609 | -搜索时自动添加`tenant_id`过滤,确保数据隔离。 | |
| 610 | - | |
| 611 | -### 8.3 数据接口约定 | |
| 612 | - | |
| 613 | -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 | |
| 614 | - | |
| 615 | -#### 8.3.1 数据流模式 | |
| 616 | - | |
| 617 | -系统采用统一的数据流模式,确保数据在各层之间的一致性: | |
| 618 | - | |
| 619 | -**数据流转路径**: | |
| 620 | -``` | |
| 621 | -API Request (JSON) | |
| 622 | - ↓ | |
| 623 | -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) | |
| 624 | - ↓ | |
| 625 | -Searcher(透传) | |
| 626 | - ↓ | |
| 627 | -ES Query Builder → model_dump() 转换为字典 | |
| 628 | - ↓ | |
| 629 | -ES Query (字典) | |
| 630 | - ↓ | |
| 631 | -Elasticsearch | |
| 632 | -``` | |
| 633 | - | |
| 634 | -#### 8.3.2 Facets 配置数据流 | |
| 635 | - | |
| 636 | -**输入格式**:`List[Union[str, FacetConfig]]` | |
| 637 | - | |
| 638 | -- **简单模式**:字符串列表(字段名),使用默认配置 | |
| 639 | - ```json | |
| 640 | - ["category.keyword", "vendor.keyword"] | |
| 641 | - ``` | |
| 642 | - | |
| 643 | -- **高级模式**:FacetConfig 对象列表,支持自定义配置 | |
| 644 | - ```json | |
| 645 | - [ | |
| 646 | - { | |
| 647 | - "field": "category.keyword", | |
| 648 | - "size": 15, | |
| 649 | - "type": "terms" | |
| 650 | - }, | |
| 651 | - { | |
| 652 | - "field": "price", | |
| 653 | - "type": "range", | |
| 654 | - "ranges": [ | |
| 655 | - {"key": "0-50", "to": 50}, | |
| 656 | - {"key": "50-100", "from": 50, "to": 100} | |
| 657 | - ] | |
| 658 | - } | |
| 659 | - ] | |
| 660 | - ``` | |
| 661 | - | |
| 662 | -**数据流**: | |
| 663 | -1. API 层:接收 `List[Union[str, FacetConfig]]` | |
| 664 | -2. Searcher 层:透传,不做转换 | |
| 665 | -3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 | |
| 666 | -4. 输出:转换为 ES 聚合查询 | |
| 667 | - | |
| 668 | -#### 8.3.3 Range Filters 数据流 | |
| 669 | - | |
| 670 | -**输入格式**:`Dict[str, RangeFilter]` | |
| 671 | - | |
| 672 | -**RangeFilter 模型**: | |
| 673 | -```python | |
| 674 | -class RangeFilter(BaseModel): | |
| 675 | - gte: Optional[Union[float, str]] # 大于等于 | |
| 676 | - gt: Optional[Union[float, str]] # 大于 | |
| 677 | - lte: Optional[Union[float, str]] # 小于等于 | |
| 678 | - lt: Optional[Union[float, str]] # 小于 | |
| 679 | -``` | |
| 680 | - | |
| 681 | -**示例**: | |
| 682 | -```json | |
| 683 | -{ | |
| 684 | - "price": {"gte": 50, "lte": 200}, | |
| 685 | - "created_at": {"gte": "2023-01-01T00:00:00Z"} | |
| 686 | -} | |
| 687 | -``` | |
| 688 | - | |
| 689 | -**数据流**: | |
| 690 | -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 | |
| 691 | -2. Searcher 层:透传 `Dict[str, RangeFilter]` | |
| 692 | -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 | |
| 693 | -4. 输出:ES range 查询(支持数值和日期) | |
| 694 | - | |
| 695 | -**特性**: | |
| 696 | -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) | |
| 697 | -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) | |
| 698 | -- 统一约定:所有范围过滤都使用 RangeFilter 模型 | |
| 699 | - | |
| 700 | -#### 8.3.4 响应 Facets 数据流 | |
| 701 | - | |
| 702 | -**输出格式**:`List[FacetResult]` | |
| 703 | - | |
| 704 | -**FacetResult 模型**: | |
| 705 | -```python | |
| 706 | -class FacetResult(BaseModel): | |
| 707 | - field: str # 字段名 | |
| 708 | - label: str # 显示标签 | |
| 709 | - type: Literal["terms", "range"] # 分面类型 | |
| 710 | - values: List[FacetValue] # 分面值列表 | |
| 711 | - total_count: Optional[int] # 总文档数 | |
| 712 | -``` | |
| 713 | - | |
| 714 | -**数据流**: | |
| 715 | -1. ES Response:返回聚合结果(字典格式) | |
| 716 | -2. Searcher 层:构建 `List[FacetResult]` 对象 | |
| 717 | -3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) | |
| 718 | - | |
| 719 | -**优势**: | |
| 720 | -- 类型安全:使用 Pydantic 模型确保数据结构一致性 | |
| 721 | -- 自动序列化:模型自动转换为 JSON,无需手动处理 | |
| 722 | -- 统一约定:所有响应都使用标准化的 Pydantic 模型 | |
| 723 | - | |
| 724 | -#### 8.3.5 统一约定的好处 | |
| 725 | - | |
| 726 | -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 | |
| 727 | -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 | |
| 728 | -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) | |
| 729 | -4. **易于维护**:修改数据结构只需更新模型定义 | |
| 730 | -5. **数据验证**:自动验证输入数据,减少错误处理代码 | |
| 731 | - | |
| 732 | -**实现模块**: | |
| 733 | -- `api/models.py` - 所有 Pydantic 模型定义 | |
| 734 | -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型) | |
| 735 | -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询) | |
| 736 | - | |
| 737 | -## 9. 配置文件示例 | |
| 738 | - | |
| 739 | -**Base配置**(店匠通用):`config/schema/base/config.yaml` | |
| 740 | - | |
| 741 | -**其他客户配置**:`config/schema/tenant1/config.yaml` | |
| 742 | - | |
| 743 | ---- |
example_usage.py
| ... | ... | @@ -38,13 +38,7 @@ def example_basic_usage(): |
| 38 | 38 | translations={"en": "red dress"} |
| 39 | 39 | ) |
| 40 | 40 | |
| 41 | - # 步骤2: 布尔解析 | |
| 42 | - if not context.query_analysis.is_simple_query: | |
| 43 | - context.start_stage(RequestContextStage.BOOLEAN_PARSING) | |
| 44 | - time.sleep(0.02) | |
| 45 | - context.end_stage(RequestContextStage.BOOLEAN_PARSING) | |
| 46 | - | |
| 47 | - # 步骤3: ES查询构建 | |
| 41 | + # 步骤2: ES查询构建 | |
| 48 | 42 | context.start_stage(RequestContextStage.QUERY_BUILDING) |
| 49 | 43 | time.sleep(0.03) |
| 50 | 44 | context.end_stage(RequestContextStage.QUERY_BUILDING) |
| ... | ... | @@ -53,7 +47,7 @@ def example_basic_usage(): |
| 53 | 47 | "size": 10 |
| 54 | 48 | }) |
| 55 | 49 | |
| 56 | - # 步骤4: ES搜索 | |
| 50 | + # 步骤3: ES搜索 | |
| 57 | 51 | context.start_stage(RequestContextStage.ELASTICSEARCH_SEARCH) |
| 58 | 52 | time.sleep(0.1) # 模拟ES响应时间 |
| 59 | 53 | context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH) |
| ... | ... | @@ -62,7 +56,7 @@ def example_basic_usage(): |
| 62 | 56 | "took": 45 |
| 63 | 57 | }) |
| 64 | 58 | |
| 65 | - # 步骤5: 结果处理 | |
| 59 | + # 步骤4: 结果处理 | |
| 66 | 60 | context.start_stage(RequestContextStage.RESULT_PROCESSING) |
| 67 | 61 | time.sleep(0.02) |
| 68 | 62 | context.end_stage(RequestContextStage.RESULT_PROCESSING) | ... | ... |
frontend/static/js/app.js
| ... | ... | @@ -950,7 +950,6 @@ function displayDebugInfo(data) { |
| 950 | 950 | html += `<div>detected_language: ${escapeHtml(debugInfo.query_analysis.detected_language || 'N/A')}</div>`; |
| 951 | 951 | html += `<div>index_languages: ${escapeHtml((debugInfo.query_analysis.index_languages || []).join(', ') || 'N/A')}</div>`; |
| 952 | 952 | html += `<div>query_tokens: ${escapeHtml((debugInfo.query_analysis.query_tokens || []).join(', ') || 'N/A')}</div>`; |
| 953 | - html += `<div>is_simple_query: ${debugInfo.query_analysis.is_simple_query ? 'yes' : 'no'}</div>`; | |
| 954 | 953 | |
| 955 | 954 | if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) { |
| 956 | 955 | html += '<div>translations: '; | ... | ... |
docs/reranker-共享前缀批量推理.md renamed to reranker/reranker-共享前缀批量推理.md
search/searcher.py
| ... | ... | @@ -396,7 +396,6 @@ class Searcher: |
| 396 | 396 | detected_language=parsed_query.detected_language, |
| 397 | 397 | translations=parsed_query.translations, |
| 398 | 398 | query_vector=parsed_query.query_vector.tolist() if parsed_query.query_vector is not None else None, |
| 399 | - is_simple_query=True | |
| 400 | 399 | ) |
| 401 | 400 | context.metadata["feature_flags"]["style_intent_active"] = self._has_style_intent(parsed_query) |
| 402 | 401 | |
| ... | ... | @@ -865,7 +864,6 @@ class Searcher: |
| 865 | 864 | "translations": context.query_analysis.translations, |
| 866 | 865 | "has_vector": context.query_analysis.query_vector is not None, |
| 867 | 866 | "query_tokens": getattr(parsed_query, "query_tokens", []), |
| 868 | - "is_simple_query": context.query_analysis.is_simple_query, | |
| 869 | 867 | "intent_detection": context.get_intermediate_result("style_intent_profile"), |
| 870 | 868 | }, |
| 871 | 869 | "es_query": context.get_intermediate_result('es_query', {}), | ... | ... |