From 4a677843805747d44b86f3e8394a9244a2ea4c19 Mon Sep 17 00:00:00 2001 From: tangwang Date: Fri, 14 Nov 2025 14:32:41 +0800 Subject: [PATCH] 文档完善 --- API_DOCUMENTATION.md | 938 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- API_EXAMPLES.md | 1148 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- API_INTEGRATION_GUIDE.md | 791 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- API_QUICK_REFERENCE.md | 233 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- BEST_PRACTICES_REFACTORING.md | 274 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- CHANGES.md | 611 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- INDEX_FIELDS_DOCUMENTATION.md | 223 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- README.md | 53 +++++++++++++++++++++-------------------------------- TEST_DATA_GUIDE.md | 527 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- USAGE_GUIDE.md | 441 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/BASE_CONFIG_GUIDE.md | 257 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/RequestContext_README.md | 374 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/Search-API-Examples.md | 1148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/TestingPipeline_README.md | 459 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- docs/Usage-Guide.md | 441 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/reference/商品数据源入ES配置规范.md | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/reference/阿里opensearch电商行业.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ docs/基础配置指南.md | 257 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/搜索API对接指南.md | 1018 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/搜索API速查表.md | 233 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/测试Pipeline说明.md | 459 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/测试数据指南.md | 527 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/环境配置说明.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/系统设计文档.md | 724 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/索引字段说明.md | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 商品数据源入ES配置规范.md | 221 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 环境相关.md | 123 --------------------------------------------------------------------------------------------------------------------------- 设计文档.md | 724 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 阿里opensearch电商行业.md | 47 ----------------------------------------------- 29 files changed, 5442 insertions(+), 7423 deletions(-) delete mode 100644 API_DOCUMENTATION.md delete mode 100644 API_EXAMPLES.md delete mode 100644 API_INTEGRATION_GUIDE.md delete mode 100644 API_QUICK_REFERENCE.md delete mode 100644 BEST_PRACTICES_REFACTORING.md delete mode 100644 CHANGES.md delete mode 100644 INDEX_FIELDS_DOCUMENTATION.md delete mode 100644 TEST_DATA_GUIDE.md delete mode 100644 USAGE_GUIDE.md delete mode 100644 docs/BASE_CONFIG_GUIDE.md delete mode 100644 docs/RequestContext_README.md create mode 100644 docs/Search-API-Examples.md delete mode 100644 docs/TestingPipeline_README.md create mode 100644 docs/Usage-Guide.md create mode 100644 docs/reference/商品数据源入ES配置规范.md create mode 100644 docs/reference/阿里opensearch电商行业.md create mode 100644 docs/基础配置指南.md create mode 100644 docs/搜索API对接指南.md create mode 100644 docs/搜索API速查表.md create mode 100644 docs/测试Pipeline说明.md create mode 100644 docs/测试数据指南.md create mode 100644 docs/环境配置说明.md create mode 100644 docs/系统设计文档.md create mode 100644 docs/索引字段说明.md delete mode 100644 商品数据源入ES配置规范.md delete mode 100644 环境相关.md delete mode 100644 设计文档.md delete mode 100644 阿里opensearch电商行业.md diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md deleted file mode 100644 index c3115ec..0000000 --- a/API_DOCUMENTATION.md +++ /dev/null @@ -1,938 +0,0 @@ -# 搜索引擎 API 接口文档 - -## 概述 - -本文档描述了电商搜索 SaaS 系统的 RESTful API 接口。系统提供强大的搜索功能,包括: - -- **多语言搜索**:支持中文、英文、俄文等多语言查询和自动翻译 -- **语义搜索**:基于 BGE-M3 文本向量和 CN-CLIP 图片向量的语义检索 -- **布尔表达式**:支持 AND、OR、RANK、ANDNOT 操作符 -- **灵活过滤**:精确匹配过滤器和数值范围过滤器 -- **分面搜索**:动态生成过滤选项,提供分组统计 -- **自定义排序**:支持按任意字段排序 -- **个性化排序**:可配置的相关性排序表达式 - -## 基础信息 - -- **Base URL**: `http://your-domain:6002` (http://120.76.41.98:6002) -- **协议**: HTTP/HTTPS -- **数据格式**: JSON -- **字符编码**: UTF-8 - -## 搜索接口 - -### 1. 文本搜索 - -**端点**: `POST /search/` - -**描述**: 执行文本搜索查询,支持多语言、布尔表达式、过滤器和分面搜索。 - -#### 请求参数 - -```json -{ - "query": "string (required)", - "size": 10, - "from": 0, - "filters": {}, - "range_filters": {}, - "facets": [], - "sort_by": "string", - "sort_order": "desc", - "min_score": 0.0, - "debug": false, - "user_id": "string", - "session_id": "string" -} -``` - -#### 参数说明 - -| 参数 | 类型 | 必填 | 默认值 | 描述 | -|------|------|------|--------|------| -| `query` | string | Y | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) | -| `size` | integer | N | 10 | 返回结果数量(1-100) | -| `from` | integer | N | 0 | 分页偏移量 | -| `filters` | object | N | null | 精确匹配过滤器(见下文) | -| `range_filters` | object | N | null | 数值范围过滤器(见下文) | -| `facets` | array | N | null | 分面配置(见下文) | -| `sort_by` | string | N | null | 排序字段名 | -| `sort_order` | string | N | "desc" | 排序方向:`asc` 或 `desc` | -| `min_score` | float | N | null | 最小相关性分数阈值 | -| `debug` | boolean | N | false | 是否返回调试信息 | -| `user_id` | string | N | null | 用户ID(用于个性化,预留) | -| `session_id` | string | N | null | 会话ID(用于分析,预留) | - -#### 过滤器详解 - -##### 精确匹配过滤器 (filters) - -用于精确匹配或多值匹配(OR 逻辑)。 - -**格式**: -```json -{ - "filters": { - "categoryName_keyword": "玩具", // 单值:精确匹配 - "brandName_keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR) - "in_stock": true // 布尔值 - } -} -``` - -**支持的值类型**: -- 字符串:精确匹配 -- 整数:精确匹配 -- 布尔值:精确匹配 -- 数组:匹配任意值(OR 逻辑) - -##### 范围过滤器 (range_filters) - -用于数值字段的范围过滤。 - -**格式**: -```json -{ - "range_filters": { - "price": { - "gte": 50, // 大于等于 - "lte": 200 // 小于等于 - }, - "days_since_last_update": { - "lte": 30 // 最近30天更新 - } - } -} -``` - -**支持的操作符**: -- `gte`: 大于等于 (>=) -- `gt`: 大于 (>) -- `lte`: 小于等于 (<=) -- `lt`: 小于 (<) - -**注意**: 至少需要指定一个操作符。 - -##### 分面配置 (facets) - -用于生成分面统计(分组聚合)。 - -**简单模式**(字符串数组): -```json -{ - "facets": ["categoryName_keyword", "brandName_keyword"] -} -``` - -**高级模式**(配置对象数组): -```json -{ - "facets": [ - { - "field": "categoryName_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ] -} -``` - -**分面配置参数**: -- `field`: 字段名(必填) -- `size`: 返回的分组数量(默认:10,范围:1-100) -- `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计) -- `ranges`: 范围定义(仅当 type='range' 时需要) - -#### 响应格式 - -```json -{ - "hits": [ - { - "_id": "12345", - "_score": 8.5, - "_custom_score": 12.3, - "_source": { - "name": "芭比时尚娃娃", - "price": 89.99, - "categoryName": "玩具", - "brandName": "美泰", - "imageUrl": "https://example.com/image.jpg" - } - } - ], - "total": 118, - "max_score": 8.5, - "took_ms": 45, - "facets": [ - { - "field": "categoryName_keyword", - "label": "商品类目", - "type": "terms", - "values": [ - { - "value": "玩具", - "label": "玩具", - "count": 85, - "selected": false - }, - { - "value": "益智玩具", - "label": "益智玩具", - "count": 33, - "selected": false - } - ] - } - ], - "query_info": { - "original_query": "芭比娃娃", - "detected_language": "zh", - "translations": { - "en": "barbie doll" - } - }, - "related_queries": null, - "performance_info": { - "total_duration": 45.2, - "stage_durations": { - "query_parsing": 5.3, - "elasticsearch_search": 35.1, - "result_processing": 4.8 - } - }, - "debug_info": null -} -``` - -#### 响应字段说明 - -| 字段 | 类型 | 描述 | -|------|------|------| -| `hits` | array | 搜索结果列表 | -| `hits[]._id` | string | 文档ID | -| `hits[]._score` | float | 相关性分数 | -| `hits[]._custom_score` | float | 自定义排序分数(如启用) | -| `hits[]._source` | object | 文档内容 | -| `total` | integer | 匹配的总文档数 | -| `max_score` | float | 最高相关性分数 | -| `took_ms` | integer | 搜索耗时(毫秒) | -| `facets` | array | 分面统计结果(标准化格式) | -| `facets[].field` | string | 字段名 | -| `facets[].label` | string | 显示标签 | -| `facets[].type` | string | 分面类型:`terms` 或 `range` | -| `facets[].values` | array | 分面值列表 | -| `facets[].values[].value` | any | 分面值 | -| `facets[].values[].label` | string | 显示标签 | -| `facets[].values[].count` | integer | 文档数量 | -| `facets[].values[].selected` | boolean | 是否已选中 | -| `query_info` | object | 查询处理信息 | -| `related_queries` | array | 相关搜索(预留) | -| `performance_info` | object | 性能信息 | -| `debug_info` | object | 调试信息(仅当 debug=true) | - -#### 请求示例 - -**示例 1: 简单搜索** - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "芭比娃娃", - "size": 20 - }' -``` - -**示例 2: 带过滤器的搜索** - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "filters": { - "categoryName_keyword": ["玩具", "益智玩具"], - "in_stock": true - }, - "range_filters": { - "price": { - "gte": 50, - "lte": 200 - } - } - }' -``` - -**示例 3: 带分面搜索(简单模式)** - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "facets": ["categoryName_keyword", "brandName_keyword"] - }' -``` - -**示例 4: 带分面搜索(高级模式)** - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "facets": [ - { - "field": "categoryName_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "brandName_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ] - }' -``` - -**示例 5: 复杂搜索(布尔表达式+过滤+排序)** - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具 AND (乐高 OR 芭比)", - "size": 20, - "filters": { - "categoryName_keyword": "玩具" - }, - "range_filters": { - "price": { - "gte": 50, - "lte": 200 - }, - "days_since_last_update": { - "lte": 30 - } - }, - "facets": [ - {"field": "brandName_keyword", "size": 15}, - {"field": "supplierName_keyword", "size": 10} - ], - "sort_by": "min_price", - "sort_order": "asc", - "debug": false - }' -``` - ---- - -### 2. 图片搜索 - -**端点**: `POST /search/image` - -**描述**: 基于图片相似度进行搜索,使用图片向量进行语义匹配。 - -#### 请求参数 - -```json -{ - "image_url": "string (required)", - "size": 10, - "filters": {}, - "range_filters": {} -} -``` - -#### 参数说明 - -| 参数 | 类型 | 必填 | 默认值 | 描述 | -|------|------|------|--------|------| -| `image_url` | string | ✅ | - | 查询图片的 URL | -| `size` | integer | ❌ | 10 | 返回结果数量(1-100) | -| `filters` | object | ❌ | null | 精确匹配过滤器 | -| `range_filters` | object | ❌ | null | 数值范围过滤器 | - -#### 响应格式 - -与文本搜索相同,但 `query_info` 包含图片信息: - -```json -{ - "hits": [...], - "total": 50, - "max_score": 0.95, - "took_ms": 120, - "query_info": { - "image_url": "https://example.com/image.jpg", - "search_type": "image_similarity" - } -} -``` - -#### 请求示例 - -```bash -curl -X POST "http://localhost:6002/search/image" \ - -H "Content-Type: application/json" \ - -d '{ - "image_url": "https://example.com/barbie.jpg", - "size": 20, - "filters": { - "categoryName_keyword": "玩具" - }, - "range_filters": { - "price": { - "lte": 100 - } - } - }' -``` - ---- - -### 3. 搜索建议(框架) - -**端点**: `GET /search/suggestions` - -**描述**: 获取搜索建议(自动补全)。 - -**注意**: 此功能暂未实现,仅返回框架响应。 - -#### 查询参数 - -| 参数 | 类型 | 必填 | 默认值 | 描述 | -|------|------|------|--------|------| -| `q` | string | ✅ | - | 搜索查询字符串(最少1个字符) | -| `size` | integer | ❌ | 5 | 建议数量(1-20) | -| `types` | string | ❌ | "query" | 建议类型(逗号分隔):query, product, category, brand | - -#### 响应格式 - -```json -{ - "query": "芭", - "suggestions": [ - { - "text": "芭比娃娃", - "type": "query", - "highlight": "比娃娃", - "popularity": 850 - } - ], - "took_ms": 5 -} -``` - -#### 请求示例 - -```bash -curl "http://localhost:6002/search/suggestions?q=芭&size=5&types=query,product" -``` - ---- - -### 4. 即时搜索(框架) - -**端点**: `GET /search/instant` - -**描述**: 即时搜索,边输入边搜索。 - -**注意**: 此功能暂未实现,调用标准搜索接口。 - -#### 查询参数 - -| 参数 | 类型 | 必填 | 默认值 | 描述 | -|------|------|------|--------|------| -| `q` | string | ✅ | - | 搜索查询(最少2个字符) | -| `size` | integer | ❌ | 5 | 结果数量(1-20) | - -#### 请求示例 - -```bash -curl "http://localhost:6002/search/instant?q=玩具&size=5" -``` - ---- - -### 5. 获取单个文档 - -**端点**: `GET /search/{doc_id}` - -**描述**: 根据文档ID获取单个文档详情。 - -#### 路径参数 - -| 参数 | 类型 | 描述 | -|------|------|------| -| `doc_id` | string | 文档ID | - -#### 响应格式 - -```json -{ - "id": "12345", - "source": { - "name": "芭比时尚娃娃", - "price": 89.99, - "categoryName": "玩具" - } -} -``` - -#### 请求示例 - -```bash -curl "http://localhost:6002/search/12345" -``` - ---- - -## 管理接口 - -### 1. 健康检查 - -**端点**: `GET /admin/health` - -**描述**: 检查服务健康状态。 - -#### 响应格式 - -```json -{ - "status": "healthy", - "elasticsearch": "connected", - "tenant_id": "tenant1" -} -``` - ---- - -### 2. 获取配置 - -**端点**: `GET /admin/config` - -**描述**: 获取当前客户配置(脱敏)。 - -#### 响应格式 - -```json -{ - "tenant_id": "tenant1", - "tenant_name": "Tenant1 Test Instance", - "es_index_name": "search_tenant1", - "num_fields": 20, - "num_indexes": 4, - "supported_languages": ["zh", "en", "ru"], - "ranking_expression": "bm25() + 0.2*text_embedding_relevance()", - "spu_enabled": false -} -``` - ---- - -### 3. 索引统计 - -**端点**: `GET /admin/stats` - -**描述**: 获取索引统计信息。 - -#### 响应格式 - -```json -{ - "index_name": "search_tenant1", - "document_count": 10000, - "size_mb": 523.45 -} -``` - ---- - -### 4. 查询改写规则 - -**端点**: `GET /admin/rewrite-rules` - -**描述**: 获取当前的查询改写规则。 - -#### 响应格式 - -```json -{ - "rules": { - "乐高": "brand:乐高 OR name:乐高", - "玩具": "category:玩具" - }, - "count": 2 -} -``` - -**端点**: `POST /admin/rewrite-rules` - -**描述**: 更新查询改写规则。 - -#### 请求格式 - -```json -{ - "乐高": "brand:乐高 OR name:乐高", - "芭比": "brand:芭比 OR name:芭比" -} -``` - ---- - -## 使用示例 - -### Python 示例 - -```python -import requests - -API_URL = "http://localhost:6002/search/" - -# 简单搜索 -response = requests.post(API_URL, json={ - "query": "芭比娃娃", - "size": 20 -}) -data = response.json() -print(f"找到 {data['total']} 个结果") - -# 带过滤器和分面的搜索 -response = requests.post(API_URL, json={ - "query": "玩具", - "size": 20, - "filters": { - "categoryName_keyword": ["玩具", "益智玩具"] - }, - "range_filters": { - "price": {"gte": 50, "lte": 200} - }, - "facets": [ - {"field": "brandName_keyword", "size": 15}, - {"field": "categoryName_keyword", "size": 15} - ], - "sort_by": "min_price", - "sort_order": "asc" -}) -result = response.json() - -# 处理分面结果 -for facet in result.get('facets', []): - print(f"\n{facet['label']}:") - for value in facet['values']: - print(f" - {value['label']}: {value['count']}") -``` - -### JavaScript 示例 - -```javascript -// 搜索函数 -async function searchProducts(query, filters, rangeFilters, facets) { - const response = await fetch('http://localhost:6002/search/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - query: query, - size: 20, - filters: filters, - range_filters: rangeFilters, - facets: facets - }) - }); - - const data = await response.json(); - return data; -} - -// 使用示例 -const result = await searchProducts( - "玩具", - { categoryName_keyword: ["玩具"] }, - { price: { gte: 50, lte: 200 } }, - [ - { field: "brandName_keyword", size: 15 }, - { field: "categoryName_keyword", size: 15 } - ] -); - -// 显示分面结果 -result.facets.forEach(facet => { - console.log(`${facet.label}:`); - facet.values.forEach(value => { - console.log(` - ${value.label}: ${value.count}`); - }); -}); - -// 显示搜索结果 -result.hits.forEach(hit => { - const product = hit._source; - console.log(`${product.name} - ¥${product.price}`); -}); -``` - -### cURL 示例 - -```bash -# 简单搜索 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{"query": "芭比娃娃", "size": 20}' - -# 带过滤和排序 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "filters": {"categoryName_keyword": "玩具"}, - "range_filters": {"price": {"gte": 50, "lte": 200}}, - "sort_by": "min_price", - "sort_order": "asc" - }' - -# 带分面搜索 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "facets": [ - {"field": "categoryName_keyword", "size": 15}, - {"field": "brandName_keyword", "size": 15} - ] - }' -``` - ---- - -## 布尔表达式语法 - -### 支持的操作符 - -| 操作符 | 描述 | 示例 | -|--------|------|------| -| `AND` | 所有词必须匹配 | `玩具 AND 乐高` | -| `OR` | 任意词匹配 | `芭比 OR 娃娃` | -| `ANDNOT` | 排除特定词 | `玩具 ANDNOT 电动` | -| `RANK` | 排序加权(不强制匹配) | `玩具 RANK 乐高` | -| `()` | 分组 | `玩具 AND (乐高 OR 芭比)` | - -### 操作符优先级 - -从高到低: -1. `()` - 括号 -2. `ANDNOT` - 排除 -3. `AND` - 与 -4. `OR` - 或 -5. `RANK` - 排序 - -### 查询示例 - -``` -# 简单查询 -"芭比娃娃" - -# AND 查询 -"玩具 AND 乐高" - -# OR 查询 -"芭比 OR 娃娃" - -# 排除查询 -"玩具 ANDNOT 电动" - -# 复杂查询 -"玩具 AND (乐高 OR 芭比) ANDNOT 电动" - -# 域查询 -"brand:乐高" -"category:玩具" -"title:芭比娃娃" -``` - ---- - -## 数据模型 - -### 商品字段 - -常见的商品字段包括: - -| 字段名 | 类型 | 描述 | -|--------|------|------| -| `skuId` | long | SKU ID(主键) | -| `name` | text | 商品名称(中文) | -| `enSpuName` | text | 商品名称(英文) | -| `ruSkuName` | text | 商品名称(俄文) | -| `categoryName` | text | 类目名称 | -| `categoryName_keyword` | keyword | 类目名称(精确匹配) | -| `brandName` | text | 品牌名称 | -| `brandName_keyword` | keyword | 品牌名称(精确匹配) | -| `supplierName` | text | 供应商名称 | -| `supplierName_keyword` | keyword | 供应商名称(精确匹配) | -| `price` | double | 价格 | -| `imageUrl` | keyword | 商品图片URL | -| `create_time` | date | 创建时间 | -| `days_since_last_update` | int | 距上次更新天数 | - -**注意**: 不同客户可能有不同的字段配置。 - ---- - - -## 常见问题 - -### Q1: 如何判断一个字段应该用哪种过滤器? - -**A**: -- **精确匹配过滤器** (`filters`): 用于 KEYWORD 类型字段(如类目、品牌、标签等) -- **范围过滤器** (`range_filters`): 用于数值类型字段(如价格、库存、时间等) - -### Q2: 可以同时使用多个过滤器吗? - -**A**: 可以。多个过滤器之间是 AND 关系(必须同时满足)。 - -```json -{ - "filters": { - "categoryName_keyword": "玩具", - "brandName_keyword": "乐高" - }, - "range_filters": { - "price": {"gte": 50, "lte": 200} - } -} -``` -结果:类目是"玩具" **并且** 品牌是"乐高" **并且** 价格在50-200之间。 - -### Q3: 如何实现"价格小于50或大于200"的过滤? - -**A**: 当前版本不支持单字段多个不连续范围。建议分两次查询或使用布尔查询。 - -### Q4: 分面搜索返回的 selected 字段是什么意思? - -**A**: `selected` 表示该分面值是否在当前的过滤器中。前端可以用它来高亮已选中的过滤项。 - -### Q5: 如何使用自定义排序? - -**A**: 使用 `sort_by` 和 `sort_order` 参数: - -```json -{ - "query": "玩具", - "sort_by": "price", - "sort_order": "asc" // 价格从低到高 -} -``` - -常用排序字段: -- `price`: 价格 -- `create_time`: 创建时间 -- `days_since_last_update`: 更新时间 - -### Q6: 如何启用调试模式? - -**A**: 设置 `debug: true`,响应中会包含 `debug_info` 字段,包含: -- 查询分析过程 -- ES 查询 DSL -- ES 响应详情 -- 各阶段耗时 - ---- - -## 版本历史 - -### v3.0 (2024-11-12) - -**重大更新**: -- ✅ 移除硬编码的 `price_ranges` 逻辑 -- ✅ 新增 `range_filters` 参数,支持任意数值字段的范围过滤 -- ✅ 新增 `facets` 参数,替代 `aggregations`,提供简化接口 -- ✅ 标准化分面搜索响应格式 -- ✅ 新增 `/search/suggestions` 端点(框架) -- ✅ 新增 `/search/instant` 端点(框架) -- ❌ **移除** `aggregations` 参数(不向后兼容) - -**迁移指南**: - -旧接口: -```json -{ - "filters": { - "price_ranges": ["0-50", "50-100"] - }, - "aggregations": { - "category_stats": {"terms": {"field": "categoryName_keyword", "size": 15}} - } -} -``` - -新接口: -```json -{ - "range_filters": { - "price": {"gte": 50, "lte": 100} - }, - "facets": [ - {"field": "categoryName_keyword", "size": 15} - ] -} -``` - ---- - -## 附录 - -### A. 支持的分析器 - -| 分析器 | 语言 | 描述 | -|--------|------|------| -| `chinese_ecommerce` | 中文 | Ansj 中文分词器(电商优化) | -| `english` | 英文 | 标准英文分析器 | -| `russian` | 俄文 | 俄文分析器 | -| `arabic` | 阿拉伯文 | 阿拉伯文分析器 | -| `spanish` | 西班牙文 | 西班牙文分析器 | -| `japanese` | 日文 | 日文分析器 | - -### B. 字段类型 - -| 类型 | ES 映射 | 用途 | -|------|---------|------| -| `TEXT` | text | 全文检索 | -| `KEYWORD` | keyword | 精确匹配、聚合、排序 | -| `LONG` | long | 整数 | -| `DOUBLE` | double | 浮点数 | -| `DATE` | date | 日期时间 | -| `BOOLEAN` | boolean | 布尔值 | -| `TEXT_EMBEDDING` | dense_vector | 文本向量(1024维) | -| `IMAGE_EMBEDDING` | dense_vector | 图片向量(1024维) | - diff --git a/API_EXAMPLES.md b/API_EXAMPLES.md deleted file mode 100644 index c262fa2..0000000 --- a/API_EXAMPLES.md +++ /dev/null @@ -1,1148 +0,0 @@ -# API 使用示例 - -本文档提供了搜索引擎 API 的详细使用示例,包括各种常见场景和最佳实践。 - ---- - -## 目录 - -1. [基础搜索](#基础搜索) -2. [过滤器使用](#过滤器使用) -3. [分面搜索](#分面搜索) -4. [排序](#排序) -5. [图片搜索](#图片搜索) -6. [布尔表达式](#布尔表达式) -7. [完整示例](#完整示例) - ---- - -## 基础搜索 - -### 示例 1:最简单的搜索 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "芭比娃娃" - }' -``` - -**响应**: -```json -{ - "hits": [...], - "total": 118, - "max_score": 8.5, - "took_ms": 45, - "query_info": { - "original_query": "芭比娃娃", - "detected_language": "zh", - "translations": {"en": "barbie doll"} - } -} -``` - -### 示例 2:指定返回数量 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 50 - }' -``` - -### 示例 3:分页查询 - -```bash -# 第1页(0-19) -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "from": 0 - }' - -# 第2页(20-39) -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "from": 20 - }' -``` - ---- - -## 过滤器使用 - -### 精确匹配过滤器 - -#### 示例 1:单值过滤 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "filters": { - "categoryName_keyword": "玩具" - } - }' -``` - -#### 示例 2:多值过滤(OR 逻辑) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "娃娃", - "filters": { - "categoryName_keyword": ["玩具", "益智玩具", "儿童玩具"] - } - }' -``` - -说明:匹配类目为"玩具" **或** "益智玩具" **或** "儿童玩具"的商品。 - -#### 示例 3:多字段过滤(AND 逻辑) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "娃娃", - "filters": { - "categoryName_keyword": "玩具", - "brandName_keyword": "美泰" - } - }' -``` - -说明:必须同时满足"类目=玩具" **并且** "品牌=美泰"。 - -### 范围过滤器 - -#### 示例 1:价格范围 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "range_filters": { - "price": { - "gte": 50, - "lte": 200 - } - } - }' -``` - -说明:价格在 50-200 元之间(包含边界)。 - -#### 示例 2:只设置下限 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "range_filters": { - "price": { - "gte": 100 - } - } - }' -``` - -说明:价格 ≥ 100 元。 - -#### 示例 3:只设置上限 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "range_filters": { - "price": { - "lt": 50 - } - } - }' -``` - -说明:价格 < 50 元(不包含50)。 - -#### 示例 4:多字段范围过滤 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "range_filters": { - "price": { - "gte": 50, - "lte": 200 - }, - "days_since_last_update": { - "lte": 30 - } - } - }' -``` - -说明:价格在 50-200 元 **并且** 最近30天内更新过。 - -### 组合过滤器 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "filters": { - "categoryName_keyword": ["玩具", "益智玩具"], - "brandName_keyword": "乐高" - }, - "range_filters": { - "price": { - "gte": 50, - "lte": 500 - } - } - }' -``` - -说明:类目是"玩具"或"益智玩具" **并且** 品牌是"乐高" **并且** 价格在 50-500 元之间。 - ---- - -## 分面搜索 - -### 简单模式 - -#### 示例 1:基础分面 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "facets": ["categoryName_keyword", "brandName_keyword"] - }' -``` - -**响应**: -```json -{ - "hits": [...], - "total": 118, - "facets": [ - { - "field": "categoryName_keyword", - "label": "categoryName_keyword", - "type": "terms", - "values": [ - {"value": "玩具", "count": 85, "selected": false}, - {"value": "益智玩具", "count": 33, "selected": false} - ] - }, - { - "field": "brandName_keyword", - "label": "brandName_keyword", - "type": "terms", - "values": [ - {"value": "乐高", "count": 42, "selected": false}, - {"value": "美泰", "count": 28, "selected": false} - ] - } - ] -} -``` - -### 高级模式 - -#### 示例 1:自定义分面大小 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "facets": [ - { - "field": "categoryName_keyword", - "size": 20, - "type": "terms" - }, - { - "field": "brandName_keyword", - "size": 30, - "type": "terms" - } - ] - }' -``` - -#### 示例 2:范围分面 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "facets": [ - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ] - }' -``` - -**响应**: -```json -{ - "facets": [ - { - "field": "price", - "label": "price", - "type": "range", - "values": [ - {"value": "0-50", "count": 23, "selected": false}, - {"value": "50-100", "count": 45, "selected": false}, - {"value": "100-200", "count": 38, "selected": false}, - {"value": "200+", "count": 12, "selected": false} - ] - } - ] -} -``` - -#### 示例 3:混合分面(Terms + Range) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "facets": [ - {"field": "categoryName_keyword", "size": 15}, - {"field": "brandName_keyword", "size": 15}, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "低价", "to": 50}, - {"key": "中价", "from": 50, "to": 200}, - {"key": "高价", "from": 200} - ] - } - ] - }' -``` - ---- - -## 排序 - -### 示例 1:按价格排序(升序) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "sort_by": "min_price", - "sort_order": "asc" - }' -``` - -### 示例 2:按创建时间排序(降序) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 20, - "sort_by": "create_time", - "sort_order": "desc" - }' -``` - -### 示例 3:排序+过滤 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "filters": { - "categoryName_keyword": "益智玩具" - }, - "sort_by": "min_price", - "sort_order": "asc" - }' -``` - ---- - -## 图片搜索 - -### 示例 1:基础图片搜索 - -```bash -curl -X POST "http://localhost:6002/search/image" \ - -H "Content-Type: application/json" \ - -d '{ - "image_url": "https://example.com/barbie.jpg", - "size": 20 - }' -``` - -### 示例 2:图片搜索+过滤器 - -```bash -curl -X POST "http://localhost:6002/search/image" \ - -H "Content-Type: application/json" \ - -d '{ - "image_url": "https://example.com/barbie.jpg", - "size": 20, - "filters": { - "categoryName_keyword": "玩具" - }, - "range_filters": { - "price": { - "lte": 100 - } - } - }' -``` - ---- - -## 布尔表达式 - -### 示例 1:AND 查询 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具 AND 乐高" - }' -``` - -说明:必须同时包含"玩具"和"乐高"。 - -### 示例 2:OR 查询 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "芭比 OR 娃娃" - }' -``` - -说明:包含"芭比"或"娃娃"即可。 - -### 示例 3:ANDNOT 查询(排除) - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具 ANDNOT 电动" - }' -``` - -说明:包含"玩具"但不包含"电动"。 - -### 示例 4:复杂布尔表达式 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" - }' -``` - -说明:必须包含"玩具",并且包含"乐高"或"芭比",但不包含"电动"。 - -### 示例 5:域查询 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "brand:乐高" - }' -``` - -说明:在品牌域中搜索"乐高"。 - ---- - -## 完整示例 - -### Python 完整示例 - -```python -#!/usr/bin/env python3 -import requests -import json - -API_URL = "http://localhost:6002/search/" - -def search_products( - query, - size=20, - from_=0, - filters=None, - range_filters=None, - facets=None, - sort_by=None, - sort_order="desc", - debug=False -): - """执行搜索查询""" - payload = { - "query": query, - "size": size, - "from": from_ - } - - if filters: - payload["filters"] = filters - if range_filters: - payload["range_filters"] = range_filters - if facets: - payload["facets"] = facets - if sort_by: - payload["sort_by"] = sort_by - payload["sort_order"] = sort_order - if debug: - payload["debug"] = debug - - response = requests.post(API_URL, json=payload) - response.raise_for_status() - return response.json() - - -# 示例 1:简单搜索 -result = search_products("芭比娃娃", size=10) -print(f"找到 {result['total']} 个结果") -for hit in result['hits'][:3]: - product = hit['_source'] - print(f" - {product['name']}: ¥{product.get('price', 'N/A')}") - -# 示例 2:带过滤和分面的搜索 -result = search_products( - query="玩具", - size=20, - filters={ - "categoryName_keyword": ["玩具", "益智玩具"] - }, - range_filters={ - "price": {"gte": 50, "lte": 200} - }, - facets=[ - {"field": "brandName_keyword", "size": 15}, - {"field": "categoryName_keyword", "size": 15}, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ], - sort_by="min_price", - sort_order="asc" -) - -# 显示分面结果 -print(f"\n分面统计:") -for facet in result.get('facets', []): - print(f"\n{facet['label']} ({facet['type']}):") - for value in facet['values'][:5]: - selected_mark = "✓" if value['selected'] else " " - print(f" [{selected_mark}] {value['label']}: {value['count']}") - -# 示例 3:分页查询 -page = 1 -page_size = 20 -total_pages = 5 - -for page in range(1, total_pages + 1): - result = search_products( - query="玩具", - size=page_size, - from_=(page - 1) * page_size - ) - print(f"\n第 {page} 页:") - for hit in result['hits']: - product = hit['_source'] - print(f" - {product['name']}") -``` - -### JavaScript 完整示例 - -```javascript -// 搜索引擎客户端 -class SearchClient { - constructor(baseUrl) { - this.baseUrl = baseUrl; - } - - async search({ - query, - size = 20, - from = 0, - filters = null, - rangeFilters = null, - facets = null, - sortBy = null, - sortOrder = 'desc', - debug = false - }) { - const payload = { - query, - size, - from - }; - - if (filters) payload.filters = filters; - if (rangeFilters) payload.range_filters = rangeFilters; - if (facets) payload.facets = facets; - if (sortBy) { - payload.sort_by = sortBy; - payload.sort_order = sortOrder; - } - if (debug) payload.debug = debug; - - const response = await fetch(`${this.baseUrl}/search/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return await response.json(); - } - - async searchByImage(imageUrl, options = {}) { - const payload = { - image_url: imageUrl, - size: options.size || 20, - filters: options.filters || null, - range_filters: options.rangeFilters || null - }; - - const response = await fetch(`${this.baseUrl}/search/image`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return await response.json(); - } -} - -// 使用示例 -const client = new SearchClient('http://localhost:6002'); - -// 简单搜索 -const result1 = await client.search({ - query: "芭比娃娃", - size: 20 -}); -console.log(`找到 ${result1.total} 个结果`); - -// 带过滤和分面的搜索 -const result2 = await client.search({ - query: "玩具", - size: 20, - filters: { - categoryName_keyword: ["玩具", "益智玩具"] - }, - rangeFilters: { - price: { gte: 50, lte: 200 } - }, - facets: [ - { field: "brandName_keyword", size: 15 }, - { field: "categoryName_keyword", size: 15 } - ], - sortBy: "price", - sortOrder: "asc" -}); - -// 显示分面结果 -result2.facets.forEach(facet => { - console.log(`\n${facet.label}:`); - facet.values.forEach(value => { - const selected = value.selected ? '✓' : ' '; - console.log(` [${selected}] ${value.label}: ${value.count}`); - }); -}); - -// 显示商品 -result2.hits.forEach(hit => { - const product = hit._source; - console.log(`${product.name} - ¥${product.price}`); -}); -``` - -### 前端完整示例(Vue.js 风格) - -```javascript -// 搜索组件 -const SearchComponent = { - data() { - return { - query: '', - results: [], - facets: [], - filters: {}, - rangeFilters: {}, - total: 0, - currentPage: 1, - pageSize: 20 - }; - }, - methods: { - async search() { - const response = await fetch('http://localhost:6002/search/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query: this.query, - size: this.pageSize, - from: (this.currentPage - 1) * this.pageSize, - filters: this.filters, - range_filters: this.rangeFilters, - facets: [ - { field: 'categoryName_keyword', size: 15 }, - { field: 'brandName_keyword', size: 15 } - ] - }) - }); - - const data = await response.json(); - this.results = data.hits; - this.facets = data.facets || []; - this.total = data.total; - }, - - toggleFilter(field, value) { - if (!this.filters[field]) { - this.filters[field] = []; - } - - const index = this.filters[field].indexOf(value); - if (index > -1) { - this.filters[field].splice(index, 1); - if (this.filters[field].length === 0) { - delete this.filters[field]; - } - } else { - this.filters[field].push(value); - } - - this.currentPage = 1; - this.search(); - }, - - setPriceRange(min, max) { - if (min !== null || max !== null) { - this.rangeFilters.price = {}; - if (min !== null) this.rangeFilters.price.gte = min; - if (max !== null) this.rangeFilters.price.lte = max; - } else { - delete this.rangeFilters.price; - } - this.currentPage = 1; - this.search(); - } - } -}; -``` - ---- - -## 调试与优化 - -### 启用调试模式 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "debug": true - }' -``` - -**响应包含调试信息**: -```json -{ - "hits": [...], - "total": 118, - "debug_info": { - "query_analysis": { - "original_query": "玩具", - "normalized_query": "玩具", - "rewritten_query": "玩具", - "detected_language": "zh", - "translations": {"en": "toy"} - }, - "es_query": { - "query": {...}, - "size": 10 - }, - "stage_timings": { - "query_parsing": 5.3, - "elasticsearch_search": 35.1, - "result_processing": 4.8 - } - } -} -``` - -### 设置最小分数阈值 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "min_score": 5.0 - }' -``` - -说明:只返回相关性分数 ≥ 5.0 的结果。 - ---- - -## 常见使用场景 - -### 场景 1:电商分类页 - -```bash -# 显示某个类目下的所有商品,按价格排序,提供品牌筛选 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "*", - "filters": { - "categoryName_keyword": "玩具" - }, - "facets": [ - {"field": "brandName_keyword", "size": 20}, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ], - "sort_by": "min_price", - "sort_order": "asc", - "size": 24 - }' -``` - -### 场景 2:搜索结果页 - -```bash -# 用户搜索关键词,提供筛选和排序 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "芭比娃娃", - "facets": [ - {"field": "categoryName_keyword", "size": 10}, - {"field": "brandName_keyword", "size": 10}, - {"field": "price", "type": "range", "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100+", "from": 100} - ]} - ], - "size": 20 - }' -``` - -### 场景 3:促销专区 - -```bash -# 显示特定价格区间的商品 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "*", - "range_filters": { - "price": { - "gte": 50, - "lte": 100 - } - }, - "facets": ["categoryName_keyword", "brandName_keyword"], - "sort_by": "min_price", - "sort_order": "asc", - "size": 50 - }' -``` - -### 场景 4:新品推荐 - -```bash -# 最近更新的商品 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "*", - "range_filters": { - "days_since_last_update": { - "lte": 7 - } - }, - "sort_by": "create_time", - "sort_order": "desc", - "size": 20 - }' -``` - ---- - -## 错误处理 - -### 示例 1:参数错误 - -```bash -# 错误:range_filters 缺少操作符 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "range_filters": { - "price": {} - } - }' -``` - -**响应**: -```json -{ - "error": "Validation error", - "detail": "至少需要指定一个范围边界(gte, gt, lte, lt)", - "timestamp": 1699800000 -} -``` - -### 示例 2:空查询 - -```bash -# 错误:query 为空 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "" - }' -``` - -**响应**: -```json -{ - "error": "Validation error", - "detail": "query field required", - "timestamp": 1699800000 -} -``` - ---- - -## 性能优化建议 - -### 1. 合理使用分面 - -```bash -# ❌ 不推荐:请求太多分面 -{ - "facets": [ - {"field": "field1", "size": 100}, - {"field": "field2", "size": 100}, - {"field": "field3", "size": 100}, - // ... 10+ facets - ] -} - -# ✅ 推荐:只请求必要的分面 -{ - "facets": [ - {"field": "categoryName_keyword", "size": 15}, - {"field": "brandName_keyword", "size": 15} - ] -} -``` - -### 2. 控制返回数量 - -```bash -# ❌ 不推荐:一次返回太多 -{ - "size": 100 -} - -# ✅ 推荐:分页查询 -{ - "size": 20, - "from": 0 -} -``` - -### 3. 使用适当的过滤器 - -```bash -# ✅ 推荐:先过滤后搜索 -{ - "query": "玩具", - "filters": { - "categoryName_keyword": "玩具" - } -} -``` - ---- - -## 高级技巧 - -### 技巧 1:获取所有类目 - -```bash -# 使用通配符查询 + 分面 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "*", - "size": 0, - "facets": [ - {"field": "categoryName_keyword", "size": 100} - ] - }' -``` - -### 技巧 2:价格分布统计 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 0, - "facets": [ - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200-500", "from": 200, "to": 500}, - {"key": "500+", "from": 500} - ] - } - ] - }' -``` - -### 技巧 3:组合多种查询类型 - -```bash -# 布尔表达式 + 过滤器 + 分面 + 排序 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", - "filters": { - "categoryName_keyword": ["玩具", "益智玩具"] - }, - "range_filters": { - "price": {"gte": 20, "lte": 100}, - "days_since_last_update": {"lte": 30} - }, - "facets": [ - {"field": "brandName_keyword", "size": 20} - ], - "sort_by": "min_price", - "sort_order": "asc", - "size": 20 - }' -``` - ---- - -## 测试数据 - -如果你需要测试数据,可以使用以下查询: - -```bash -# 测试类目:玩具 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{"query": "玩具", "size": 5}' - -# 测试品牌:乐高 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{"query": "brand:乐高", "size": 5}' - -# 测试布尔表达式 -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{"query": "玩具 AND 乐高", "size": 5}' -``` - ---- - -**文档版本**: 3.0 -**最后更新**: 2024-11-12 -**相关文档**: `API_DOCUMENTATION.md` - diff --git a/API_INTEGRATION_GUIDE.md b/API_INTEGRATION_GUIDE.md deleted file mode 100644 index 1639f83..0000000 --- a/API_INTEGRATION_GUIDE.md +++ /dev/null @@ -1,791 +0,0 @@ -# 搜索API接口对接指南 - -本文档为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。 - -## 目录 - -1. [快速开始](#快速开始) -2. [接口概览](#接口概览) -3. [文本搜索接口](#文本搜索接口) -4. [图片搜索接口](#图片搜索接口) -5. [响应格式说明](#响应格式说明) -6. [常见场景示例](#常见场景示例) -7. [错误处理](#错误处理) -8. [最佳实践](#最佳实践) - ---- - -## 快速开始 - -### 基础信息 - -- **Base URL**: `http://your-domain:6002` 或 `http://120.76.41.98:6002` -- **协议**: HTTP/HTTPS -- **数据格式**: JSON -- **字符编码**: UTF-8 -- **请求方法**: POST(搜索接口) - -### 最简单的搜索请求 - -```bash -curl -X POST "http://localhost:6002/search/" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "芭比娃娃" - }' -``` - -### Python示例 - -```python -import requests - -url = "http://localhost:6002/search/" -response = requests.post(url, json={"query": "芭比娃娃"}) -data = response.json() -print(f"找到 {data['total']} 个结果") -``` - -### JavaScript示例 - -```javascript -const response = await fetch('http://localhost:6002/search/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - query: '芭比娃娃' - }) -}); -const data = await response.json(); -console.log(`找到 ${data.total} 个结果`); -``` - ---- - -## 接口概览 - -| 接口 | 方法 | 路径 | 说明 | -|------|------|------|------| -| 文本搜索 | POST | `/search/` | 执行文本搜索查询 | -| 图片搜索 | POST | `/search/image` | 基于图片相似度搜索 | -| 搜索建议 | GET | `/search/suggestions` | 获取搜索建议(框架,暂未实现) | -| 获取文档 | GET | `/search/{doc_id}` | 根据ID获取单个文档 | -| 健康检查 | GET | `/admin/health` | 检查服务状态 | - ---- - -## 文本搜索接口 - -### 接口信息 - -- **端点**: `POST /search/` -- **描述**: 执行文本搜索查询,支持多语言、布尔表达式、过滤器和分面搜索 - -### 请求参数 - -#### 完整请求体结构 - -```json -{ - "query": "string (required)", - "size": 10, - "from": 0, - "filters": {}, - "range_filters": {}, - "facets": [], - "sort_by": "string", - "sort_order": "desc", - "min_score": 0.0, - "debug": false, - "user_id": "string", - "session_id": "string" -} -``` - -#### 参数详细说明 - -| 参数 | 类型 | 必填 | 默认值 | 说明 | -|------|------|------|--------|------| -| `query` | string | ✅ | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) | -| `size` | integer | ❌ | 10 | 返回结果数量(1-100) | -| `from` | integer | ❌ | 0 | 分页偏移量(用于分页) | -| `filters` | object | ❌ | null | 精确匹配过滤器(见下文) | -| `range_filters` | object | ❌ | null | 数值范围过滤器(见下文) | -| `facets` | array | ❌ | null | 分面配置(见下文) | -| `sort_by` | string | ❌ | null | 排序字段名(如 `min_price`, `max_price`, `title`) | -| `sort_order` | string | ❌ | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | -| `min_score` | float | ❌ | null | 最小相关性分数阈值 | -| `debug` | boolean | ❌ | false | 是否返回调试信息 | -| `user_id` | string | ❌ | null | 用户ID(用于个性化,预留) | -| `session_id` | string | ❌ | null | 会话ID(用于分析,预留) | - -### 过滤器详解 - -#### 1. 精确匹配过滤器 (filters) - -用于精确匹配或多值匹配(OR 逻辑)。 - -**格式**: -```json -{ - "filters": { - "category_keyword": "玩具", // 单值:精确匹配 - "vendor_keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR) - "product_type_keyword": "益智玩具" // 单值:精确匹配 - } -} -``` - -**支持的值类型**: -- 字符串:精确匹配 -- 整数:精确匹配 -- 布尔值:精确匹配 -- 数组:匹配任意值(OR 逻辑) - -**常用过滤字段**: -- `category_keyword`: 类目 -- `vendor_keyword`: 品牌/供应商 -- `product_type_keyword`: 商品类型 -- `tags_keyword`: 标签 - -#### 2. 范围过滤器 (range_filters) - -用于数值字段的范围过滤。 - -**格式**: -```json -{ - "range_filters": { - "min_price": { - "gte": 50, // 大于等于 - "lte": 200 // 小于等于 - }, - "max_price": { - "gt": 100 // 大于 - }, - "create_time": { - "gte": "2024-01-01T00:00:00Z" // 日期时间字符串 - } - } -} -``` - -**支持的操作符**: -- `gte`: 大于等于 (>=) -- `gt`: 大于 (>) -- `lte`: 小于等于 (<=) -- `lt`: 小于 (<) - -**注意**: 至少需要指定一个操作符。 - -**常用范围字段**: -- `min_price`: 最低价格 -- `max_price`: 最高价格 -- `compare_at_price`: 原价 -- `create_time`: 创建时间 -- `update_time`: 更新时间 - -#### 3. 分面配置 (facets) - -用于生成分面统计(分组聚合),常用于构建筛选器UI。 - -**简单模式**(字符串数组): -```json -{ - "facets": ["category_keyword", "vendor_keyword"] -} -``` - -**高级模式**(配置对象数组): -```json -{ - "facets": [ - { - "field": "category_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "min_price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ] -} -``` - -**分面配置参数**: -- `field`: 字段名(必填) -- `size`: 返回的分组数量(默认:10,范围:1-100) -- `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计) -- `ranges`: 范围定义(仅当 type='range' 时需要) - -### 布尔表达式语法 - -搜索查询支持布尔表达式,提供更灵活的搜索能力。 - -**支持的操作符**: - -| 操作符 | 描述 | 示例 | -|--------|------|------| -| `AND` | 所有词必须匹配 | `玩具 AND 乐高` | -| `OR` | 任意词匹配 | `芭比 OR 娃娃` | -| `ANDNOT` | 排除特定词 | `玩具 ANDNOT 电动` | -| `RANK` | 排序加权(不强制匹配) | `玩具 RANK 乐高` | -| `()` | 分组 | `玩具 AND (乐高 OR 芭比)` | - -**操作符优先级**(从高到低): -1. `()` - 括号 -2. `ANDNOT` - 排除 -3. `AND` - 与 -4. `OR` - 或 -5. `RANK` - 排序 - -**示例**: -``` -"芭比娃娃" // 简单查询 -"玩具 AND 乐高" // AND 查询 -"芭比 OR 娃娃" // OR 查询 -"玩具 ANDNOT 电动" // 排除查询 -"玩具 AND (乐高 OR 芭比)" // 复杂查询 -``` - ---- - -## 图片搜索接口 - -### 接口信息 - -- **端点**: `POST /search/image` -- **描述**: 基于图片相似度进行搜索,使用图片向量进行语义匹配 - -### 请求参数 - -```json -{ - "image_url": "string (required)", - "size": 10, - "filters": {}, - "range_filters": {} -} -``` - -### 参数说明 - -| 参数 | 类型 | 必填 | 默认值 | 描述 | -|------|------|------|--------|------| -| `image_url` | string | ✅ | - | 查询图片的 URL | -| `size` | integer | ❌ | 10 | 返回结果数量(1-100) | -| `filters` | object | ❌ | null | 精确匹配过滤器 | -| `range_filters` | object | ❌ | null | 数值范围过滤器 | - -### 请求示例 - -```bash -curl -X POST "http://localhost:6002/search/image" \ - -H "Content-Type: application/json" \ - -d '{ - "image_url": "https://example.com/barbie.jpg", - "size": 20, - "filters": { - "category_keyword": "玩具" - }, - "range_filters": { - "min_price": { - "lte": 100 - } - } - }' -``` - ---- - -## 响应格式说明 - -### 标准响应结构 - -```json -{ - "results": [ - { - "product_id": "12345", - "title": "芭比时尚娃娃", - "handle": "barbie-doll", - "description": "高品质芭比娃娃", - "vendor": "美泰", - "product_type": "玩具", - "tags": "娃娃, 玩具, 女孩", - "price": 89.99, - "compare_at_price": 129.99, - "currency": "USD", - "image_url": "https://example.com/image.jpg", - "in_stock": true, - "variants": [ - { - "variant_id": "67890", - "title": "粉色款", - "price": 89.99, - "compare_at_price": 129.99, - "sku": "BARBIE-001", - "stock": 100, - "options": { - "option1": "粉色", - "option2": "标准款" - } - } - ], - "relevance_score": 8.5 - } - ], - "total": 118, - "max_score": 8.5, - "facets": [ - { - "field": "category_keyword", - "label": "category_keyword", - "type": "terms", - "values": [ - { - "value": "玩具", - "label": "玩具", - "count": 85, - "selected": false - } - ] - } - ], - "query_info": { - "original_query": "芭比娃娃", - "detected_language": "zh", - "translations": { - "en": "barbie doll" - } - }, - "suggestions": [], - "related_searches": [], - "took_ms": 45, - "performance_info": null, - "debug_info": null -} -``` - -### 响应字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `results` | array | 搜索结果列表(ProductResult对象数组) | -| `results[].product_id` | string | 商品ID | -| `results[].title` | string | 商品标题 | -| `results[].price` | float | 价格(min_price) | -| `results[].variants` | array | 变体列表(SKU列表) | -| `results[].relevance_score` | float | 相关性分数 | -| `total` | integer | 匹配的总文档数 | -| `max_score` | float | 最高相关性分数 | -| `facets` | array | 分面统计结果 | -| `query_info` | object | 查询处理信息 | -| `took_ms` | integer | 搜索耗时(毫秒) | - -### ProductResult字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `product_id` | string | 商品ID(SPU ID) | -| `title` | string | 商品标题 | -| `handle` | string | 商品URL handle | -| `description` | string | 商品描述 | -| `vendor` | string | 供应商/品牌 | -| `product_type` | string | 商品类型 | -| `tags` | string | 标签 | -| `price` | float | 价格(min_price) | -| `compare_at_price` | float | 原价 | -| `currency` | string | 货币单位(默认USD) | -| `image_url` | string | 主图URL | -| `in_stock` | boolean | 是否有库存(任意变体有库存即为true) | -| `variants` | array | 变体列表 | -| `relevance_score` | float | 相关性分数 | - -### VariantResult字段说明 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `variant_id` | string | 变体ID(SKU ID) | -| `title` | string | 变体标题 | -| `price` | float | 价格 | -| `compare_at_price` | float | 原价 | -| `sku` | string | SKU编码 | -| `stock` | integer | 库存数量 | -| `options` | object | 选项(颜色、尺寸等) | - ---- - -## 常见场景示例 - -### 场景1:商品列表页搜索 - -**需求**: 搜索"玩具",按价格从低到高排序,显示前20个结果 - -```json -{ - "query": "玩具", - "size": 20, - "from": 0, - "sort_by": "min_price", - "sort_order": "asc" -} -``` - -### 场景2:带筛选的商品搜索 - -**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 - -```json -{ - "query": "玩具", - "size": 20, - "filters": { - "category_keyword": "益智玩具" - }, - "range_filters": { - "min_price": { - "gte": 50, - "lte": 200 - } - } -} -``` - -### 场景3:带分面的商品搜索 - -**需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器 - -```json -{ - "query": "玩具", - "size": 20, - "facets": [ - "category_keyword", - "vendor_keyword" - ] -} -``` - -### 场景4:多条件组合搜索 - -**需求**: 搜索"玩具",筛选多个品牌,价格范围,并获取分面统计 - -```json -{ - "query": "玩具", - "size": 20, - "filters": { - "vendor_keyword": ["乐高", "孩之宝", "美泰"] - }, - "range_filters": { - "min_price": { - "gte": 50, - "lte": 200 - } - }, - "facets": [ - { - "field": "category_keyword", - "size": 15 - }, - { - "field": "min_price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ], - "sort_by": "min_price", - "sort_order": "asc" -} -``` - -### 场景5:布尔表达式搜索 - -**需求**: 搜索包含"玩具"和"乐高"的商品,排除"电动" - -```json -{ - "query": "玩具 AND 乐高 ANDNOT 电动", - "size": 20 -} -``` - -### 场景6:分页查询 - -**需求**: 获取第2页结果(每页20条) - -```json -{ - "query": "玩具", - "size": 20, - "from": 20 -} -``` - ---- - -## 错误处理 - -### 错误响应格式 - -```json -{ - "error": "错误信息", - "detail": "详细错误信息(可选)" -} -``` - -### 常见错误码 - -| HTTP状态码 | 说明 | 处理建议 | -|-----------|------|---------| -| 200 | 成功 | - | -| 400 | 请求参数错误 | 检查请求参数格式和必填字段 | -| 404 | 接口不存在 | 检查接口路径 | -| 500 | 服务器内部错误 | 联系技术支持 | - -### 错误处理示例 - -**Python**: -```python -import requests - -try: - response = requests.post(url, json=payload, timeout=10) - response.raise_for_status() - data = response.json() -except requests.exceptions.HTTPError as e: - print(f"HTTP错误: {e}") - if response.status_code == 400: - error_data = response.json() - print(f"错误详情: {error_data.get('detail')}") -except requests.exceptions.RequestException as e: - print(f"请求异常: {e}") -``` - -**JavaScript**: -```javascript -try { - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || `HTTP ${response.status}`); - } - - const data = await response.json(); -} catch (error) { - console.error('搜索失败:', error.message); -} -``` - ---- - -### 5. 代码示例 - -**完整的搜索函数(Python)**: - -```python -import requests -from typing import Dict, Any, Optional, List - -class SearchClient: - def __init__(self, base_url: str = "http://localhost:6002"): - self.base_url = base_url - self.timeout = 10 - - def search( - self, - query: str, - size: int = 20, - from_: int = 0, - filters: Optional[Dict] = None, - range_filters: Optional[Dict] = None, - facets: Optional[List] = None, - sort_by: Optional[str] = None, - sort_order: str = "desc" - ) -> Dict[str, Any]: - """ - 执行搜索查询 - - Args: - query: 搜索查询字符串 - size: 返回结果数量 - from_: 分页偏移量 - filters: 精确匹配过滤器 - range_filters: 范围过滤器 - facets: 分面配置 - sort_by: 排序字段 - sort_order: 排序方向 - - Returns: - 搜索结果字典 - """ - url = f"{self.base_url}/search/" - payload = { - "query": query, - "size": size, - "from": from_, - } - - if filters: - payload["filters"] = filters - if range_filters: - payload["range_filters"] = range_filters - if facets: - payload["facets"] = facets - if sort_by: - payload["sort_by"] = sort_by - payload["sort_order"] = sort_order - - try: - response = requests.post( - url, - json=payload, - timeout=self.timeout - ) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - raise Exception(f"搜索请求失败: {e}") - -# 使用示例 -client = SearchClient() -result = client.search( - query="玩具", - size=20, - filters={"category_keyword": "益智玩具"}, - range_filters={"min_price": {"gte": 50, "lte": 200}}, - facets=["category_keyword", "vendor_keyword"], - sort_by="min_price", - sort_order="asc" -) - -print(f"找到 {result['total']} 个结果") -for product in result['results']: - print(f"{product['title']} - ¥{product['price']}") -``` - -**完整的搜索函数(JavaScript)**: - -```javascript -class SearchClient { - constructor(baseUrl = 'http://localhost:6002') { - this.baseUrl = baseUrl; - this.timeout = 10000; - } - - async search({ - query, - size = 20, - from = 0, - filters = null, - rangeFilters = null, - facets = null, - sortBy = null, - sortOrder = 'desc' - }) { - const url = `${this.baseUrl}/search/`; - const payload = { - query, - size, - from, - }; - - if (filters) payload.filters = filters; - if (rangeFilters) payload.range_filters = rangeFilters; - if (facets) payload.facets = facets; - if (sortBy) { - payload.sort_by = sortBy; - payload.sort_order = sortOrder; - } - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload), - signal: AbortSignal.timeout(this.timeout) - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || `HTTP ${response.status}`); - } - - return await response.json(); - } catch (error) { - throw new Error(`搜索请求失败: ${error.message}`); - } - } -} - -// 使用示例 -const client = new SearchClient(); -const result = await client.search({ - query: '玩具', - size: 20, - filters: { category_keyword: '益智玩具' }, - rangeFilters: { min_price: { gte: 50, lte: 200 } }, - facets: ['category_keyword', 'vendor_keyword'], - sortBy: 'min_price', - sortOrder: 'asc' -}); - -console.log(`找到 ${result.total} 个结果`); -result.results.forEach(product => { - console.log(`${product.title} - ¥${product.price}`); -}); -``` - ---- - -## 附录 - -### 常用字段列表 - -#### 过滤字段(使用 `*_keyword` 后缀) - -- `category_keyword`: 类目 -- `vendor_keyword`: 品牌/供应商 -- `product_type_keyword`: 商品类型 -- `tags_keyword`: 标签 - -#### 范围字段 - -- `min_price`: 最低价格 -- `max_price`: 最高价格 -- `compare_at_price`: 原价 -- `create_time`: 创建时间 -- `update_time`: 更新时间 - -#### 排序字段 - -- `min_price`: 最低价格 -- `max_price`: 最高价格 -- `title`: 标题(字母序) -- `create_time`: 创建时间 -- `update_time`: 更新时间 -- `relevance_score`: 相关性分数(默认) - diff --git a/API_QUICK_REFERENCE.md b/API_QUICK_REFERENCE.md deleted file mode 100644 index ba9f8d5..0000000 --- a/API_QUICK_REFERENCE.md +++ /dev/null @@ -1,233 +0,0 @@ -# API 快速参考 (v3.0) - -## 基础搜索 - -```bash -POST /search/ -{ - "query": "芭比娃娃", - "size": 20 -} -``` - ---- - -## 精确匹配过滤 - -```bash -{ - "filters": { - "categoryName_keyword": "玩具", // 单值 - "brandName_keyword": ["乐高", "美泰"] // 多值(OR) - } -} -``` - ---- - -## 范围过滤 - -```bash -{ - "range_filters": { - "price": { - "gte": 50, // >= - "lte": 200 // <= - } - } -} -``` - -**操作符**: `gte` (>=), `gt` (>), `lte` (<=), `lt` (<) - ---- - -## 分面搜索 - -### 简单模式 - -```bash -{ - "facets": ["categoryName_keyword", "brandName_keyword"] -} -``` - -### 高级模式 - -```bash -{ - "facets": [ - {"field": "categoryName_keyword", "size": 15}, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100} - ] - } - ] -} -``` - ---- - -## 排序 - -```bash -{ - "sort_by": "min_price", - "sort_order": "asc" // asc 或 desc -} -``` - ---- - -## 布尔表达式 - -```bash -{ - "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" -} -``` - -**操作符优先级**: `()` > `ANDNOT` > `AND` > `OR` > `RANK` - ---- - -## 分页 - -```bash -{ - "size": 20, // 每页数量 - "from": 0 // 偏移量(第1页=0,第2页=20) -} -``` - ---- - -## 完整示例 - -```bash -POST /search/ -{ - "query": "玩具", - "size": 20, - "from": 0, - "filters": { - "categoryName_keyword": ["玩具", "益智玩具"] - }, - "range_filters": { - "price": {"gte": 50, "lte": 200} - }, - "facets": [ - {"field": "brandName_keyword", "size": 15}, - {"field": "categoryName_keyword", "size": 15} - ], - "sort_by": "min_price", - "sort_order": "asc" -} -``` - ---- - -## 响应格式 - -```json -{ - "hits": [ - { - "_id": "12345", - "_score": 8.5, - "_source": {...} - } - ], - "total": 118, - "max_score": 8.5, - "took_ms": 45, - "facets": [ - { - "field": "categoryName_keyword", - "label": "商品类目", - "type": "terms", - "values": [ - { - "value": "玩具", - "label": "玩具", - "count": 85, - "selected": false - } - ] - } - ] -} -``` - ---- - -## 其他端点 - -```bash -POST /search/image -{ - "image_url": "https://example.com/image.jpg", - "size": 20 -} - -GET /search/suggestions?q=芭&size=5 - -GET /search/instant?q=玩具&size=5 - -GET /search/{doc_id} - -GET /admin/health -GET /admin/config -GET /admin/stats -``` - ---- - -## Python 快速示例 - -```python -import requests - -result = requests.post('http://localhost:6002/search/', json={ - "query": "玩具", - "filters": {"categoryName_keyword": "玩具"}, - "range_filters": {"price": {"gte": 50, "lte": 200}}, - "facets": ["brandName_keyword"], - "sort_by": "min_price", - "sort_order": "asc" -}).json() - -print(f"找到 {result['total']} 个结果") -``` - ---- - -## JavaScript 快速示例 - -```javascript -const result = await fetch('http://localhost:6002/search/', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - query: "玩具", - filters: {categoryName_keyword: "玩具"}, - range_filters: {price: {gte: 50, lte: 200}}, - facets: ["brandName_keyword"], - sort_by: "min_price", - sort_order: "asc" - }) -}).then(r => r.json()); - -console.log(`找到 ${result.total} 个结果`); -``` - ---- - -**详细文档**: [API_DOCUMENTATION.md](API_DOCUMENTATION.md) -**更多示例**: [API_EXAMPLES.md](API_EXAMPLES.md) -**在线文档**: http://localhost:6002/docs - diff --git a/BEST_PRACTICES_REFACTORING.md b/BEST_PRACTICES_REFACTORING.md deleted file mode 100644 index 9153cb4..0000000 --- a/BEST_PRACTICES_REFACTORING.md +++ /dev/null @@ -1,274 +0,0 @@ -# 最佳实践重构总结 - -**重构日期**: 2025-11-12 -**重构人员**: AI Assistant -**重构原则**: 代码简洁、类型安全、单一职责 - ---- - -## 重构目标 - -1. **移除兼容代码**:不再支持多种数据格式,统一使用最佳实践 -2. **修复前端422错误**:支持日期时间字符串的 range filter -3. **保持代码简洁**:单一数据流,清晰的类型定义 - ---- - -## 修改文件 - -### 1. `/home/tw/SearchEngine/api/models.py` - -#### 修改:RangeFilter 模型 - -**之前**:只支持数值 (float) -```python -gte: Optional[float] = Field(None, description="大于等于 (>=)") -``` - -**之后**:支持数值和日期时间字符串 -```python -gte: Optional[Union[float, str]] = Field(None, description="大于等于 (>=)。数值或ISO日期时间字符串") -``` - -**理由**: -- 价格字段需要数值过滤:`{"gte": 50, "lte": 200}` -- 时间字段需要字符串过滤:`{"gte": "2023-01-01T00:00:00Z"}` -- 使用 `Union[float, str]` 同时支持两种类型 - ---- - -### 2. `/home/tw/SearchEngine/search/es_query_builder.py` - -#### 修改:build_facets 方法 - -**之前**:兼容字典和 Pydantic 模型 -```python -else: - if isinstance(config, dict): - field = config['field'] - facet_type = config.get('type', 'terms') - ... - else: - # Pydantic模型 - field = config.field - facet_type = config.type - ... -``` - -**之后**:只支持标准格式(str 或 FacetConfig) -```python -# 简单模式:只有字段名(字符串) -if isinstance(config, str): - field = config - ... - -# 高级模式:FacetConfig 对象 -else: - field = config.field - facet_type = config.type - ... -``` - -**理由**: -- API 层已经定义标准格式:`List[Union[str, FacetConfig]]` -- 移除字典支持,保持单一数据流 -- 代码更简洁,意图更清晰 - ---- - -### 3. `/home/tw/SearchEngine/search/searcher.py` - -#### 修改:_standardize_facets 方法 - -**之前**:兼容字典和 Pydantic 模型 -```python -else: - field = config.get('field') if isinstance(config, dict) else config.field - facet_type = config.get('type', 'terms') if isinstance(config, dict) else getattr(config, 'type', 'terms') -``` - -**之后**:只支持标准格式 -```python -else: - # FacetConfig 对象 - field = config.field - facet_type = config.type -``` - -**理由**: -- 与 build_facets 保持一致 -- 移除冗余的类型检查 -- 代码更清晰易读 - ---- - -## 数据流设计 - -### 标准数据流(最佳实践) - -``` -API Request - ↓ -Pydantic 验证 (SearchRequest) - ↓ facets: List[Union[str, FacetConfig]] -Searcher.search() - ↓ -QueryBuilder.build_facets() - ↓ 只接受 str 或 FacetConfig -ES Query (aggs) - ↓ -ES Response (aggregations) - ↓ -Searcher._standardize_facets() - ↓ 只处理 str 或 FacetConfig -List[FacetResult] (Pydantic 模型) - ↓ -SearchResponse - ↓ -API Response (JSON) -``` - -### 类型定义 - -```python -# API 层 -facets: Optional[List[Union[str, FacetConfig]]] = None - -# Query Builder 层 -def build_facets(facet_configs: Optional[List[Union[str, 'FacetConfig']]]) - -# Searcher 层 -def _standardize_facets(...) -> Optional[List[FacetResult]] - -# Response 层 -facets: Optional[List[FacetResult]] = None -``` - ---- - -## 测试结果 - -### ✅ 所有测试通过 - -| 测试场景 | 状态 | 说明 | -|---------|------|------| -| 字符串 facets | ✓ | `["categoryName_keyword"]` | -| FacetConfig 对象 | ✓ | `{"field": "price", "type": "range", ...}` | -| 数值 range filter | ✓ | `{"gte": 50, "lte": 200}` | -| 日期时间 range filter | ✓ | `{"gte": "2023-01-01T00:00:00Z"}` | -| 混合使用 | ✓ | filters + range_filters + facets | - -### ✅ 前端 422 错误已修复 - -**问题**:前端筛选 listing time 时返回 422 Unprocessable Entity - -**原因**:`RangeFilter` 只接受 `float`,不接受日期时间字符串 - -**解决**:修改 `RangeFilter` 支持 `Union[float, str]` - -**验证**: -```bash -curl -X POST /search/ \ - -d '{"query": "玩具", "range_filters": {"create_time": {"gte": "2023-01-01T00:00:00Z"}}}' -# ✓ 200 OK -``` - ---- - -## 代码质量 - -### ✅ 无 Linter 错误 -```bash -No linter errors found. -``` - -### ✅ 类型安全 -- 所有类型明确定义 -- Pydantic 自动验证 -- IDE 类型提示完整 - -### ✅ 代码简洁 -- 移除所有兼容代码 -- 单一数据格式 -- 清晰的控制流 - ---- - -## 最佳实践原则 - -### 1. **单一职责** -每个方法只处理一种标准格式,不兼容多种输入 - -### 2. **类型明确** -使用 Pydantic 模型而不是字典,类型在编译时就确定 - -### 3. **数据流清晰** -API → Pydantic → 业务逻辑 → Pydantic → Response - -### 4. **早期验证** -在 API 层就验证数据,不在内部做多重兼容检查 - -### 5. **代码可维护** -- 删除冗余代码 -- 保持一致性 -- 易于理解和修改 - ---- - -## 对比总结 - -| 方面 | 重构前 | 重构后 | -|------|-------|--------| -| **数据格式** | 字典 + Pydantic(兼容) | 只有 Pydantic | -| **类型检查** | 运行时多重检查 | 编译时类型明确 | -| **代码行数** | 更多(兼容代码) | 更少(单一逻辑) | -| **可维护性** | 复杂(多种路径) | 简单(单一路径) | -| **错误处理** | 隐式容错 | 明确验证 | -| **RangeFilter** | 只支持数值 | 支持数值+字符串 | -| **前端兼容** | 422 错误 | 完全兼容 | - ---- - -## 后续建议 - -### 1. 代码审查 -- [x] 移除所有字典兼容代码 -- [x] 统一使用 Pydantic 模型 -- [x] 修复前端 422 错误 - -### 2. 测试覆盖 -- [x] 字符串 facets -- [x] FacetConfig 对象 -- [x] 数值 range filter -- [x] 日期时间 range filter -- [x] 混合场景 - -### 3. 文档更新 -- [x] 最佳实践文档 -- [x] 数据流设计 -- [ ] API 文档更新 - -### 4. 性能优化 -- [ ] 添加请求缓存 -- [ ] 优化 Pydantic 验证 -- [ ] 监控性能指标 - ---- - -## 结论 - -本次重构成功实现了以下目标: - -✅ **代码简洁**:移除所有兼容代码,保持单一数据流 -✅ **类型安全**:统一使用 Pydantic 模型,编译时类型检查 -✅ **功能完整**:修复前端 422 错误,支持所有筛选场景 -✅ **可维护性**:代码清晰,易于理解和修改 - -**核心原则**:*不做兼容多种方式的代码,定义一种最佳实践,所有模块都适配这种新方式* - ---- - -**版本**: v3.1 -**状态**: ✅ 完成并通过测试 -**下次更新**: 根据业务需求扩展 - diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100644 index 6bbb8f6..0000000 --- a/CHANGES.md +++ /dev/null @@ -1,611 +0,0 @@ -# 变更日志 (CHANGELOG) - ---- - -## v3.0 - API 接口重构 (2024-11-12) - -### 重大更新 - -本版本对搜索 API 进行了全面重构,移除硬编码逻辑,实现了灵活通用的 SaaS 接口设计。 - -#### ❌ 破坏性变更(不向后兼容) - -1. **移除硬编码的 price_ranges 参数** - - 旧方式:`filters: {"price_ranges": ["0-50", "50-100"]}` - - 新方式:`range_filters: {"price": {"gte": 50, "lte": 100}}` - -2. **移除 aggregations 参数** - - 旧方式:`aggregations: {"category_stats": {"terms": {...}}}`(ES DSL) - - 新方式:`facets: [{"field": "categoryName_keyword", "size": 15}]`(简化配置) - -3. **响应格式变更** - - 旧方式:`response.aggregations`(ES 原始格式) - - 新方式:`response.facets`(标准化格式) - -#### ✅ 新增功能 - -1. **结构化过滤参数** - - 新增 `range_filters` 参数:支持任意数值字段的范围过滤 - - 支持 `gte`, `gt`, `lte`, `lt` 操作符 - - 示例: - ```json - { - "range_filters": { - "price": {"gte": 50, "lte": 200}, - "days_since_last_update": {"lte": 30} - } - } - ``` - -2. **简化的分面配置** - - 新增 `facets` 参数:替代复杂的 ES DSL - - 支持简单模式(字符串数组)和高级模式(配置对象) - - 示例: - ```json - { - "facets": [ - "categoryName_keyword", // 简单模式 - {"field": "brandName_keyword", "size": 15} // 高级模式 - ] - } - ``` - -3. **标准化分面响应** - - 统一的分面结果格式 - - 包含 `field`, `label`, `type`, `values` - - `values` 包含 `value`, `label`, `count`, `selected` - - 示例: - ```json - { - "facets": [ - { - "field": "categoryName_keyword", - "label": "商品类目", - "type": "terms", - "values": [ - {"value": "玩具", "label": "玩具", "count": 85, "selected": false} - ] - } - ] - } - ``` - -4. **新增搜索建议端点**(框架) - - `GET /search/suggestions`: 自动补全 - - `GET /search/instant`: 即时搜索 - - 注:暂未实现,仅返回框架响应 - -#### 🔧 代码改进 - -1. **后端模型层** (`api/models.py`) - - 新增 `RangeFilter` 模型 - - 新增 `FacetConfig` 模型 - - 新增 `FacetValue` 和 `FacetResult` 模型 - - 更新 `SearchRequest` 和 `SearchResponse` - -2. **查询构建器** (`search/es_query_builder.py`) - - **完全移除** 硬编码的 `price_ranges` 逻辑(第 205-233 行) - - 重构 `_build_filters` 方法,支持 `range_filters` - - **删除** `add_dynamic_aggregations` 方法 - - 新增 `build_facets` 方法 - -3. **搜索执行层** (`search/searcher.py`) - - 更新 `search()` 方法签名 - - 新增 `_standardize_facets()` 方法 - - 新增 `_get_field_label()` 方法 - - 更新 `SearchResult` 类 - -4. **API 路由层** (`api/routes/search.py`) - - 更新所有搜索端点 - - 新增 `/search/suggestions` 端点 - - 新增 `/search/instant` 端点 - -5. **前端代码** (`frontend/static/js/app.js`) - - 更新状态管理,添加 `rangeFilters` - - 完全重写 `displayAggregations` 为 `displayFacets` - - 更新过滤器处理逻辑 - - 删除所有硬编码的 `price_ranges` - -#### 📚 文档更新 - -- 新增 `API_DOCUMENTATION.md`:完整的 API 接口文档 -- 更新 `README.md`:添加 v3.0 新功能说明 -- 更新 `USER_GUIDE.md`:更新 API 使用示例 - -#### 🎯 改进要点 - -**从特定实现到通用 SaaS**: -- ❌ 移除:硬编码的价格范围值 -- ❌ 移除:暴露 ES DSL 的聚合接口 -- ❌ 移除:不统一的响应格式 -- ✅ 新增:通用的范围过滤器 -- ✅ 新增:简化的分面配置 -- ✅ 新增:标准化的响应格式 - -#### 📋 迁移指南 - -**旧接口** → **新接口** - -1. **过滤器迁移**: - ```json - // 旧 - {"filters": {"price_ranges": ["50-100"]}} - - // 新 - {"range_filters": {"price": {"gte": 50, "lte": 100}}} - ``` - -2. **聚合迁移**: - ```json - // 旧 - { - "aggregations": { - "category_stats": { - "terms": {"field": "categoryName_keyword", "size": 15} - } - } - } - - // 新 - { - "facets": [ - {"field": "categoryName_keyword", "size": 15} - ] - } - ``` - -3. **响应解析迁移**: - ```javascript - // 旧 - data.aggregations.category_stats.buckets.forEach(bucket => { - console.log(bucket.key, bucket.doc_count); - }); - - // 新 - data.facets.forEach(facet => { - facet.values.forEach(value => { - console.log(value.value, value.count); - }); - }); - ``` - ---- - -## v2.x - 前端优化更新 (2025-11-11) - -## 概述 -基于提供的电商搜索引擎参考图片,对前端界面进行了全面重新设计和优化,采用更现代、简洁的布局风格。 - ---- - -## 修改的文件 - -### 1. `/home/tw/SearchEngine/frontend/index.html` ✅ 完全重写 -**更改内容:** -- 去除旧的搜索示例和复杂布局 -- 添加简洁的顶部标题栏(Product + 商品数量 + Fold按钮) -- 重新设计搜索栏(更简洁) -- 添加水平筛选标签区域(Categories, Brand, Supplier) -- 添加排序工具栏(带上下箭头的排序按钮) -- 改用网格布局展示商品 -- 添加分页组件 -- 将查询信息改为可折叠的Debug区域 - -**关键改进:** -```html - -
-
- - 0 products found -
-
- -
-
- - -
-
-
Categories:
-
-
- -
- - -
- -
- - -
- - - -``` - ---- - -### 2. `/home/tw/SearchEngine/frontend/static/css/style.css` ✅ 完全重写 -**更改内容:** -- 去除紫色渐变背景,改为白色简洁背景 -- 重新设计所有组件样式 -- 添加顶部标题栏样式 -- 添加水平筛选标签样式(带hover和active状态) -- 添加排序按钮样式(带箭头) -- 重新设计商品卡片样式(网格布局) -- 添加分页样式 -- 优化响应式设计 - -**关键样式:** -```css -/* 白色背景 */ -body { - background: #f5f5f5; -} - -/* 筛选标签 */ -.filter-tag { - padding: 6px 15px; - background: #f8f8f8; - border: 1px solid #ddd; - cursor: pointer; -} - -.filter-tag.active { - background: #e74c3c; - color: white; -} - -/* 排序箭头 */ -.sort-arrows { - display: inline-flex; - flex-direction: column; - font-size: 10px; -} - -/* 商品网格 */ -.product-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 20px; -} - -/* 商品卡片 */ -.product-card { - background: white; - border: 1px solid #e0e0e0; - border-radius: 8px; - transition: all 0.3s; -} - -.product-card:hover { - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - transform: translateY(-2px); -} -``` - -**代码量对比:** -- 旧版:433行 -- 新版:450行 -- 变化:+17行(增加了更多功能和响应式样式) - ---- - -### 3. `/home/tw/SearchEngine/frontend/static/js/app.js` ✅ 完全重构 -**更改内容:** -- 添加状态管理对象(统一管理所有状态) -- 重写搜索函数(支持分页) -- 重写结果展示函数(商品网格布局) -- 重写筛选聚合函数(水平标签展示) -- 添加排序函数(支持字段+方向) -- 添加分页函数(完整分页导航) -- 优化代码结构(更模块化) - -**关键功能:** -```javascript -// 状态管理 -let state = { - query: '', - currentPage: 1, - pageSize: 20, - totalResults: 0, - filters: {}, - sortBy: '', - sortOrder: 'desc', - aggregations: null -}; - -// 排序函数(支持上下箭头) -function sortByField(field, order) { - state.sortBy = field; - state.sortOrder = order; - performSearch(state.currentPage); -} - -// 分页函数 -function goToPage(page) { - performSearch(page); - window.scrollTo({ top: 0, behavior: 'smooth' }); -} - -// 商品网格展示 -function displayResults(data) { - // 生成商品卡片HTML - data.hits.forEach((hit) => { - html += ` -
-
...
-
...
-
...
-
...
-
- `; - }); -} - -// 水平筛选标签 -function displayAggregations(aggregations) { - // 显示为可点击的标签 - html += ` - - ${key} (${count}) - - `; -} -``` - -**代码量对比:** -- 旧版:516行 -- 新版:465行 -- 变化:-51行(代码更简洁,功能更强) - ---- - -### 4. `/home/tw/SearchEngine/api/app.py` ✅ 添加静态文件服务 -**更改内容:** -- 导入 `FileResponse` 和 `StaticFiles` -- 添加前端HTML服务路由 -- 挂载静态文件目录(CSS, JS) -- 将原有的 `/` 路由改为 `/api` - -**关键代码:** -```python -from fastapi.responses import FileResponse -from fastapi.staticfiles import StaticFiles - -# 在文件末尾添加 -frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "frontend") -if os.path.exists(frontend_path): - # 服务前端HTML - @app.get("/") - async def serve_frontend(): - index_path = os.path.join(frontend_path, "index.html") - if os.path.exists(index_path): - return FileResponse(index_path) - - # 挂载静态文件 - app.mount("/static", StaticFiles(directory=os.path.join(frontend_path, "static")), name="static") -``` - ---- - -## 新增的文件 - -### 5. `/home/tw/SearchEngine/frontend/README.md` ✅ 新建 -前端详细文档,包含: -- 优化说明 -- 功能介绍 -- 使用方法 -- 技术特点 -- 浏览器兼容性 -- 未来改进计划 - -### 6. `/home/tw/SearchEngine/FRONTEND_GUIDE.md` ✅ 新建 -快速上手指南,包含: -- 优化总结 -- 启动方法 -- 测试步骤 -- 常见问题 -- API接口说明 -- 性能指标 - -### 7. `/home/tw/SearchEngine/scripts/test_frontend.sh` ✅ 新建 -自动化测试脚本,测试: -- 健康检查 -- 前端HTML -- CSS文件 -- JavaScript文件 -- 搜索API - -### 8. `/home/tw/SearchEngine/CHANGES.md` ✅ 新建 -本文件,记录所有更改。 - ---- - -## 功能对比表 - -| 功能 | 旧版前端 | 新版前端 | 状态 | -|------|---------|---------|------| -| 背景颜色 | 紫色渐变 | 白色简洁 | ✅ 优化 | -| 顶部标题栏 | 大标题+副标题 | Product + 商品数 | ✅ 优化 | -| 搜索框 | 带多个选项 | 简洁搜索框 | ✅ 优化 | -| 筛选方式 | 左侧垂直面板 | 顶部水平标签 | ✅ 优化 | -| 筛选交互 | 复选框 | 可点击标签 | ✅ 优化 | -| 排序方式 | 下拉选择 | 按钮+箭头 | ✅ 优化 | -| 商品展示 | 列表布局 | 网格布局 | ✅ 优化 | -| 商品卡片 | 横向卡片 | 垂直卡片 | ✅ 优化 | -| 分页功能 | ❌ 无 | ✅ 完整分页 | ✅ 新增 | -| 响应式设计 | 基础支持 | 完整响应式 | ✅ 优化 | -| 代码结构 | 混乱 | 模块化 | ✅ 优化 | -| 状态管理 | 分散 | 统一管理 | ✅ 优化 | - ---- - -## 技术改进 - -### 前端架构 -- ✅ **状态管理**:统一的state对象 -- ✅ **模块化**:功能清晰分离 -- ✅ **代码简化**:去除冗余代码 -- ✅ **性能优化**:减少DOM操作 - -### UI/UX设计 -- ✅ **视觉一致性**:统一的设计语言 -- ✅ **交互直观**:标签式筛选,箭头排序 -- ✅ **响应迅速**:即时反馈 -- ✅ **移动友好**:完整的响应式支持 - -### 代码质量 -- ✅ **可维护性**:清晰的结构 -- ✅ **可扩展性**:易于添加新功能 -- ✅ **可读性**:注释完整 -- ✅ **无linter错误**:代码规范 - ---- - -## 测试步骤 - -### 1. 启动服务 -```bash -cd /home/tw/SearchEngine -bash scripts/start_backend.sh -``` - -### 2. 运行测试 -```bash -bash scripts/test_frontend.sh -``` - -### 3. 手动测试 -访问:`http://120.76.41.98:6002/` - -测试项目: -- [ ] 页面正常加载 -- [ ] 搜索功能正常 -- [ ] 筛选标签可点击 -- [ ] 排序箭头可用 -- [ ] 商品网格展示正常 -- [ ] 分页功能正常 -- [ ] 响应式布局正常 - ---- - -## 兼容性 - -### 浏览器 -- ✅ Chrome 90+ -- ✅ Firefox 88+ -- ✅ Safari 14+ -- ✅ Edge 90+ -- ✅ 移动浏览器 - -### 屏幕尺寸 -- ✅ 桌面(1920x1080) -- ✅ 笔记本(1366x768) -- ✅ 平板(768x1024) -- ✅ 手机(375x667) - ---- - -## 性能指标 - -| 指标 | 旧版 | 新版 | 改进 | -|------|------|------|------| -| 首屏加载 | ~1.5s | ~0.8s | ⬇️ 47% | -| JavaScript大小 | 15KB | 13KB | ⬇️ 13% | -| CSS大小 | 12KB | 11KB | ⬇️ 8% | -| DOM节点数 | ~350 | ~200 | ⬇️ 43% | -| 重绘次数 | 高 | 低 | ⬆️ 优化 | - ---- - -## 最佳实践应用 - -### HTML -- ✅ 语义化标签 -- ✅ 无障碍支持(ARIA) -- ✅ SEO友好 - -### CSS -- ✅ CSS Grid布局 -- ✅ Flexbox布局 -- ✅ CSS变量 -- ✅ 媒体查询(响应式) - -### JavaScript -- ✅ ES6+语法 -- ✅ 事件委托 -- ✅ 防抖/节流(如需要) -- ✅ 错误处理 - ---- - -## 下一步优化建议 - -### 短期(1-2周) -- [ ] 添加加载骨架屏 -- [ ] 优化图片懒加载 -- [ ] 添加搜索建议(自动完成) - -### 中期(1个月) -- [ ] 添加用户偏好设置 -- [ ] 支持多主题切换 -- [ ] 添加商品收藏功能 - -### 长期(3个月) -- [ ] PWA支持(离线访问) -- [ ] 国际化(多语言) -- [ ] 性能监控 - ---- - -## 回滚方案 - -如需回滚到旧版: - -```bash -cd /home/tw/SearchEngine -git checkout HEAD~1 frontend/ -# 或从备份恢复 -``` - ---- - -## 总结 - -### 完成情况 -- ✅ HTML重构:100% -- ✅ CSS重写:100% -- ✅ JavaScript重构:100% -- ✅ 后端适配:100% -- ✅ 文档编写:100% -- ✅ 测试脚本:100% - -### 核心成果 -1. **更好的用户体验**:简洁、直观的界面 -2. **更强的功能**:完整的筛选、排序、分页 -3. **更好的代码**:模块化、可维护 -4. **更好的性能**:更快的加载和响应 - -### 达成目标 -✅ 完全符合参考图片的布局风格 -✅ 实现了所有要求的功能 -✅ 遵循了最佳实践 -✅ 代码质量高,易于维护 -✅ 响应式设计,支持多端 - ---- - -**优化完成时间**:2025-11-11 -**总耗时**:约2小时 -**状态**:✅ 生产就绪 - diff --git a/INDEX_FIELDS_DOCUMENTATION.md b/INDEX_FIELDS_DOCUMENTATION.md deleted file mode 100644 index 3b85168..0000000 --- a/INDEX_FIELDS_DOCUMENTATION.md +++ /dev/null @@ -1,223 +0,0 @@ -# 索引字段说明文档 - -本文档详细说明了 Elasticsearch 索引中所有字段的类型、索引方式、数据来源等信息。 - -## 索引基本信息 - -- **索引名称**: `search_products` -- **索引级别**: SPU级别(商品级别) -- **数据结构**: SPU文档包含嵌套的variants(SKU)数组 - -## 字段说明表 - -### 基础字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|------| -| tenant_id | KEYWORD | 是 | 精确匹配 | SPU表 | tenant_id | BIGINT | 租户ID,用于多租户隔离 | -| product_id | KEYWORD | 是 | 精确匹配 | SPU表 | id | BIGINT | 商品ID(SPU ID) | -| handle | KEYWORD | 是 | 精确匹配 | SPU表 | handle | VARCHAR(255) | 商品URL handle | - -### 文本搜索字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|------| -| title | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | title | VARCHAR(512) | 3.0 | 商品标题,权重最高 | -| brief | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | brief | VARCHAR(512) | 1.5 | 商品简介 | -| description | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | description | TEXT | 1.0 | 商品详细描述 | - -### SEO字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------| -| seo_title | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_title | VARCHAR(512) | 2.0 | 否 | SEO标题,用于提升相关性 | -| seo_description | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_description | TEXT | 1.5 | 否 | SEO描述 | -| seo_keywords | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_keywords | VARCHAR(1024) | 2.0 | 否 | SEO关键词 | - -### 分类和标签字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------| -| vendor | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | 供应商/品牌(文本搜索) | -| vendor_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | vendor | VARCHAR(255) | - | 否 | 供应商/品牌(精确匹配,用于过滤) | -| product_type | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 商品类型(文本搜索) | -| product_type_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 商品类型(精确匹配,用于过滤) | -| tags | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | 标签(文本搜索) | -| tags_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | tags | VARCHAR(1024) | - | 否 | 标签(精确匹配,用于过滤) | -| category | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 类目(文本搜索) | -| category_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 类目(精确匹配,用于过滤) | - -### 价格字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|------| -| min_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | price | DECIMAL(10,2) | 最低价格(从所有SKU中取最小值) | -| max_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | price | DECIMAL(10,2) | 最高价格(从所有SKU中取最大值) | -| compare_at_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | compare_at_price | DECIMAL(10,2) | 原价(从所有SKU中取最大值) | - -**价格计算逻辑**: -- `min_price`: 取该SPU下所有SKU的price字段的最小值 -- `max_price`: 取该SPU下所有SKU的price字段的最大值 -- `compare_at_price`: 取该SPU下所有SKU的compare_at_price字段的最大值(如果存在) - -### 图片字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|------| -| image_url | KEYWORD | 否 | 不索引 | SPU表 | image_src | VARCHAR(500) | 商品主图URL,仅用于展示 | - -### 文本嵌入字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|------| -| title_embedding | TEXT_EMBEDDING | 是 | 向量相似度(dot_product) | 计算生成 | title | VARCHAR(512) | 标题的文本向量(1024维),用于语义搜索 | - -**说明**: -- 向量维度:1024 -- 相似度算法:dot_product(点积) -- 数据来源:基于title字段通过BGE-M3模型生成 - -### 时间字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 是否返回 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|---------|------| -| create_time | DATE | 是 | 日期范围 | SPU表 | create_time | DATETIME | 是 | 创建时间 | -| update_time | DATE | 是 | 日期范围 | SPU表 | update_time | DATETIME | 是 | 更新时间 | -| shoplazza_created_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_created_at | DATETIME | 否 | 店匠系统创建时间 | -| shoplazza_updated_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_updated_at | DATETIME | 否 | 店匠系统更新时间 | - -### 嵌套Variants字段(SKU级别) - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|------| -| variants | JSON (nested) | 是 | 嵌套对象 | SKU表 | - | - | 商品变体数组(嵌套结构) | - -#### Variants子字段 - -| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | -|-----------|-----------|---------|---------|-----------|-----------|-------------|------| -| variants.variant_id | keyword | 是 | 精确匹配 | SKU表 | id | BIGINT | 变体ID(SKU ID) | -| variants.title | text | 是 | chinese_ecommerce分析器 | SKU表 | title | VARCHAR(500) | 变体标题 | -| variants.price | float | 是 | 数值范围 | SKU表 | price | DECIMAL(10,2) | 变体价格 | -| variants.compare_at_price | float | 是 | 数值范围 | SKU表 | compare_at_price | DECIMAL(10,2) | 变体原价 | -| variants.sku | keyword | 是 | 精确匹配 | SKU表 | sku | VARCHAR(100) | SKU编码 | -| variants.stock | long | 是 | 数值范围 | SKU表 | inventory_quantity | INT(11) | 库存数量 | -| variants.options | object | 是 | 对象 | SKU表 | option1/option2/option3 | VARCHAR(255) | 选项(颜色、尺寸等) | - -**Variants结构说明**: -- `variants` 是一个嵌套对象数组,每个元素代表一个SKU -- 使用ES的nested类型,支持对嵌套字段进行独立查询和过滤 -- `options` 对象包含 `option1`、`option2`、`option3` 三个字段,分别对应SKU表中的选项值 - -## 字段类型说明 - -### ES字段类型映射 - -| ES字段类型 | Elasticsearch映射 | 用途 | -|-----------|------------------|------| -| KEYWORD | keyword | 精确匹配、过滤、聚合、排序 | -| TEXT | text | 全文检索(支持分词) | -| FLOAT | float | 浮点数(价格、权重等) | -| LONG | long | 整数(库存、计数等) | -| DATE | date | 日期时间 | -| TEXT_EMBEDDING | dense_vector | 文本向量(1024维) | -| JSON | object/nested | 嵌套对象 | - -### 分析器说明 - -| 分析器名称 | 语言 | 说明 | -|-----------|------|------| -| chinese_ecommerce | 中文 | Ansj中文分词器(电商优化),用于中文文本的分词和搜索 | - -## 索引配置 - -### 索引设置 - -- **分片数**: 1 -- **副本数**: 0 -- **刷新间隔**: 30秒 - -### 查询域(Query Domains) - -系统定义了多个查询域,用于在不同场景下搜索不同的字段组合: - -1. **default(默认索引)**: 搜索所有文本字段 - - 包含字段:title, brief, description, seo_title, seo_description, seo_keywords, vendor, product_type, tags, category - - Boost: 1.0 - -2. **title(标题索引)**: 仅搜索标题相关字段 - - 包含字段:title, seo_title - - Boost: 2.0 - -3. **vendor(品牌索引)**: 仅搜索品牌字段 - - 包含字段:vendor - - Boost: 1.5 - -4. **category(类目索引)**: 仅搜索类目字段 - - 包含字段:category - - Boost: 1.5 - -5. **tags(标签索引)**: 搜索标签和SEO关键词 - - 包含字段:tags, seo_keywords - - Boost: 1.0 - -## 数据转换规则 - -### 数据类型转换 - -1. **BIGINT → KEYWORD**: 数字ID转换为字符串(如 `product_id`, `variant_id`) -2. **DECIMAL → FLOAT**: 价格字段从DECIMAL转换为FLOAT -3. **INT → LONG**: 库存数量从INT转换为LONG -4. **DATETIME → DATE**: 时间字段转换为ISO格式字符串 - -### 特殊处理 - -1. **价格聚合**: 从多个SKU的价格中计算min_price、max_price、compare_at_price -2. **图片URL处理**: 如果image_src不是完整URL,会自动添加协议前缀 -3. **选项合并**: 将SKU表的option1、option2、option3合并为options对象 - -## 注意事项 - -1. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 -2. **嵌套查询**: 查询variants字段时需要使用nested查询语法 -3. **字段命名**: 用于过滤的字段应使用 `*_keyword` 后缀的字段 -4. **向量搜索**: title_embedding字段用于语义搜索,需要配合文本查询使用 -5. **Boost权重**: 不同字段的boost权重影响搜索结果的相关性排序 - -## 数据来源表结构 - -### SPU表(shoplazza_product_spu) - -主要字段: -- `id`: BIGINT - 主键ID -- `tenant_id`: BIGINT - 租户ID -- `handle`: VARCHAR(255) - URL handle -- `title`: VARCHAR(512) - 商品标题 -- `brief`: VARCHAR(512) - 商品简介 -- `description`: TEXT - 商品描述 -- `vendor`: VARCHAR(255) - 供应商/品牌 -- `category`: VARCHAR(255) - 类目 -- `tags`: VARCHAR(1024) - 标签 -- `seo_title`: VARCHAR(512) - SEO标题 -- `seo_description`: TEXT - SEO描述 -- `seo_keywords`: VARCHAR(1024) - SEO关键词 -- `image_src`: VARCHAR(500) - 图片URL -- `create_time`: DATETIME - 创建时间 -- `update_time`: DATETIME - 更新时间 -- `shoplazza_created_at`: DATETIME - 店匠创建时间 -- `shoplazza_updated_at`: DATETIME - 店匠更新时间 - -### SKU表(shoplazza_product_sku) - -主要字段: -- `id`: BIGINT - 主键ID(对应variant_id) -- `spu_id`: BIGINT - SPU ID(关联字段) -- `title`: VARCHAR(500) - 变体标题 -- `price`: DECIMAL(10,2) - 价格 -- `compare_at_price`: DECIMAL(10,2) - 原价 -- `sku`: VARCHAR(100) - SKU编码 -- `inventory_quantity`: INT(11) - 库存数量 -- `option1`: VARCHAR(255) - 选项1 -- `option2`: VARCHAR(255) - 选项2 -- `option3`: VARCHAR(255) - 选项3 - diff --git a/README.md b/README.md index fa3060c..1aa5d31 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@ | 步骤 | 去哪里看 | 摘要 | |------|---------|------| -| 1. 准备环境 | `环境相关.md` / `USAGE_GUIDE.md` | Conda/依赖、Elasticsearch、MySQL、必需的变量 | -| 2. 构造测试数据 | `TEST_DATA_GUIDE.md` | Tenant1 Mock、Tenant2 CSV、`mock_data.sh` / `ingest.sh` | -| 3. 启动与验证 | `USAGE_GUIDE.md` | `run.sh` 一键启动、分步脚本、日志与健康检查 | -| 4. 理解架构 | `设计文档.md` | 数据流、配置系统、查询/搜索/索引模块 | -| 5. 接入搜索 API | `API_DOCUMENTATION.md` / `API_INTEGRATION_GUIDE.md` | REST 端点、参数、响应、最佳实践 | -| 6. 查字段定义 | `INDEX_FIELDS_DOCUMENTATION.md` | `search_products` 映射、字段来源、类型与用途 | +| 1. 准备环境 | `环境配置说明.md` / `Usage-Guide.md` | Conda/依赖、Elasticsearch、MySQL、必需变量 | +| 2. 构造测试数据 | `测试数据指南.md` | Tenant1 Mock、Tenant2 CSV、`mock_data.sh` / `ingest.sh` | +| 3. 启动与验证 | `Usage-Guide.md` | `run.sh` 一键启动、分步脚本、日志与健康检查 | +| 4. 理解架构 | `系统设计文档.md` | 数据流、配置系统、查询/搜索/索引模块 | +| 5. 接入搜索 API | `搜索API对接指南.md` / `搜索API速查表.md` | REST 端点、参数、响应、最佳实践 | +| 6. 查字段定义 | `索引字段说明.md` | `search_products` 映射、字段来源、类型与用途 | > README 仅保留最常用命令的“索引”。细节以主题文档为准。 @@ -52,19 +52,21 @@ curl -X POST http://localhost:6002/search/ \ | 文档 | 内容提要 | 适用场景 | |------|----------|----------| -| `环境相关.md` | 系统要求、Conda/依赖、外部服务账号、常用端口 | 首次部署、环境核对 | -| `USAGE_GUIDE.md` | 环境准备、服务启动、配置、日志、验证手册 | 日常运维、调试 | -| `TEST_DATA_GUIDE.md` | 两个租户的模拟/CSV数据构造 & MySQL→ES流程 | 数据准备、联调 | -| `设计文档.md` | 架构、配置系统、索引/查询/排序模块细节 | 研发/扩展功能 | -| `INDEX_FIELDS_DOCUMENTATION.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 | -| `API_DOCUMENTATION.md` | REST API(搜索/图片/管理)详解、示例、响应格式 | API 使用、测试 | -| `API_INTEGRATION_GUIDE.md` | 客户对接指引、最佳实践、错误处理、语言说明 | 第三方集成、SDK 开发 | -| `API_QUICK_REFERENCE.md` | 常用请求体速查表 | 支持团队快速查阅 | -| `环境相关.md` + `.env` 模板 | 运行依赖账号、端口、密钥对照表 | 交付 & 运维 | +| `环境配置说明.md` | 系统要求、Conda/依赖、外部服务账号、常用端口 | 首次部署、环境核对 | +| `Usage-Guide.md` | 环境准备、服务启动、配置、日志、验证手册 | 日常运维、调试 | +| `基础配置指南.md` | 租户字段、索引域、排序表达式配置流程 | 新租户开通、配置变更 | +| `测试数据指南.md` | 两个租户的模拟/CSV 数据构造 & MySQL→ES 流程 | 数据准备、联调 | +| `测试Pipeline说明.md` | 测试流水线、CI 脚本、上下文说明 | 自动化测试、追踪流水线 | +| `系统设计文档.md` | 架构、配置系统、索引/查询/排序模块细节 | 研发/扩展功能 | +| `索引字段说明.md` | `search_products` 字段、类型、来源、嵌套结构 | 新增字段、数据对齐 | +| `搜索API对接指南.md` | REST API(文本/图片/管理)详解、示例、响应格式 | API 使用、测试 | +| `搜索API速查表.md` | 常用请求体、过滤器、分面速查表 | 支持团队快速查阅 | +| `Search-API-Examples.md` | Python/JS/cURL 端到端示例 | 客户工程、SDK 参考 | +| `环境配置说明.md` + `.env` 模板 | 运行依赖账号、端口、密钥对照表 | 交付 & 运维 | 更多补充材料: -- `TEST_DATA_GUIDE.md`:包含完整工作流脚本示例 +- `测试数据指南.md`:包含完整工作流脚本示例 - `商品数据源入ES配置规范.md`:数据源映射约定 - `MULTILANG_FEATURE.md`:多语言处理细节 @@ -73,12 +75,12 @@ curl -X POST http://localhost:6002/search/ \ - **数据构建 → MySQL → Elasticsearch** - `scripts/mock_data.sh`:Tenant1 Mock + Tenant2 CSV 一条龙 - `scripts/ingest.sh [recreate]`:驱动 `indexer/` 模块写入 `search_products` - - 详解:`TEST_DATA_GUIDE.md` + - 详解:`测试数据指南.md` - **搜索服务 & API** - `api/`(FastAPI)承载 REST API,`search/` + `query/` 负责查询解析与下发 - - API、分页、过滤、Facet、KNN 等:`API_DOCUMENTATION.md` - - 对接案例与错误码:`API_INTEGRATION_GUIDE.md` + - API、分页、过滤、Facet、KNN 等:`搜索API对接指南.md` + - 对接案例、示例与错误码:`搜索API对接指南.md`、`Search-API-Examples.md` - **配置驱动能力** - `config/schema/{tenant_id}/config.yaml`:字段定义、索引域、排序表达式、SPU 聚合 @@ -96,16 +98,3 @@ scripts/ 数据/服务脚本(mock_data, ingest, run 等) frontend/ 简易调试页面 docs/ 运营及中文资料 ``` - -## 常用参考 - -- **运行/排障**:`USAGE_GUIDE.md`、`环境相关.md` -- **功能设计**:`设计文档.md` -- **字段/数据对齐**:`INDEX_FIELDS_DOCUMENTATION.md` -- **API 对接**:`API_DOCUMENTATION.md`、`API_INTEGRATION_GUIDE.md` -- **测试数据**:`TEST_DATA_GUIDE.md` - -## 许可证 - -专有软件 - 保留所有权利 - diff --git a/TEST_DATA_GUIDE.md b/TEST_DATA_GUIDE.md deleted file mode 100644 index 2da4375..0000000 --- a/TEST_DATA_GUIDE.md +++ /dev/null @@ -1,527 +0,0 @@ -# 测试数据构造指南 - SearchEngine - -本文档说明如何构造测试数据,包括两种数据源的准备和导入流程。 - ---- - -## 快速开始 - -### 1. 构造 Mock 数据(tenant_id=1 和 tenant_id=2) - -```bash -./scripts/mock_data.sh -``` - -功能:自动生成 tenant_id=1 的Mock数据,并从CSV导入 tenant_id=2 的数据到MySQL - ---- - -### 2. 从 MySQL → Elasticsearch - -```bash -# 导入 tenant_id=1 的数据(重建索引) -./scripts/ingest.sh 1 true - -# 导入 tenant_id=2 的数据(重建索引) -./scripts/ingest.sh 2 true -``` - - -**用法**:`./scripts/ingest.sh [recreate_index]` -- `tenant_id`: 租户ID(1 或 2) -- `recreate_index`: 是否重建索引(`true`/`false`,默认:`false`) - ---- - -## 完整工作流程 - -```bash -# 1. 构造并导入测试数据到MySQL -./scripts/mock_data.sh - -# 2. 导入 tenant_id=1 的数据到ES -./scripts/ingest.sh 1 true - -# 3. 导入 tenant_id=2 的数据到ES -./scripts/ingest.sh 2 true -``` - ---- - -## 目录 - -1. [数据说明](#数据说明) -2. [构造Mock数据(tenant_id=1)](#构造mock数据tenant_id1) -3. [从CSV导入数据(tenant_id=2)](#从csv导入数据tenant_id2) -4. [从MySQL导入到Elasticsearch](#从mysql导入到elasticsearch) -5. [完整工作流程](#完整工作流程) -6. [常见问题](#常见问题) - ---- - -## 数据说明 - -系统支持两种测试数据源: - -1. **Tenant ID = 1**: 自动生成的Mock数据(使用 `generate_test_data.py` 生成) -2. **Tenant ID = 2**: 从CSV文件导入的真实数据(使用 `import_tenant2_csv.py` 导入) - -### 数据表结构 - -系统使用店匠标准表结构: - -- **SPU表**: `shoplazza_product_spu` - 商品SPU数据 -- **SKU表**: `shoplazza_product_sku` - 商品SKU数据 - -表结构详见 `INDEX_FIELDS_DOCUMENTATION.md`。 - ---- - -## 构造Mock数据(tenant_id=1) - -### 使用一键脚本(推荐) - -`mock_data.sh` 脚本会自动生成并导入 tenant_id=1 的Mock数据: - -```bash -cd /home/tw/SearchEngine -./scripts/mock_data.sh -``` - -脚本会自动: -- 生成 1000 个SPU的Mock数据 -- 导入数据到MySQL -- 自动计算起始ID,避免主键冲突 - -### 手动分步执行 - -如果需要自定义参数,可以分步执行: - -#### 步骤1: 生成Mock测试数据 - -```bash -python scripts/generate_test_data.py \ - --num-spus 1000 \ - --tenant-id "1" \ - --output test_data_tenant1.sql \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> -``` - -参数说明: -- `--num-spus`: 生成的SPU数量(默认:1000) -- `--tenant-id`: 租户ID(默认:1) -- `--output`: 输出的SQL文件路径 -- `--db-host`, `--db-port`, `--db-database`, `--db-username`, `--db-password`: 数据库连接信息 - -#### 步骤2: 导入数据到MySQL - -```bash -python scripts/import_test_data.py \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> \ - --sql-file test_data_tenant1.sql \ - --tenant-id "1" -``` - -参数说明: -- `--sql-file`: SQL文件路径 -- `--tenant-id`: 租户ID(用于清理旧数据) -- 其他参数:数据库连接信息 - -**注意**: 导入会先清理该 tenant_id 的旧数据,再导入新数据。 - ---- - -## 从CSV导入数据(tenant_id=2) - -### 使用一键脚本(推荐) - -`mock_data.sh` 脚本会自动从CSV文件导入 tenant_id=2 的数据: - -```bash -cd /home/tw/SearchEngine -./scripts/mock_data.sh -``` - -**前提条件**: 确保CSV文件存在于以下路径: -``` -data/customer1/goods_with_pic.5years_congku.csv.shuf.1w -``` - -如果CSV文件路径不同,需要修改 `scripts/mock_data.sh` 中的 `TENANT2_CSV_FILE` 变量。 - -### CSV文件格式要求 - -CSV文件需要包含以下列(列名不区分大小写): - -- `skuId` - SKU ID -- `name` - 商品名称 -- `name_pinyin` - 拼音(可选) -- `create_time` - 创建时间(格式:YYYY-MM-DD HH:MM:SS) -- `ruSkuName` - 俄文SKU名称(可选) -- `enSpuName` - 英文SPU名称(可选) -- `categoryName` - 类别名称 -- `supplierName` - 供应商名称 -- `brandName` - 品牌名称 -- `file_id` - 文件ID(可选) -- `days_since_last_update` - 更新天数(可选) -- `id` - 商品ID(可选) -- `imageUrl` - 图片URL(可选) - -### 手动分步执行 - -如果需要自定义参数,可以分步执行: - -#### 步骤1: 从CSV生成SQL文件 - -```bash -python scripts/import_tenant2_csv.py \ - --csv-file data/customer1/goods_with_pic.5years_congku.csv.shuf.1w \ - --tenant-id "2" \ - --output customer1_data.sql \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> -``` - -参数说明: -- `--csv-file`: CSV文件路径 -- `--tenant-id`: 租户ID(默认:2) -- `--output`: 输出的SQL文件路径 -- 其他参数:数据库连接信息 - -#### 步骤2: 导入数据到MySQL - -```bash -python scripts/import_test_data.py \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> \ - --sql-file customer1_data.sql \ - --tenant-id "2" -``` - -**注意**: -- CSV导入会先清理该 tenant_id 的旧数据,再导入新数据 -- 脚本会自动计算起始ID,避免主键冲突 - ---- - -## 从MySQL导入到Elasticsearch - -数据导入到MySQL后,需要使用 `ingest.sh` 脚本将数据从MySQL导入到Elasticsearch。 - -### 基本用法 - -```bash -./scripts/ingest.sh [recreate_index] -``` - -参数说明: -- `tenant_id`: **必需**,租户ID,用于筛选数据库中的数据 -- `recreate_index`: 可选,是否删除并重建索引(true/false,默认:false) - -### 使用示例 - -#### 重建索引并导入数据(推荐首次导入) - -```bash -# 导入tenant_id=1的数据并重建索引 -./scripts/ingest.sh 1 true - -# 导入tenant_id=2的数据并重建索引 -./scripts/ingest.sh 2 true -``` - -#### 增量导入(不重建索引) - -```bash -# 增量导入tenant_id=1的数据 -./scripts/ingest.sh 1 false - -# 增量导入tenant_id=2的数据 -./scripts/ingest.sh 2 false -``` - -### 手动执行 - -如果需要自定义参数,可以手动执行: - -```bash -python scripts/ingest_shoplazza.py \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> \ - --tenant-id 1 \ - --es-host http://localhost:9200 \ - --recreate \ - --batch-size 500 -``` - -参数说明: -- `--db-host`, `--db-port`, `--db-database`, `--db-username`, `--db-password`: MySQL连接信息 -- `--tenant-id`: 租户ID(必需) -- `--es-host`: Elasticsearch地址 -- `--recreate`: 是否重建索引 -- `--batch-size`: 批量处理大小(默认:500) - -### 检查可用的 tenant_id - -如果导入时显示 "No documents to index",脚本会自动显示调试信息,包括: -- 该 tenant_id 的统计信息(总数、活跃数、已删除数) -- 数据库中存在的其他 tenant_id 列表 - -也可以直接查询数据库: - -```sql --- 查看有哪些 tenant_id -SELECT tenant_id, COUNT(*) as count, - SUM(CASE WHEN deleted = 0 THEN 1 ELSE 0 END) as active -FROM shoplazza_product_spu -GROUP BY tenant_id; - --- 检查特定 tenant_id 的数据 -SELECT COUNT(*) FROM shoplazza_product_spu -WHERE tenant_id = 2 AND deleted = 0; -``` - -**注意**: -- 只有 `deleted=0` 的记录会被导入 -- 首次运行会下载模型文件(BGE-M3和CN-CLIP),大约需要10-30分钟 -- 确保MySQL中存在对应 tenant_id 的数据 - ---- - -## 完整工作流程 - -### 完整示例:构造并导入所有测试数据 - -```bash -# 1. 构造并导入 tenant_id=1 的Mock数据到MySQL -./scripts/mock_data.sh - -# 脚本会自动完成: -# - 生成 tenant_id=1 的Mock数据(1000个SPU) -# - 从CSV导入 tenant_id=2 的数据 -# - 导入数据到MySQL - -# 2. 从MySQL导入 tenant_id=1 的数据到ES -./scripts/ingest.sh 1 true - -# 3. 从MySQL导入 tenant_id=2 的数据到ES -./scripts/ingest.sh 2 true - -# 4. 验证数据导入 -curl http://localhost:9200/search_products/_count -``` - -### 分步执行示例 - -如果需要更细粒度的控制,可以分步执行: - -```bash -# ===== Part 1: 构造 tenant_id=1 的Mock数据 ===== - -# 1.1 生成Mock数据 -python scripts/generate_test_data.py \ - --num-spus 1000 \ - --tenant-id "1" \ - --output test_data_tenant1.sql \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> - -# 1.2 导入到MySQL -python scripts/import_test_data.py \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> \ - --sql-file test_data_tenant1.sql \ - --tenant-id "1" - -# ===== Part 2: 从CSV导入 tenant_id=2 的数据 ===== - -# 2.1 从CSV生成SQL -python scripts/import_tenant2_csv.py \ - --csv-file data/customer1/goods_with_pic.5years_congku.csv.shuf.1w \ - --tenant-id "2" \ - --output customer1_data.sql \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> - -# 2.2 导入到MySQL -python scripts/import_test_data.py \ - --db-host 120.79.247.228 \ - --db-port 3316 \ - --db-database saas \ - --db-username saas \ - --db-password <密码> \ - --sql-file customer1_data.sql \ - --tenant-id "2" - -# ===== Part 3: 从MySQL导入到ES ===== - -# 3.1 导入 tenant_id=1 的数据到ES -./scripts/ingest.sh 1 true - -# 3.2 导入 tenant_id=2 的数据到ES -./scripts/ingest.sh 2 true - -# ===== Part 4: 验证 ===== - -# 4.1 检查ES中的数据量 -curl http://localhost:9200/search_products/_count - -# 4.2 测试搜索 -curl -X POST http://localhost:6002/search/ \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 1" \ - -d '{"query": "玩具", "size": 10}' -``` - ---- - -## 常见问题 - -### Q1: 数据导入失败 - -**症状**: `Error during data ingestion` - -**解决方案**: -```bash -# 检查MySQL数据是否存在 -mysql -h 120.79.247.228 -P 3316 -u saas -p saas -e \ - "SELECT COUNT(*) FROM shoplazza_product_spu WHERE tenant_id=1" - -# 检查ES索引是否存在 -curl http://localhost:9200/search_products - -# 查看详细错误日志 -python scripts/ingest_shoplazza.py --tenant-id 1 --recreate -``` - -### Q2: CSV文件找不到 - -**症状**: `ERROR: CSV file not found` - -**解决方案**: -```bash -# 检查CSV文件是否存在 -ls -lh data/customer1/goods_with_pic.5years_congku.csv.shuf.1w - -# 如果路径不同,修改 scripts/mock_data.sh 中的 TENANT2_CSV_FILE 变量 -``` - -### Q3: 导入时没有数据 - -**症状**: `WARNING: No documents to index` 或 `Transformed 0 SPU documents` - -**可能原因**: -1. 数据库中不存在该 tenant_id 的数据 -2. 数据都被标记为 `deleted=1` -3. tenant_id 类型不匹配 - -**解决步骤**: - -1. **查看调试信息**: 脚本会自动显示调试信息,包括: - ``` - DEBUG: tenant_id=1000: total=0, active=0, deleted=0 - DEBUG: Available tenant_ids in shoplazza_product_spu: - tenant_id=1: total=100, active=100 - tenant_id=2: total=50, active=50 - ``` - -2. **检查数据库**: 直接查询MySQL确认数据 - ```sql - -- 查看有哪些 tenant_id - SELECT tenant_id, COUNT(*) as count, - SUM(CASE WHEN deleted = 0 THEN 1 ELSE 0 END) as active - FROM shoplazza_product_spu - GROUP BY tenant_id; - - -- 检查特定 tenant_id 的数据 - SELECT COUNT(*) FROM shoplazza_product_spu - WHERE tenant_id = 2 AND deleted = 0; - ``` - -3. **如果数据库中没有数据,需要先导入数据**: - - 如果有CSV文件,使用CSV导入脚本 - - 如果没有CSV文件,可以使用mock数据生成脚本 - -4. **使用正确的 tenant_id**: 根据调试信息显示的可用 tenant_id,使用正确的值重新导入 - ```bash - ./scripts/ingest.sh 2 true # 使用调试信息中显示的 tenant_id - ``` - -### Q4: 模型下载慢或失败 - -**症状**: 首次运行时模型下载很慢或超时 - -**解决方案**: -```bash -# 跳过embedding快速测试(不推荐,但可以快速验证流程) -# 注意:这会导致搜索功能不完整 - -# 或手动下载模型到指定目录 -# TEXT_MODEL_DIR=/data/tw/models/bge-m3 -# IMAGE_MODEL_DIR=/data/tw/models/cn-clip -``` - -### Q5: 内存不足 - -**症状**: `Out of memory` - -**解决方案**: -```bash -# 减少批量大小 -python scripts/ingest_shoplazza.py \ - --tenant-id 1 \ - --batch-size 200 # 默认500,可以减少到100-200 -``` - -### Q6: 主键冲突 - -**症状**: `Duplicate entry` 错误 - -**解决方案**: -- Mock数据脚本会自动计算起始ID,避免冲突 -- 如果仍有冲突,可以手动清理旧数据: - ```sql - DELETE FROM shoplazza_product_spu WHERE tenant_id = 1; - DELETE FROM shoplazza_product_sku WHERE tenant_id = 1; - ``` - ---- - -## 相关文档 - -- **使用文档**: `USAGE_GUIDE.md` - 环境、启动、配置、日志查看 -- **字段说明文档**: `INDEX_FIELDS_DOCUMENTATION.md` - 索引字段详细说明 -- **API接口文档**: `API_INTEGRATION_GUIDE.md` - 完整的API对接指南 -- **README**: `README.md` - 项目概述和快速开始 - ---- - -**文档版本**: v2.0 -**最后更新**: 2024-12 - diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md deleted file mode 100644 index 8305050..0000000 --- a/USAGE_GUIDE.md +++ /dev/null @@ -1,441 +0,0 @@ -# 使用指南 - SearchEngine - -本文档提供完整的使用指南,包括环境准备、服务启动、配置说明、日志查看等。 - -## 目录 - -1. [环境准备](#环境准备) -2. [服务启动](#服务启动) -3. [配置说明](#配置说明) -4. [查看日志](#查看日志) -5. [测试验证](#测试验证) -6. [常见问题](#常见问题) - ---- - -## 环境准备 - -### 系统要求 - -- **操作系统**: Linux (推荐 CentOS 7+ / Ubuntu 18.04+) -- **Python**: 3.8+ -- **内存**: 建议 8GB+ -- **磁盘**: 10GB+ (包含模型文件) -- **Elasticsearch**: 8.x (可通过Docker运行) - -### 安装依赖 - -#### 1. 安装Python依赖 - -```bash -cd /home/tw/SearchEngine -pip install -r requirements.txt -``` - -#### 2. 启动Elasticsearch - -**方式1: 使用Docker(推荐)** - -```bash -docker run -d \ - --name elasticsearch \ - -p 9200:9200 \ - -e "discovery.type=single-node" \ - -e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \ - elasticsearch:8.11.0 -``` - -**方式2: 本地安装** - -参考 [Elasticsearch官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/8.11/install-elasticsearch.html) - -#### 3. 配置环境变量 - -创建 `.env` 文件: - -```bash -# MySQL配置 -DB_HOST=120.79.247.228 -DB_PORT=3316 -DB_DATABASE=saas -DB_USERNAME=saas -DB_PASSWORD=your_password - -# Elasticsearch配置 -ES_HOST=http://localhost:9200 -ES_USERNAME=essa -ES_PASSWORD=4hOaLaf41y2VuI8y - -# Redis配置(可选,用于缓存) -REDIS_HOST=localhost -REDIS_PORT=6479 -REDIS_PASSWORD=BMfv5aI31kgHWtlx - -# DeepL翻译API(可选) -DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed - -# API服务配置 -API_HOST=0.0.0.0 -API_PORT=6002 -``` - ---- - -## 服务启动 - -### 方式1: 一键启动(推荐) - -```bash -cd /home/tw/SearchEngine -./run.sh -``` - -这个脚本会自动: -1. 创建日志目录 -2. 启动后端API服务(后台运行) -3. 启动前端Web界面(后台运行) -4. 等待服务就绪 - -启动完成后,访问: -- **前端界面**: http://localhost:6003 -- **后端API**: http://localhost:6002 -- **API文档**: http://localhost:6002/docs - -### 方式2: 分步启动 - -#### 启动后端服务 - -```bash -./scripts/start_backend.sh -``` - -后端API会在 http://localhost:6002 启动 - -#### 启动前端服务 - -```bash -./scripts/start_frontend.sh -``` - -前端界面会在 http://localhost:6003 启动 - -### 方式3: 手动启动 - -#### 启动后端API服务 - -```bash -python -m api.app \ - --host 0.0.0.0 \ - --port 6002 \ - --es-host http://localhost:9200 \ - --reload -``` - -#### 启动前端服务(可选) - -```bash -# 使用Python简单HTTP服务器 -cd frontend -python -m http.server 6003 -``` - -### 停止服务 - -```bash -# 停止后端 -kill $(cat logs/backend.pid) - -# 停止前端 -kill $(cat logs/frontend.pid) - -# 或使用停止脚本 -./scripts/stop.sh -``` - -### 服务端口 - -| 服务 | 端口 | URL | -|------|------|-----| -| Elasticsearch | 9200 | http://localhost:9200 | -| Backend API | 6002 | http://localhost:6002 | -| Frontend Web | 6003 | http://localhost:6003 | -| API Docs | 6002 | http://localhost:6002/docs | - ---- - -## 配置说明 - -### 环境配置文件 (.env) - -主要配置项说明: - -```bash -# Elasticsearch配置 -ES_HOST=http://localhost:9200 -ES_USERNAME=essa -ES_PASSWORD=4hOaLaf41y2VuI8y - -# MySQL配置 -DB_HOST=120.79.247.228 -DB_PORT=3316 -DB_DATABASE=saas -DB_USERNAME=saas -DB_PASSWORD=your_password - -# Redis配置(可选,用于缓存) -REDIS_HOST=localhost -REDIS_PORT=6479 -REDIS_PASSWORD=BMfv5aI31kgHWtlx - -# DeepL翻译API -DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed - -# API服务配置 -API_HOST=0.0.0.0 -API_PORT=6002 -``` - -### 修改配置 - -1. 编辑 `.env` 文件 -2. 重启相关服务 - ---- - -## 查看日志 - -### 日志文件位置 - -日志文件存储在 `logs/` 目录下: - -- `logs/backend.log` - 后端服务日志 -- `logs/frontend.log` - 前端服务日志 -- `logs/search_engine.log` - 应用主日志(按天轮转) -- `logs/errors.log` - 错误日志(按天轮转) - -### 查看实时日志 - -```bash -# 查看后端日志 -tail -f logs/backend.log - -# 查看前端日志 -tail -f logs/frontend.log - -# 查看应用主日志 -tail -f logs/search_engine.log - -# 查看错误日志 -tail -f logs/errors.log -``` - -### 日志级别 - -日志级别可以通过环境变量 `LOG_LEVEL` 设置: - -```bash -# 在 .env 文件中设置 -LOG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR, CRITICAL -``` - -### 日志轮转 - -日志文件按天自动轮转,保留30天的历史日志。 - ---- - -## 测试验证 - -### 1. 健康检查 - -```bash -curl http://localhost:6002/admin/health -``` - -**预期响应**: -```json -{ - "status": "healthy", - "elasticsearch": "connected" -} -``` - -### 2. 索引统计 - -```bash -curl http://localhost:6002/admin/stats -``` - -### 3. 简单搜索测试 - -```bash -curl -X POST http://localhost:6002/search/ \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 2" \ - -d '{ - "query": "玩具", - "size": 10 - }' -``` - -或者通过查询参数: - -```bash -curl -X POST "http://localhost:6002/search/?tenant_id=2" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具", - "size": 10 - }' -``` - -### 4. 带过滤器的搜索 - -```bash -curl -X POST http://localhost:6002/search/ \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 2" \ - -d '{ - "query": "玩具", - "size": 10, - "filters": { - "categoryName_keyword": ["玩具", "益智玩具"] - }, - "range_filters": { - "price": {"gte": 50, "lte": 200} - } - }' -``` - -### 5. 分面搜索测试 - -```bash -curl -X POST http://localhost:6002/search/ \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 2" \ - -d '{ - "query": "玩具", - "size": 10, - "facets": [ - {"field": "categoryName_keyword", "size": 15}, - {"field": "brandName_keyword", "size": 15} - ] - }' -``` - -### 6. 图片搜索测试 - -```bash -curl -X POST http://localhost:6002/search/image \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 2" \ - -d '{ - "image_url": "https://oss.essa.cn/example.jpg", - "size": 10 - }' -``` - -### 7. 前端界面测试 - -访问 http://localhost:6003 或 http://localhost:6002/ 进行可视化测试。 - -**注意**: 所有搜索接口都需要通过 `X-Tenant-ID` 请求头或 `tenant_id` 查询参数指定租户ID。 - ---- - -## 常见问题 - -### Q1: MySQL连接失败 - -**症状**: `Failed to connect to MySQL` - -**解决方案**: -```bash -# 检查MySQL服务状态 -mysql -h 120.79.247.228 -P 3316 -u saas -p -e "SELECT 1" - -# 检查配置 -cat .env | grep DB_ -``` - -### Q2: Elasticsearch连接失败 - -**症状**: `Failed to connect to Elasticsearch` - -**解决方案**: -```bash -# 检查ES服务状态 -curl http://localhost:9200 - -# 检查ES版本 -curl http://localhost:9200 | grep version - -# 确认配置 -cat .env | grep ES_ -``` - -### Q3: 服务启动失败 - -**症状**: `Address already in use` 或端口被占用 - -**解决方案**: -```bash -# 查看占用端口的进程 -lsof -i :6002 # 后端 -lsof -i :6003 # 前端 -lsof -i :9200 # ES - -# 杀掉进程 -kill -9 - -# 或修改端口配置 -``` - -### Q4: 搜索无结果 - -**症状**: 搜索返回空结果 - -**解决方案**: -```bash -# 检查ES中是否有数据 -curl http://localhost:9200/search_products/_count - -# 检查tenant_id过滤是否正确 -curl -X POST http://localhost:6002/search/ \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: 2" \ - -d '{"query": "*", "size": 10, "debug": true}' -``` - -### Q5: 前端无法连接后端 - -**症状**: CORS错误 - -**解决方案**: -- 确保后端在 http://localhost:6002 运行 -- 检查浏览器控制台错误信息 -- 检查后端日志中的CORS配置 - -### Q6: 翻译不工作 - -**症状**: 翻译返回原文 - -**解决方案**: -- 检查DEEPL_AUTH_KEY是否正确 -- 如果没有API key,系统会使用mock模式(返回原文) - ---- - -## 相关文档 - -- **测试数据构造文档**: `TEST_DATA_GUIDE.md` - 如何构造和导入测试数据 -- **API接口文档**: `API_INTEGRATION_GUIDE.md` - 完整的API对接指南 -- **字段说明文档**: `INDEX_FIELDS_DOCUMENTATION.md` - 索引字段详细说明 -- **设计文档**: `设计文档.md` - 系统架构和设计说明 -- **README**: `README.md` - 项目概述和快速开始 - ---- - -**文档版本**: v2.0 -**最后更新**: 2024-12 - diff --git a/docs/BASE_CONFIG_GUIDE.md b/docs/BASE_CONFIG_GUIDE.md deleted file mode 100644 index 013ec98..0000000 --- a/docs/BASE_CONFIG_GUIDE.md +++ /dev/null @@ -1,257 +0,0 @@ -# Base Configuration Guide - -店匠通用配置(Base Configuration)使用指南 - -## 概述 - -Base配置是店匠(Shoplazza)通用配置,适用于所有使用店匠标准表的客户。该配置采用SPU级别的索引结构,所有客户共享同一个Elasticsearch索引(`search_products`),通过`tenant_id`字段实现数据隔离。 - -## 核心特性 - -- **SPU级别索引**:每个ES文档代表一个SPU,包含嵌套的variants数组 -- **统一索引**:所有客户共享`search_products`索引 -- **租户隔离**:通过`tenant_id`字段实现数据隔离 -- **配置简化**:配置只包含ES搜索相关配置,不包含MySQL数据源配置 -- **外部友好格式**:API返回格式不包含ES内部字段(`_id`, `_score`, `_source`) - -## 配置说明 - -### 配置文件位置 - -`config/schema/base/config.yaml` - -### 配置内容 - -Base配置**不包含**以下内容: -- `mysql_config` - MySQL数据库配置 -- `main_table` - 主表配置 -- `extension_table` - 扩展表配置 -- `source_table` / `source_column` - 字段数据源映射 - -Base配置**只包含**: -- ES字段定义(字段类型、分析器、boost等) -- 查询域(indexes)配置 -- 查询处理配置(query_config) -- 排序和打分配置(function_score) -- SPU配置(spu_config) - -### 必需字段 - -- `tenant_id` (KEYWORD, required) - 租户隔离字段 - -### 主要字段 - -- `product_id` - 商品ID -- `title`, `brief`, `description` - 文本搜索字段 -- `seo_title`, `seo_description`, `seo_keywords` - SEO字段 -- `vendor`, `product_type`, `tags`, `category` - 分类和标签字段 -- `min_price`, `max_price`, `compare_at_price` - 价格字段 -- `variants` (nested) - 嵌套变体数组 - -## 数据导入流程 - -### 1. 生成测试数据 - -```bash -python scripts/generate_test_data.py \ - --num-spus 100 \ - --tenant-id "1" \ - --start-spu-id 1 \ - --start-sku-id 1 \ - --output test_data.sql -``` - -### 2. 导入测试数据到MySQL - -```bash -python scripts/import_test_data.py \ - --db-host localhost \ - --db-port 3306 \ - --db-database saas \ - --db-username root \ - --db-password password \ - --sql-file test_data.sql \ - --tenant-id "1" -``` - -### 3. 导入数据到Elasticsearch - -```bash -python scripts/ingest_shoplazza.py \ - --db-host localhost \ - --db-port 3306 \ - --db-database saas \ - --db-username root \ - --db-password password \ - --tenant-id "1" \ - --config base \ - --es-host http://localhost:9200 \ - --recreate \ - --batch-size 500 -``` - -## API使用 - -### 搜索接口 - -**端点**: `POST /search/` - -**请求头**: -``` -X-Tenant-ID: 1 -Content-Type: application/json -``` - -**请求体**: -```json -{ - "query": "耳机", - "size": 10, - "from": 0, - "filters": { - "category_keyword": "电子产品" - }, - "facets": ["category_keyword", "vendor_keyword"] -} -``` - -**响应格式**: -```json -{ - "results": [ - { - "product_id": "1", - "title": "蓝牙耳机 Sony", - "handle": "product-1", - "description": "高品质无线蓝牙耳机", - "vendor": "Sony", - "product_type": "电子产品", - "price": 199.99, - "compare_at_price": 299.99, - "currency": "USD", - "image_url": "//cdn.example.com/products/1.jpg", - "in_stock": true, - "variants": [ - { - "variant_id": "1", - "title": "黑色", - "price": 199.99, - "compare_at_price": 299.99, - "sku": "SKU-1-1", - "stock": 50, - "options": { - "option1": "黑色" - } - } - ], - "relevance_score": 0.95 - } - ], - "total": 10, - "max_score": 1.0, - "facets": [ - { - "field": "category_keyword", - "label": "category_keyword", - "type": "terms", - "values": [ - { - "value": "电子产品", - "label": "电子产品", - "count": 5, - "selected": false - } - ] - } - ], - "suggestions": [], - "related_searches": [], - "took_ms": 15, - "query_info": {} -} -``` - -### 响应格式说明 - -#### 主要变化 - -1. **`results`替代`hits`**:返回字段从`hits`改为`results` -2. **结构化结果**:每个结果包含`product_id`, `title`, `variants`, `relevance_score`等字段 -3. **无ES内部字段**:不包含`_id`, `_score`, `_source`等ES内部字段 -4. **嵌套variants**:每个商品包含variants数组,每个variant包含完整的变体信息 -5. **相关性分数**:`relevance_score`是ES原始分数(不进行归一化) - -#### ProductResult字段 - -- `product_id` - 商品ID -- `title` - 商品标题 -- `handle` - 商品handle -- `description` - 商品描述 -- `vendor` - 供应商/品牌 -- `product_type` - 商品类型 -- `tags` - 标签 -- `price` - 最低价格(min_price) -- `compare_at_price` - 原价 -- `currency` - 货币单位(默认USD) -- `image_url` - 主图URL -- `in_stock` - 是否有库存 -- `variants` - 变体列表 -- `relevance_score` - 相关性分数(ES原始分数) - -#### VariantResult字段 - -- `variant_id` - 变体ID -- `title` - 变体标题 -- `price` - 价格 -- `compare_at_price` - 原价 -- `sku` - SKU编码 -- `stock` - 库存数量 -- `options` - 选项(颜色、尺寸等) - -## 测试 - -### 运行测试脚本 - -```bash -python scripts/test_base.py \ - --api-url http://localhost:8000 \ - --tenant-id "1" \ - --test-tenant-2 "2" -``` - -### 测试内容 - -1. **基本搜索**:测试搜索API基本功能 -2. **响应格式验证**:验证返回格式是否符合要求 -3. **Facets聚合**:测试分面搜索功能 -4. **租户隔离**:验证不同租户的数据隔离 - -## 常见问题 - -### Q: 为什么配置中没有MySQL相关配置? - -A: 数据源配置和数据导入流程是写死的脚本,不在搜索配置中。搜索配置只关注ES搜索相关的内容。 - -### Q: 如何为新的租户导入数据? - -A: 使用`ingest_shoplazza.py`脚本,指定不同的`--tenant-id`参数即可。 - -### Q: 如何验证租户隔离是否生效? - -A: 使用`test_base.py`脚本,指定两个不同的`--tenant-id`,检查搜索结果是否隔离。 - -### Q: API返回格式中为什么没有`_id`和`_score`? - -A: 为了提供外部友好的API格式,我们移除了ES内部字段,使用`product_id`和`relevance_score`替代。 - -### Q: 如何添加新的搜索字段? - -A: 在`config/schema/base/config.yaml`中添加字段定义,然后重新生成索引映射并重新导入数据。 - -## 注意事项 - -1. **tenant_id必需**:所有API请求必须提供`tenant_id`(通过请求头`X-Tenant-ID`或查询参数`tenant_id`) -2. **索引共享**:所有客户共享`search_products`索引,确保`tenant_id`字段正确设置 -3. **数据导入**:数据导入脚本是写死的,不依赖配置中的MySQL设置 -4. **配置分离**:搜索配置和数据源配置完全分离,提高可维护性 - diff --git a/docs/RequestContext_README.md b/docs/RequestContext_README.md deleted file mode 100644 index 3af13ec..0000000 --- a/docs/RequestContext_README.md +++ /dev/null @@ -1,374 +0,0 @@ -# RequestContext 使用指南 - -## 概述 - -`RequestContext` 是一个请求粒度的上下文管理器,用于跟踪和管理搜索请求的整个生命周期。它提供了统一的数据存储、性能监控和日志记录功能。 - -## 核心功能 - -### 1. 查询分析结果存储 -- 原始查询、规范化查询、重写查询 -- 检测语言和翻译结果 -- 查询向量(embedding) -- 布尔查询AST - -### 2. 各检索阶段中间结果 -- 解析后的查询对象 -- 布尔查询语法树 -- ES查询DSL -- ES响应数据 -- 处理后的搜索结果 - -### 3. 性能监控 -- 自动计时各阶段耗时 -- 计算各阶段耗时占比 -- 识别性能瓶颈 -- 详细的性能摘要日志 - -### 4. 错误处理和警告 -- 统一的错误信息存储 -- 警告信息收集 -- 完整的上下文错误跟踪 - -## 支持的搜索阶段 - -```python -class RequestContextStage(Enum): - TOTAL = "total_search" # 总搜索时间 - QUERY_PARSING = "query_parsing" # 查询解析 - BOOLEAN_PARSING = "boolean_parsing" # 布尔查询解析 - QUERY_BUILDING = "query_building" # ES查询构建 - ELASTICSEARCH_SEARCH = "elasticsearch_search" # ES搜索 - RESULT_PROCESSING = "result_processing" # 结果处理 - RERANKING = "reranking" # 重排序 -``` - -## 基本使用方法 - -### 1. 创建RequestContext - -```python -from context import create_request_context, RequestContext - -# 方式1: 使用工厂函数 -context = create_request_context(reqid="req-001", uid="user-123") - -# 方式2: 直接创建 -context = RequestContext(reqid="req-001", uid="user-123") - -# 方式3: 作为上下文管理器使用 -with create_request_context("req-002", "user-456") as context: - # 搜索逻辑 - pass # 自动记录性能摘要 -``` - -### 2. 阶段计时 - -```python -from context import RequestContextStage - -# 开始计时 -context.start_stage(RequestContextStage.QUERY_PARSING) - -# 执行查询解析逻辑 -# parsed_query = query_parser.parse(query, context=context) - -# 结束计时 -duration = context.end_stage(RequestContextStage.QUERY_PARSING) -print(f"查询解析耗时: {duration:.2f}ms") -``` - -### 3. 存储查询分析结果 - -```python -context.store_query_analysis( - original_query="红色连衣裙", - normalized_query="红色 连衣裙", - rewritten_query="红色 女 连衣裙", - detected_language="zh", - translations={"en": "red dress"}, - query_vector=[0.1, 0.2, 0.3, ...], # 如果有向量 - is_simple_query=True -) -``` - -### 4. 存储中间结果 - -```python -# 存储解析后的查询对象 -context.store_intermediate_result('parsed_query', parsed_query) - -# 存储ES查询DSL -context.store_intermediate_result('es_query', es_query_dict) - -# 存储ES响应 -context.store_intermediate_result('es_response', es_response) - -# 存储处理后的结果 -context.store_intermediate_result('processed_hits', hits) -``` - -### 5. 错误处理和警告 - -```python -try: - # 可能出错的操作 - risky_operation() -except Exception as e: - context.set_error(e) - -# 添加警告信息 -context.add_warning("查询结果较少,建议放宽搜索条件") - -# 检查是否有错误 -if context.has_error(): - print(f"搜索出错: {context.metadata['error_info']}") -``` - -## 在Searcher中使用 - -### 1. 自动创建Context(向后兼容) - -```python -searcher = Searcher(config, es_client) - -# Searcher会自动创建RequestContext -result = searcher.search( - query="无线蓝牙耳机", - size=10, - enable_embedding=True -) - -# 结果中包含context信息 -print(result.context.get_summary()) -``` - -### 2. 手动创建和传递Context - -```python -# 创建自己的context -context = create_request_context("my-req-001", "user-789") - -# 传递给searcher -result = searcher.search( - query="运动鞋", - context=context # 传递自定义context -) - -# 使用context进行详细分析 -summary = context.get_summary() -print(f"总耗时: {summary['performance']['total_duration_ms']:.1f}ms") -``` - -## 性能分析 - -### 1. 获取性能摘要 - -```python -summary = context.get_summary() - -# 基本信息 -print(f"请求ID: {summary['request_info']['reqid']}") -print(f"总耗时: {summary['performance']['total_duration_ms']:.1f}ms") - -# 各阶段耗时 -for stage, duration in summary['performance']['stage_timings_ms'].items(): - percentage = summary['performance']['stage_percentages'].get(stage, 0) - print(f"{stage}: {duration:.1f}ms ({percentage:.1f}%)") - -# 查询分析信息 -query_info = summary['query_analysis'] -print(f"原查询: {query_info['original_query']}") -print(f"重写查询: {query_info['rewritten_query']}") -print(f"检测语言: {query_info['detected_language']}") -``` - -### 2. 识别性能瓶颈 - -```python -summary = context.get_summary() - -# 找出耗时超过20%的阶段 -bottlenecks = [] -for stage, percentage in summary['performance']['stage_percentages'].items(): - if percentage > 20: - bottlenecks.append((stage, percentage)) - -if bottlenecks: - print("性能瓶颈:") - for stage, percentage in bottlenecks: - print(f" - {stage}: {percentage:.1f}%") -``` - -### 3. 自动性能日志 - -RequestContext会在以下时机自动记录详细的性能摘要日志: - -- 上下文管理器退出时 (`with context:`) -- 手动调用 `context.log_performance_summary()` -- Searcher.search() 完成时 - -日志格式示例: -``` -[2024-01-01 10:30:45] [INFO] [request_context] 搜索请求性能摘要 | reqid: req-001 | 总耗时: 272.6ms | 阶段耗时: | - query_parsing: 35.3ms (13.0%) | - elasticsearch_search: 146.0ms (53.6%) | - result_processing: 18.6ms (6.8%) | 查询: '红色连衣裙' -> '红色 女 连衣裙' (zh) | 结果: 156 hits ES查询: 2456 chars -``` - -## 线程安全 - -RequestContext是线程安全的,支持并发请求处理。每个请求使用独立的context实例,互不干扰。 - -```python -import threading -from context import create_request_context - -def worker(request_id, query): - context = create_request_context(request_id) - # 搜索逻辑 - # context自动跟踪此线程的请求 - pass - -# 多线程并发处理 -threads = [] -for i in range(5): - t = threading.Thread(target=worker, args=(f"req-{i}", f"query-{i}")) - threads.append(t) - t.start() - -for t in threads: - t.join() -``` - -## 调试支持 - -### 1. 检查中间结果 - -```python -# 获取查询解析结果 -parsed_query = context.get_intermediate_result('parsed_query') - -# 获取ES查询DSL -es_query = context.get_intermediate_result('es_query') - -# 获取ES响应 -es_response = context.get_intermediate_result('es_response') - -# 获取原始搜索结果 -raw_hits = context.get_intermediate_result('raw_hits') - -# 获取最终处理后的结果 -processed_hits = context.get_intermediate_result('processed_hits') -``` - -### 2. 错误诊断 - -```python -if context.has_error(): - error_info = context.metadata['error_info'] - print(f"错误类型: {error_info['type']}") - print(f"错误消息: {error_info['message']}") - - # 检查是否有警告 - if context.metadata['warnings']: - print("警告信息:") - for warning in context.metadata['warnings']: - print(f" - {warning}") -``` - -## 最佳实践 - -### 1. 统一使用Context - -```python -# 推荐:在整个搜索流程中传递同一个context -result = searcher.search(query, context=context) - -# 不推荐:在各个环节创建不同的context -``` - -### 2. 合理设置阶段边界 - -```python -# 只在有意义的大阶段之间计时 -context.start_stage(RequestContextStage.QUERY_PARSING) -# 整个查询解析逻辑 -context.end_stage(RequestContextStage.QUERY_PARSING) - -# 避免在细粒度操作间频繁计时 -``` - -### 3. 及时存储关键数据 - -```python -# 在每个阶段完成后及时存储结果 -context.store_intermediate_result('parsed_query', parsed_query) -context.store_intermediate_result('es_query', es_query) - -# 便于后续调试和分析 -``` - -### 4. 适当使用警告 - -```python -# 使用警告记录非致命问题 -if total_hits < 10: - context.add_warning("搜索结果较少,建议放宽搜索条件") - -if query_time > 5.0: - context.add_warning(f"查询耗时较长: {query_time:.1f}秒") -``` - -## 集成示例 - -### API接口集成 - -```python -from flask import Flask, request, jsonify -from context import create_request_context - -app = Flask(__name__) - -@app.route('/search') -def api_search(): - # 从请求中获取参数 - query = request.args.get('q', '') - uid = request.args.get('uid', 'anonymous') - - # 创建context - context = create_request_context(uid=uid) - - try: - # 执行搜索 - result = searcher.search(query, context=context) - - # 返回结果(包含性能信息) - response = { - 'results': result.to_dict(), - 'performance': context.get_summary()['performance'] - } - - return jsonify(response) - - except Exception as e: - context.set_error(e) - context.log_performance_summary() - - return jsonify({ - 'error': str(e), - 'request_id': context.reqid - }), 500 -``` - -## 总结 - -RequestContext提供了一个强大而灵活的框架,用于管理搜索请求的整个生命周期。通过统一的上下文管理、自动性能监控和详细的日志记录,它显著提升了搜索系统的可观测性和调试能力。 - -主要优势: - -1. **统一管理**: 所有请求相关数据集中存储 -2. **自动监控**: 无需手动计时,自动跟踪性能 -3. **详细日志**: 完整的请求生命周期记录 -4. **向后兼容**: 现有代码无需修改即可受益 -5. **线程安全**: 支持高并发场景 -6. **易于调试**: 丰富的中间结果和错误信息 - -通过合理使用RequestContext,可以构建更加可靠、高性能和易维护的搜索系统。 \ No newline at end of file diff --git a/docs/Search-API-Examples.md b/docs/Search-API-Examples.md new file mode 100644 index 0000000..c262fa2 --- /dev/null +++ b/docs/Search-API-Examples.md @@ -0,0 +1,1148 @@ +# API 使用示例 + +本文档提供了搜索引擎 API 的详细使用示例,包括各种常见场景和最佳实践。 + +--- + +## 目录 + +1. [基础搜索](#基础搜索) +2. [过滤器使用](#过滤器使用) +3. [分面搜索](#分面搜索) +4. [排序](#排序) +5. [图片搜索](#图片搜索) +6. [布尔表达式](#布尔表达式) +7. [完整示例](#完整示例) + +--- + +## 基础搜索 + +### 示例 1:最简单的搜索 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "芭比娃娃" + }' +``` + +**响应**: +```json +{ + "hits": [...], + "total": 118, + "max_score": 8.5, + "took_ms": 45, + "query_info": { + "original_query": "芭比娃娃", + "detected_language": "zh", + "translations": {"en": "barbie doll"} + } +} +``` + +### 示例 2:指定返回数量 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 50 + }' +``` + +### 示例 3:分页查询 + +```bash +# 第1页(0-19) +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 20, + "from": 0 + }' + +# 第2页(20-39) +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 20, + "from": 20 + }' +``` + +--- + +## 过滤器使用 + +### 精确匹配过滤器 + +#### 示例 1:单值过滤 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "filters": { + "categoryName_keyword": "玩具" + } + }' +``` + +#### 示例 2:多值过滤(OR 逻辑) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "娃娃", + "filters": { + "categoryName_keyword": ["玩具", "益智玩具", "儿童玩具"] + } + }' +``` + +说明:匹配类目为"玩具" **或** "益智玩具" **或** "儿童玩具"的商品。 + +#### 示例 3:多字段过滤(AND 逻辑) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "娃娃", + "filters": { + "categoryName_keyword": "玩具", + "brandName_keyword": "美泰" + } + }' +``` + +说明:必须同时满足"类目=玩具" **并且** "品牌=美泰"。 + +### 范围过滤器 + +#### 示例 1:价格范围 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "range_filters": { + "price": { + "gte": 50, + "lte": 200 + } + } + }' +``` + +说明:价格在 50-200 元之间(包含边界)。 + +#### 示例 2:只设置下限 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "range_filters": { + "price": { + "gte": 100 + } + } + }' +``` + +说明:价格 ≥ 100 元。 + +#### 示例 3:只设置上限 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "range_filters": { + "price": { + "lt": 50 + } + } + }' +``` + +说明:价格 < 50 元(不包含50)。 + +#### 示例 4:多字段范围过滤 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "range_filters": { + "price": { + "gte": 50, + "lte": 200 + }, + "days_since_last_update": { + "lte": 30 + } + } + }' +``` + +说明:价格在 50-200 元 **并且** 最近30天内更新过。 + +### 组合过滤器 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "filters": { + "categoryName_keyword": ["玩具", "益智玩具"], + "brandName_keyword": "乐高" + }, + "range_filters": { + "price": { + "gte": 50, + "lte": 500 + } + } + }' +``` + +说明:类目是"玩具"或"益智玩具" **并且** 品牌是"乐高" **并且** 价格在 50-500 元之间。 + +--- + +## 分面搜索 + +### 简单模式 + +#### 示例 1:基础分面 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 20, + "facets": ["categoryName_keyword", "brandName_keyword"] + }' +``` + +**响应**: +```json +{ + "hits": [...], + "total": 118, + "facets": [ + { + "field": "categoryName_keyword", + "label": "categoryName_keyword", + "type": "terms", + "values": [ + {"value": "玩具", "count": 85, "selected": false}, + {"value": "益智玩具", "count": 33, "selected": false} + ] + }, + { + "field": "brandName_keyword", + "label": "brandName_keyword", + "type": "terms", + "values": [ + {"value": "乐高", "count": 42, "selected": false}, + {"value": "美泰", "count": 28, "selected": false} + ] + } + ] +} +``` + +### 高级模式 + +#### 示例 1:自定义分面大小 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "facets": [ + { + "field": "categoryName_keyword", + "size": 20, + "type": "terms" + }, + { + "field": "brandName_keyword", + "size": 30, + "type": "terms" + } + ] + }' +``` + +#### 示例 2:范围分面 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "facets": [ + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + } + ] + }' +``` + +**响应**: +```json +{ + "facets": [ + { + "field": "price", + "label": "price", + "type": "range", + "values": [ + {"value": "0-50", "count": 23, "selected": false}, + {"value": "50-100", "count": 45, "selected": false}, + {"value": "100-200", "count": 38, "selected": false}, + {"value": "200+", "count": 12, "selected": false} + ] + } + ] +} +``` + +#### 示例 3:混合分面(Terms + Range) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "facets": [ + {"field": "categoryName_keyword", "size": 15}, + {"field": "brandName_keyword", "size": 15}, + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "低价", "to": 50}, + {"key": "中价", "from": 50, "to": 200}, + {"key": "高价", "from": 200} + ] + } + ] + }' +``` + +--- + +## 排序 + +### 示例 1:按价格排序(升序) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 20, + "sort_by": "min_price", + "sort_order": "asc" + }' +``` + +### 示例 2:按创建时间排序(降序) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 20, + "sort_by": "create_time", + "sort_order": "desc" + }' +``` + +### 示例 3:排序+过滤 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "filters": { + "categoryName_keyword": "益智玩具" + }, + "sort_by": "min_price", + "sort_order": "asc" + }' +``` + +--- + +## 图片搜索 + +### 示例 1:基础图片搜索 + +```bash +curl -X POST "http://localhost:6002/search/image" \ + -H "Content-Type: application/json" \ + -d '{ + "image_url": "https://example.com/barbie.jpg", + "size": 20 + }' +``` + +### 示例 2:图片搜索+过滤器 + +```bash +curl -X POST "http://localhost:6002/search/image" \ + -H "Content-Type: application/json" \ + -d '{ + "image_url": "https://example.com/barbie.jpg", + "size": 20, + "filters": { + "categoryName_keyword": "玩具" + }, + "range_filters": { + "price": { + "lte": 100 + } + } + }' +``` + +--- + +## 布尔表达式 + +### 示例 1:AND 查询 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具 AND 乐高" + }' +``` + +说明:必须同时包含"玩具"和"乐高"。 + +### 示例 2:OR 查询 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "芭比 OR 娃娃" + }' +``` + +说明:包含"芭比"或"娃娃"即可。 + +### 示例 3:ANDNOT 查询(排除) + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具 ANDNOT 电动" + }' +``` + +说明:包含"玩具"但不包含"电动"。 + +### 示例 4:复杂布尔表达式 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" + }' +``` + +说明:必须包含"玩具",并且包含"乐高"或"芭比",但不包含"电动"。 + +### 示例 5:域查询 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "brand:乐高" + }' +``` + +说明:在品牌域中搜索"乐高"。 + +--- + +## 完整示例 + +### Python 完整示例 + +```python +#!/usr/bin/env python3 +import requests +import json + +API_URL = "http://localhost:6002/search/" + +def search_products( + query, + size=20, + from_=0, + filters=None, + range_filters=None, + facets=None, + sort_by=None, + sort_order="desc", + debug=False +): + """执行搜索查询""" + payload = { + "query": query, + "size": size, + "from": from_ + } + + if filters: + payload["filters"] = filters + if range_filters: + payload["range_filters"] = range_filters + if facets: + payload["facets"] = facets + if sort_by: + payload["sort_by"] = sort_by + payload["sort_order"] = sort_order + if debug: + payload["debug"] = debug + + response = requests.post(API_URL, json=payload) + response.raise_for_status() + return response.json() + + +# 示例 1:简单搜索 +result = search_products("芭比娃娃", size=10) +print(f"找到 {result['total']} 个结果") +for hit in result['hits'][:3]: + product = hit['_source'] + print(f" - {product['name']}: ¥{product.get('price', 'N/A')}") + +# 示例 2:带过滤和分面的搜索 +result = search_products( + query="玩具", + size=20, + filters={ + "categoryName_keyword": ["玩具", "益智玩具"] + }, + range_filters={ + "price": {"gte": 50, "lte": 200} + }, + facets=[ + {"field": "brandName_keyword", "size": 15}, + {"field": "categoryName_keyword", "size": 15}, + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + } + ], + sort_by="min_price", + sort_order="asc" +) + +# 显示分面结果 +print(f"\n分面统计:") +for facet in result.get('facets', []): + print(f"\n{facet['label']} ({facet['type']}):") + for value in facet['values'][:5]: + selected_mark = "✓" if value['selected'] else " " + print(f" [{selected_mark}] {value['label']}: {value['count']}") + +# 示例 3:分页查询 +page = 1 +page_size = 20 +total_pages = 5 + +for page in range(1, total_pages + 1): + result = search_products( + query="玩具", + size=page_size, + from_=(page - 1) * page_size + ) + print(f"\n第 {page} 页:") + for hit in result['hits']: + product = hit['_source'] + print(f" - {product['name']}") +``` + +### JavaScript 完整示例 + +```javascript +// 搜索引擎客户端 +class SearchClient { + constructor(baseUrl) { + this.baseUrl = baseUrl; + } + + async search({ + query, + size = 20, + from = 0, + filters = null, + rangeFilters = null, + facets = null, + sortBy = null, + sortOrder = 'desc', + debug = false + }) { + const payload = { + query, + size, + from + }; + + if (filters) payload.filters = filters; + if (rangeFilters) payload.range_filters = rangeFilters; + if (facets) payload.facets = facets; + if (sortBy) { + payload.sort_by = sortBy; + payload.sort_order = sortOrder; + } + if (debug) payload.debug = debug; + + const response = await fetch(`${this.baseUrl}/search/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return await response.json(); + } + + async searchByImage(imageUrl, options = {}) { + const payload = { + image_url: imageUrl, + size: options.size || 20, + filters: options.filters || null, + range_filters: options.rangeFilters || null + }; + + const response = await fetch(`${this.baseUrl}/search/image`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return await response.json(); + } +} + +// 使用示例 +const client = new SearchClient('http://localhost:6002'); + +// 简单搜索 +const result1 = await client.search({ + query: "芭比娃娃", + size: 20 +}); +console.log(`找到 ${result1.total} 个结果`); + +// 带过滤和分面的搜索 +const result2 = await client.search({ + query: "玩具", + size: 20, + filters: { + categoryName_keyword: ["玩具", "益智玩具"] + }, + rangeFilters: { + price: { gte: 50, lte: 200 } + }, + facets: [ + { field: "brandName_keyword", size: 15 }, + { field: "categoryName_keyword", size: 15 } + ], + sortBy: "price", + sortOrder: "asc" +}); + +// 显示分面结果 +result2.facets.forEach(facet => { + console.log(`\n${facet.label}:`); + facet.values.forEach(value => { + const selected = value.selected ? '✓' : ' '; + console.log(` [${selected}] ${value.label}: ${value.count}`); + }); +}); + +// 显示商品 +result2.hits.forEach(hit => { + const product = hit._source; + console.log(`${product.name} - ¥${product.price}`); +}); +``` + +### 前端完整示例(Vue.js 风格) + +```javascript +// 搜索组件 +const SearchComponent = { + data() { + return { + query: '', + results: [], + facets: [], + filters: {}, + rangeFilters: {}, + total: 0, + currentPage: 1, + pageSize: 20 + }; + }, + methods: { + async search() { + const response = await fetch('http://localhost:6002/search/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: this.query, + size: this.pageSize, + from: (this.currentPage - 1) * this.pageSize, + filters: this.filters, + range_filters: this.rangeFilters, + facets: [ + { field: 'categoryName_keyword', size: 15 }, + { field: 'brandName_keyword', size: 15 } + ] + }) + }); + + const data = await response.json(); + this.results = data.hits; + this.facets = data.facets || []; + this.total = data.total; + }, + + toggleFilter(field, value) { + if (!this.filters[field]) { + this.filters[field] = []; + } + + const index = this.filters[field].indexOf(value); + if (index > -1) { + this.filters[field].splice(index, 1); + if (this.filters[field].length === 0) { + delete this.filters[field]; + } + } else { + this.filters[field].push(value); + } + + this.currentPage = 1; + this.search(); + }, + + setPriceRange(min, max) { + if (min !== null || max !== null) { + this.rangeFilters.price = {}; + if (min !== null) this.rangeFilters.price.gte = min; + if (max !== null) this.rangeFilters.price.lte = max; + } else { + delete this.rangeFilters.price; + } + this.currentPage = 1; + this.search(); + } + } +}; +``` + +--- + +## 调试与优化 + +### 启用调试模式 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "debug": true + }' +``` + +**响应包含调试信息**: +```json +{ + "hits": [...], + "total": 118, + "debug_info": { + "query_analysis": { + "original_query": "玩具", + "normalized_query": "玩具", + "rewritten_query": "玩具", + "detected_language": "zh", + "translations": {"en": "toy"} + }, + "es_query": { + "query": {...}, + "size": 10 + }, + "stage_timings": { + "query_parsing": 5.3, + "elasticsearch_search": 35.1, + "result_processing": 4.8 + } + } +} +``` + +### 设置最小分数阈值 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "min_score": 5.0 + }' +``` + +说明:只返回相关性分数 ≥ 5.0 的结果。 + +--- + +## 常见使用场景 + +### 场景 1:电商分类页 + +```bash +# 显示某个类目下的所有商品,按价格排序,提供品牌筛选 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "*", + "filters": { + "categoryName_keyword": "玩具" + }, + "facets": [ + {"field": "brandName_keyword", "size": 20}, + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + } + ], + "sort_by": "min_price", + "sort_order": "asc", + "size": 24 + }' +``` + +### 场景 2:搜索结果页 + +```bash +# 用户搜索关键词,提供筛选和排序 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "芭比娃娃", + "facets": [ + {"field": "categoryName_keyword", "size": 10}, + {"field": "brandName_keyword", "size": 10}, + {"field": "price", "type": "range", "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100+", "from": 100} + ]} + ], + "size": 20 + }' +``` + +### 场景 3:促销专区 + +```bash +# 显示特定价格区间的商品 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "*", + "range_filters": { + "price": { + "gte": 50, + "lte": 100 + } + }, + "facets": ["categoryName_keyword", "brandName_keyword"], + "sort_by": "min_price", + "sort_order": "asc", + "size": 50 + }' +``` + +### 场景 4:新品推荐 + +```bash +# 最近更新的商品 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "*", + "range_filters": { + "days_since_last_update": { + "lte": 7 + } + }, + "sort_by": "create_time", + "sort_order": "desc", + "size": 20 + }' +``` + +--- + +## 错误处理 + +### 示例 1:参数错误 + +```bash +# 错误:range_filters 缺少操作符 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "range_filters": { + "price": {} + } + }' +``` + +**响应**: +```json +{ + "error": "Validation error", + "detail": "至少需要指定一个范围边界(gte, gt, lte, lt)", + "timestamp": 1699800000 +} +``` + +### 示例 2:空查询 + +```bash +# 错误:query 为空 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "" + }' +``` + +**响应**: +```json +{ + "error": "Validation error", + "detail": "query field required", + "timestamp": 1699800000 +} +``` + +--- + +## 性能优化建议 + +### 1. 合理使用分面 + +```bash +# ❌ 不推荐:请求太多分面 +{ + "facets": [ + {"field": "field1", "size": 100}, + {"field": "field2", "size": 100}, + {"field": "field3", "size": 100}, + // ... 10+ facets + ] +} + +# ✅ 推荐:只请求必要的分面 +{ + "facets": [ + {"field": "categoryName_keyword", "size": 15}, + {"field": "brandName_keyword", "size": 15} + ] +} +``` + +### 2. 控制返回数量 + +```bash +# ❌ 不推荐:一次返回太多 +{ + "size": 100 +} + +# ✅ 推荐:分页查询 +{ + "size": 20, + "from": 0 +} +``` + +### 3. 使用适当的过滤器 + +```bash +# ✅ 推荐:先过滤后搜索 +{ + "query": "玩具", + "filters": { + "categoryName_keyword": "玩具" + } +} +``` + +--- + +## 高级技巧 + +### 技巧 1:获取所有类目 + +```bash +# 使用通配符查询 + 分面 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "*", + "size": 0, + "facets": [ + {"field": "categoryName_keyword", "size": 100} + ] + }' +``` + +### 技巧 2:价格分布统计 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 0, + "facets": [ + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200-500", "from": 200, "to": 500}, + {"key": "500+", "from": 500} + ] + } + ] + }' +``` + +### 技巧 3:组合多种查询类型 + +```bash +# 布尔表达式 + 过滤器 + 分面 + 排序 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "(玩具 OR 游戏) AND 儿童 ANDNOT 电子", + "filters": { + "categoryName_keyword": ["玩具", "益智玩具"] + }, + "range_filters": { + "price": {"gte": 20, "lte": 100}, + "days_since_last_update": {"lte": 30} + }, + "facets": [ + {"field": "brandName_keyword", "size": 20} + ], + "sort_by": "min_price", + "sort_order": "asc", + "size": 20 + }' +``` + +--- + +## 测试数据 + +如果你需要测试数据,可以使用以下查询: + +```bash +# 测试类目:玩具 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{"query": "玩具", "size": 5}' + +# 测试品牌:乐高 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{"query": "brand:乐高", "size": 5}' + +# 测试布尔表达式 +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{"query": "玩具 AND 乐高", "size": 5}' +``` + +--- + +**文档版本**: 3.0 +**最后更新**: 2024-11-12 +**相关文档**: `API_DOCUMENTATION.md` + diff --git a/docs/TestingPipeline_README.md b/docs/TestingPipeline_README.md deleted file mode 100644 index 06ca2c1..0000000 --- a/docs/TestingPipeline_README.md +++ /dev/null @@ -1,459 +0,0 @@ -# 搜索引擎测试流水线指南 - -## 概述 - -本文档介绍了搜索引擎项目的完整测试流水线,包括测试环境搭建、测试执行、结果分析等内容。测试流水线设计用于commit前的自动化质量保证。 - -## 🏗️ 测试架构 - -### 测试层次 - -``` -测试流水线 -├── 代码质量检查 (Code Quality) -│ ├── 代码格式化检查 (Black, isort) -│ ├── 静态分析 (Flake8, MyPy, Pylint) -│ └── 安全扫描 (Safety, Bandit) -│ -├── 单元测试 (Unit Tests) -│ ├── RequestContext测试 -│ ├── Searcher测试 -│ ├── QueryParser测试 -│ └── BooleanParser测试 -│ -├── 集成测试 (Integration Tests) -│ ├── 端到端搜索流程测试 -│ ├── 多组件协同测试 -│ └── 错误处理测试 -│ -├── API测试 (API Tests) -│ ├── REST API接口测试 -│ ├── 参数验证测试 -│ ├── 并发请求测试 -│ └── 错误响应测试 -│ -└── 性能测试 (Performance Tests) - ├── 响应时间测试 - ├── 并发性能测试 - └── 资源使用测试 -``` - -### 核心组件 - -1. **RequestContext**: 请求级别的上下文管理器,用于跟踪测试过程中的所有数据 -2. **测试环境管理**: 自动化启动/停止测试依赖服务 -3. **测试执行引擎**: 统一的测试运行和结果收集 -4. **报告生成系统**: 多格式的测试报告生成 - -## 🚀 快速开始 - -### 本地测试环境 - -1. **启动测试环境** - ```bash - # 启动所有必要的测试服务 - ./scripts/start_test_environment.sh - ``` - -2. **运行完整测试套件** - ```bash - # 运行所有测试 - python scripts/run_tests.py - - # 或者使用pytest直接运行 - pytest tests/ -v - ``` - -3. **停止测试环境** - ```bash - ./scripts/stop_test_environment.sh - ``` - -### CI/CD测试 - -1. **GitHub Actions** - - Push到主分支自动触发 - - Pull Request自动运行 - - 手动触发支持 - -2. **测试报告** - - 自动生成并上传 - - PR评论显示测试摘要 - - 详细报告下载 - -## 📋 测试类型详解 - -### 1. 单元测试 (Unit Tests) - -**位置**: `tests/unit/` - -**目的**: 测试单个函数、类、模块的功能 - -**覆盖范围**: -- `test_context.py`: RequestContext功能测试 -- `test_searcher.py`: Searcher核心功能测试 -- `test_query_parser.py`: QueryParser处理逻辑测试 - -**运行方式**: -```bash -# 运行所有单元测试 -pytest tests/unit/ -v - -# 运行特定测试 -pytest tests/unit/test_context.py -v - -# 生成覆盖率报告 -pytest tests/unit/ --cov=. --cov-report=html -``` - -### 2. 集成测试 (Integration Tests) - -**位置**: `tests/integration/` - -**目的**: 测试多个组件协同工作的功能 - -**覆盖范围**: -- `test_search_integration.py`: 完整搜索流程集成 -- 数据库、ES、搜索器集成测试 -- 错误传播和处理测试 - -**运行方式**: -```bash -# 运行集成测试(需要启动测试环境) -pytest tests/integration/ -v -m "not slow" - -# 运行包含慢速测试的集成测试 -pytest tests/integration/ -v -``` - -### 3. API测试 (API Tests) - -**位置**: `tests/integration/test_api_integration.py` - -**目的**: 测试HTTP API接口的功能和性能 - -**覆盖范围**: -- 基本搜索API -- 参数验证 -- 错误处理 -- 并发请求 -- Unicode支持 - -**运行方式**: -```bash -# 运行API测试 -pytest tests/integration/test_api_integration.py -v -``` - -### 4. 性能测试 (Performance Tests) - -**目的**: 验证系统性能指标 - -**测试内容**: -- 搜索响应时间 -- API并发处理能力 -- 资源使用情况 - -**运行方式**: -```bash -# 运行性能测试 -python scripts/run_performance_tests.py -``` - -## 🛠️ 环境配置 - -### 测试环境要求 - -1. **Python环境** - ```bash - # 创建测试环境 - conda create -n searchengine-test python=3.9 - conda activate searchengine-test - - # 安装依赖 - pip install -r requirements.txt - pip install pytest pytest-cov pytest-json-report - ``` - -2. **Elasticsearch** - ```bash - # 使用Docker启动ES - docker run -d \ - --name elasticsearch \ - -p 9200:9200 \ - -e "discovery.type=single-node" \ - -e "xpack.security.enabled=false" \ - elasticsearch:8.8.0 - ``` - -3. **环境变量** - ```bash - export ES_HOST="http://localhost:9200" - export ES_USERNAME="elastic" - export ES_PASSWORD="changeme" - export API_HOST="127.0.0.1" - export API_PORT="6003" - export TENANT_ID="test_tenant" - export TESTING_MODE="true" - ``` - -### 服务依赖 - -测试环境需要以下服务: - -1. **Elasticsearch** (端口9200) - - 存储和搜索测试数据 - - 支持中文和英文索引 - -2. **API服务** (端口6003) - - FastAPI测试服务 - - 提供搜索接口 - -3. **测试数据库** - - 预配置的测试索引 - - 包含测试数据 - -## 📊 测试报告 - -### 报告类型 - -1. **实时控制台输出** - - 测试进度显示 - - 失败详情 - - 性能摘要 - -2. **JSON格式报告** - ```json - { - "timestamp": "2024-01-01T10:00:00", - "summary": { - "total_tests": 150, - "passed": 148, - "failed": 2, - "success_rate": 98.7 - }, - "suites": { ... } - } - ``` - -3. **文本格式报告** - - 人类友好的格式 - - 包含测试摘要和详情 - - 适合PR评论 - -4. **HTML覆盖率报告** - - 代码覆盖率可视化 - - 分支和行覆盖率 - - 缺失测试高亮 - -### 报告位置 - -``` -test_logs/ -├── unit_test_results.json # 单元测试结果 -├── integration_test_results.json # 集成测试结果 -├── api_test_results.json # API测试结果 -├── test_report_20240101_100000.txt # 文本格式摘要 -├── test_report_20240101_100000.json # JSON格式详情 -└── htmlcov/ # HTML覆盖率报告 -``` - -## 🔄 CI/CD集成 - -### GitHub Actions工作流 - -**触发条件**: -- Push到主分支 -- Pull Request创建/更新 -- 手动触发 - -**工作流阶段**: - -1. **代码质量检查** - - 代码格式验证 - - 静态代码分析 - - 安全漏洞扫描 - -2. **单元测试** - - 多Python版本矩阵测试 - - 代码覆盖率收集 - - 自动上传到Codecov - -3. **集成测试** - - 服务依赖启动 - - 端到端功能测试 - - 错误处理验证 - -4. **API测试** - - 接口功能验证 - - 参数校验测试 - - 并发请求测试 - -5. **性能测试** - - 响应时间检查 - - 资源使用监控 - - 性能回归检测 - -6. **测试报告生成** - - 结果汇总 - - 报告上传 - - PR评论更新 - -### 工作流配置 - -**文件**: `.github/workflows/test.yml` - -**关键特性**: -- 并行执行提高效率 -- 服务容器化隔离 -- 自动清理资源 -- 智能缓存依赖 - -## 🧪 测试最佳实践 - -### 1. 测试编写原则 - -- **独立性**: 每个测试应该独立运行 -- **可重复性**: 测试结果应该一致 -- **快速执行**: 单元测试应该快速完成 -- **清晰命名**: 测试名称应该描述测试内容 - -### 2. 测试数据管理 - -```python -# 使用fixture提供测试数据 -@pytest.fixture -def sample_tenant_config(): - return TenantConfig( - tenant_id="test_tenant", - es_index_name="test_products" - ) - -# 使用mock避免外部依赖 -@patch('search.searcher.ESClient') -def test_search_with_mock_es(mock_es_client, test_searcher): - mock_es_client.search.return_value = mock_response - result = test_searcher.search("test query") - assert result is not None -``` - -### 3. RequestContext集成 - -```python -def test_with_context(test_searcher): - context = create_request_context("test-req", "test-user") - - result = test_searcher.search("test query", context=context) - - # 验证context被正确更新 - assert context.query_analysis.original_query == "test query" - assert context.get_stage_duration("elasticsearch_search") > 0 -``` - -### 4. 性能测试指南 - -```python -def test_search_performance(client): - start_time = time.time() - response = client.get("/search", params={"q": "test query"}) - response_time = (time.time() - start_time) * 1000 - - assert response.status_code == 200 - assert response_time < 2000 # 2秒内响应 -``` - -## 🚨 故障排除 - -### 常见问题 - -1. **Elasticsearch连接失败** - ```bash - # 检查ES状态 - curl http://localhost:9200/_cluster/health - - # 重启ES服务 - docker restart elasticsearch - ``` - -2. **测试端口冲突** - ```bash - # 检查端口占用 - lsof -i :6003 - - # 修改API端口 - export API_PORT="6004" - ``` - -3. **依赖包缺失** - ```bash - # 重新安装依赖 - pip install -r requirements.txt - pip install pytest pytest-cov pytest-json-report - ``` - -4. **测试数据问题** - ```bash - # 重新创建测试索引 - curl -X DELETE http://localhost:9200/test_products - ./scripts/start_test_environment.sh - ``` - -### 调试技巧 - -1. **详细日志输出** - ```bash - pytest tests/unit/test_context.py -v -s --tb=long - ``` - -2. **运行单个测试** - ```bash - pytest tests/unit/test_context.py::TestRequestContext::test_create_context -v - ``` - -3. **调试模式** - ```python - import pdb; pdb.set_trace() - ``` - -4. **性能分析** - ```bash - pytest --profile tests/ - ``` - -## 📈 持续改进 - -### 测试覆盖率目标 - -- **单元测试**: > 90% -- **集成测试**: > 80% -- **API测试**: > 95% - -### 性能基准 - -- **搜索响应时间**: < 2秒 -- **API并发处理**: 100 QPS -- **系统资源使用**: < 80% CPU, < 4GB RAM - -### 质量门禁 - -- **所有测试必须通过** -- **代码覆盖率不能下降** -- **性能不能显著退化** -- **不能有安全漏洞** - -## 📚 相关文档 - -- [RequestContext使用指南](RequestContext_README.md) -- [API文档](../api/README.md) -- [配置指南](../config/README.md) -- [部署指南](Deployment_README.md) - -## 🤝 贡献指南 - -1. 为新功能编写对应的测试 -2. 确保测试覆盖率不下降 -3. 遵循测试命名约定 -4. 更新相关文档 -5. 运行完整测试套件后提交 - -通过这套完整的测试流水线,我们可以确保搜索引擎代码的质量、性能和可靠性,为用户提供稳定高效的搜索服务。 \ No newline at end of file diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md new file mode 100644 index 0000000..8305050 --- /dev/null +++ b/docs/Usage-Guide.md @@ -0,0 +1,441 @@ +# 使用指南 - SearchEngine + +本文档提供完整的使用指南,包括环境准备、服务启动、配置说明、日志查看等。 + +## 目录 + +1. [环境准备](#环境准备) +2. [服务启动](#服务启动) +3. [配置说明](#配置说明) +4. [查看日志](#查看日志) +5. [测试验证](#测试验证) +6. [常见问题](#常见问题) + +--- + +## 环境准备 + +### 系统要求 + +- **操作系统**: Linux (推荐 CentOS 7+ / Ubuntu 18.04+) +- **Python**: 3.8+ +- **内存**: 建议 8GB+ +- **磁盘**: 10GB+ (包含模型文件) +- **Elasticsearch**: 8.x (可通过Docker运行) + +### 安装依赖 + +#### 1. 安装Python依赖 + +```bash +cd /home/tw/SearchEngine +pip install -r requirements.txt +``` + +#### 2. 启动Elasticsearch + +**方式1: 使用Docker(推荐)** + +```bash +docker run -d \ + --name elasticsearch \ + -p 9200:9200 \ + -e "discovery.type=single-node" \ + -e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \ + elasticsearch:8.11.0 +``` + +**方式2: 本地安装** + +参考 [Elasticsearch官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/8.11/install-elasticsearch.html) + +#### 3. 配置环境变量 + +创建 `.env` 文件: + +```bash +# MySQL配置 +DB_HOST=120.79.247.228 +DB_PORT=3316 +DB_DATABASE=saas +DB_USERNAME=saas +DB_PASSWORD=your_password + +# Elasticsearch配置 +ES_HOST=http://localhost:9200 +ES_USERNAME=essa +ES_PASSWORD=4hOaLaf41y2VuI8y + +# Redis配置(可选,用于缓存) +REDIS_HOST=localhost +REDIS_PORT=6479 +REDIS_PASSWORD=BMfv5aI31kgHWtlx + +# DeepL翻译API(可选) +DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed + +# API服务配置 +API_HOST=0.0.0.0 +API_PORT=6002 +``` + +--- + +## 服务启动 + +### 方式1: 一键启动(推荐) + +```bash +cd /home/tw/SearchEngine +./run.sh +``` + +这个脚本会自动: +1. 创建日志目录 +2. 启动后端API服务(后台运行) +3. 启动前端Web界面(后台运行) +4. 等待服务就绪 + +启动完成后,访问: +- **前端界面**: http://localhost:6003 +- **后端API**: http://localhost:6002 +- **API文档**: http://localhost:6002/docs + +### 方式2: 分步启动 + +#### 启动后端服务 + +```bash +./scripts/start_backend.sh +``` + +后端API会在 http://localhost:6002 启动 + +#### 启动前端服务 + +```bash +./scripts/start_frontend.sh +``` + +前端界面会在 http://localhost:6003 启动 + +### 方式3: 手动启动 + +#### 启动后端API服务 + +```bash +python -m api.app \ + --host 0.0.0.0 \ + --port 6002 \ + --es-host http://localhost:9200 \ + --reload +``` + +#### 启动前端服务(可选) + +```bash +# 使用Python简单HTTP服务器 +cd frontend +python -m http.server 6003 +``` + +### 停止服务 + +```bash +# 停止后端 +kill $(cat logs/backend.pid) + +# 停止前端 +kill $(cat logs/frontend.pid) + +# 或使用停止脚本 +./scripts/stop.sh +``` + +### 服务端口 + +| 服务 | 端口 | URL | +|------|------|-----| +| Elasticsearch | 9200 | http://localhost:9200 | +| Backend API | 6002 | http://localhost:6002 | +| Frontend Web | 6003 | http://localhost:6003 | +| API Docs | 6002 | http://localhost:6002/docs | + +--- + +## 配置说明 + +### 环境配置文件 (.env) + +主要配置项说明: + +```bash +# Elasticsearch配置 +ES_HOST=http://localhost:9200 +ES_USERNAME=essa +ES_PASSWORD=4hOaLaf41y2VuI8y + +# MySQL配置 +DB_HOST=120.79.247.228 +DB_PORT=3316 +DB_DATABASE=saas +DB_USERNAME=saas +DB_PASSWORD=your_password + +# Redis配置(可选,用于缓存) +REDIS_HOST=localhost +REDIS_PORT=6479 +REDIS_PASSWORD=BMfv5aI31kgHWtlx + +# DeepL翻译API +DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed + +# API服务配置 +API_HOST=0.0.0.0 +API_PORT=6002 +``` + +### 修改配置 + +1. 编辑 `.env` 文件 +2. 重启相关服务 + +--- + +## 查看日志 + +### 日志文件位置 + +日志文件存储在 `logs/` 目录下: + +- `logs/backend.log` - 后端服务日志 +- `logs/frontend.log` - 前端服务日志 +- `logs/search_engine.log` - 应用主日志(按天轮转) +- `logs/errors.log` - 错误日志(按天轮转) + +### 查看实时日志 + +```bash +# 查看后端日志 +tail -f logs/backend.log + +# 查看前端日志 +tail -f logs/frontend.log + +# 查看应用主日志 +tail -f logs/search_engine.log + +# 查看错误日志 +tail -f logs/errors.log +``` + +### 日志级别 + +日志级别可以通过环境变量 `LOG_LEVEL` 设置: + +```bash +# 在 .env 文件中设置 +LOG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR, CRITICAL +``` + +### 日志轮转 + +日志文件按天自动轮转,保留30天的历史日志。 + +--- + +## 测试验证 + +### 1. 健康检查 + +```bash +curl http://localhost:6002/admin/health +``` + +**预期响应**: +```json +{ + "status": "healthy", + "elasticsearch": "connected" +} +``` + +### 2. 索引统计 + +```bash +curl http://localhost:6002/admin/stats +``` + +### 3. 简单搜索测试 + +```bash +curl -X POST http://localhost:6002/search/ \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "玩具", + "size": 10 + }' +``` + +或者通过查询参数: + +```bash +curl -X POST "http://localhost:6002/search/?tenant_id=2" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具", + "size": 10 + }' +``` + +### 4. 带过滤器的搜索 + +```bash +curl -X POST http://localhost:6002/search/ \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "玩具", + "size": 10, + "filters": { + "categoryName_keyword": ["玩具", "益智玩具"] + }, + "range_filters": { + "price": {"gte": 50, "lte": 200} + } + }' +``` + +### 5. 分面搜索测试 + +```bash +curl -X POST http://localhost:6002/search/ \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "query": "玩具", + "size": 10, + "facets": [ + {"field": "categoryName_keyword", "size": 15}, + {"field": "brandName_keyword", "size": 15} + ] + }' +``` + +### 6. 图片搜索测试 + +```bash +curl -X POST http://localhost:6002/search/image \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{ + "image_url": "https://oss.essa.cn/example.jpg", + "size": 10 + }' +``` + +### 7. 前端界面测试 + +访问 http://localhost:6003 或 http://localhost:6002/ 进行可视化测试。 + +**注意**: 所有搜索接口都需要通过 `X-Tenant-ID` 请求头或 `tenant_id` 查询参数指定租户ID。 + +--- + +## 常见问题 + +### Q1: MySQL连接失败 + +**症状**: `Failed to connect to MySQL` + +**解决方案**: +```bash +# 检查MySQL服务状态 +mysql -h 120.79.247.228 -P 3316 -u saas -p -e "SELECT 1" + +# 检查配置 +cat .env | grep DB_ +``` + +### Q2: Elasticsearch连接失败 + +**症状**: `Failed to connect to Elasticsearch` + +**解决方案**: +```bash +# 检查ES服务状态 +curl http://localhost:9200 + +# 检查ES版本 +curl http://localhost:9200 | grep version + +# 确认配置 +cat .env | grep ES_ +``` + +### Q3: 服务启动失败 + +**症状**: `Address already in use` 或端口被占用 + +**解决方案**: +```bash +# 查看占用端口的进程 +lsof -i :6002 # 后端 +lsof -i :6003 # 前端 +lsof -i :9200 # ES + +# 杀掉进程 +kill -9 + +# 或修改端口配置 +``` + +### Q4: 搜索无结果 + +**症状**: 搜索返回空结果 + +**解决方案**: +```bash +# 检查ES中是否有数据 +curl http://localhost:9200/search_products/_count + +# 检查tenant_id过滤是否正确 +curl -X POST http://localhost:6002/search/ \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 2" \ + -d '{"query": "*", "size": 10, "debug": true}' +``` + +### Q5: 前端无法连接后端 + +**症状**: CORS错误 + +**解决方案**: +- 确保后端在 http://localhost:6002 运行 +- 检查浏览器控制台错误信息 +- 检查后端日志中的CORS配置 + +### Q6: 翻译不工作 + +**症状**: 翻译返回原文 + +**解决方案**: +- 检查DEEPL_AUTH_KEY是否正确 +- 如果没有API key,系统会使用mock模式(返回原文) + +--- + +## 相关文档 + +- **测试数据构造文档**: `TEST_DATA_GUIDE.md` - 如何构造和导入测试数据 +- **API接口文档**: `API_INTEGRATION_GUIDE.md` - 完整的API对接指南 +- **字段说明文档**: `INDEX_FIELDS_DOCUMENTATION.md` - 索引字段详细说明 +- **设计文档**: `设计文档.md` - 系统架构和设计说明 +- **README**: `README.md` - 项目概述和快速开始 + +--- + +**文档版本**: v2.0 +**最后更新**: 2024-12 + diff --git a/docs/reference/商品数据源入ES配置规范.md b/docs/reference/商品数据源入ES配置规范.md new file mode 100644 index 0000000..a503d64 --- /dev/null +++ b/docs/reference/商品数据源入ES配置规范.md @@ -0,0 +1,221 @@ +根据您提供的内容,我将其整理为规范的Markdown格式: + +# ES索引配置文档 + +## 1. 全局配置 + +### 1.1 文本字段相关性设定 +需要修改所有text字段相关性算法-BM25算法的默认参数: +```json +"similarity": { + "default": { + "type": "BM25", + "b": "0.0", + "k1": "0.0" + } +} +``` + +### 1.2 索引分片设定 +- `number_of_replicas`:0/1 +- `number_of_shards`:设置建议 分片数 <= ES集群的总CPU核心个数/ (副本数 + 1) + +### 1.3 索引刷新时间设定 +- `refresh_interval`:默认30S,根据客户需要进行调整 +```json +"refresh_interval": "30s" +``` + +## 2. 单个字段配置 + +| 分析方式 | 字段预处理和ES输入格式要求 | 对应ES mapping配置 | 备注 | +|---------|--------------------------|-------------------|------| +| 电商通用分析-中文 | - | ```json { "type": "text", "analyzer": "index_ansj", "search_analyzer": "query_ansj" } ``` | - | +| 文本-多语言向量化 | 调用"文本向量化"模块得到1024维向量 | ```json { "type": "dense_vector", "dims": 1024, "index": true, "similarity": "dot_product" } ``` | 1. 依赖"文本向量化"模块
2. 如果定期全量,需要对向量化结果做缓存 | +| 图片-向量化 | 调用"图片向量化"模块得到1024维向量 | ```json { "type": "nested", "properties": { "vector": { "type": "dense_vector", "dims": 1024, "similarity": "dot_product" }, "url": { "type": "text" } } } ``` | 1. 依赖"图片向量化"模块
2. 如果定期全量,需要对向量化结果做缓存 | +| 关键词 | ES输入格式:list或者单个值 | ```json {"type": "keyword"} ``` | - | +| 电商通用分析-英文 | - | ```json {"type": "text", "analyzer": "english"} ``` | - | +| 电商通用分析-阿拉伯文 | - | ```json {"type": "text", "analyzer": "arabic"} ``` | - | +| 电商通用分析-西班牙文 | - | ```json {"type": "text", "analyzer": "spanish"} ``` | - | +| 电商通用分析-俄文 | - | ```json {"type": "text", "analyzer": "russian"} ``` | - | +| 电商通用分析-日文 | - | ```json {"type": "text", "analyzer": "japanese"} ``` | - | +| 数值-整数 | - | ```json {"type": "long"} ``` | - | +| 数值-浮点型 | - | ```json {"type": "float"} ``` | - | +| 分值 | 输入是float,配置处理方式:log, pow, sigmoid等 | TODO:给代码, log | - | +| 子串 | - | 暂时不支持 | - | +| ngram匹配或前缀匹配或边缘前缀匹配 | - | 暂时不支持 | 以后根据需要再添加 | + +这样整理后,文档结构更加清晰,表格格式规范,便于阅读和理解。 + + +参考 opensearch: + +数据接口 +文本相关性字段 +向量相关性字段 +3. 模块提取 +文本向量化 +import sys +import torch +from sentence_transformers import SentenceTransformer +import time +import threading +from modelscope import snapshot_download +from transformers import AutoModel +import os +from openai import OpenAI +from config.logging_config import get_app_logger + +# Get logger for this module +logger = get_app_logger(__name__) + +class BgeEncoder: + _instance = None + _lock = threading.Lock() + + def __new__(cls, model_dir='Xorbits/bge-m3'): + with cls._lock: + if cls._instance is None: + cls._instance = super(BgeEncoder, cls).__new__(cls) + logger.info("[BgeEncoder] Creating a new instance with model directory: %s", model_dir) + cls._instance.model = SentenceTransformer(snapshot_download(model_dir)) + logger.info("[BgeEncoder] New instance has been created") + return cls._instance + + def encode(self, sentences, normalize_embeddings=True, device='cuda'): + # Move model to specified device + if device == 'gpu': + device = 'cuda' + self.model = self.model.to(device) + embeddings = self.model.encode(sentences, normalize_embeddings=normalize_embeddings, device=device, show_progress_bar=False) + return embeddings +图片向量化 +import sys +import os +import io +import requests +import torch +import numpy as np +from PIL import Image +import logging +import threading +from typing import List, Optional, Union +from config.logging_config import get_app_logger +import cn_clip.clip as clip +from cn_clip.clip import load_from_name + +# Get logger for this module +logger = get_app_logger(__name__) + +# DEFAULT_MODEL_NAME = "ViT-L-14-336" # ["ViT-B-16", "ViT-L-14", "ViT-L-14-336", "ViT-H-14", "RN50"] +DEFAULT_MODEL_NAME = "ViT-H-14" +MODEL_DOWNLOAD_DIR = "/data/tw/uat/EsSearcher" + +class CLIPImageEncoder: + """CLIP Image Encoder for generating image embeddings using cn_clip""" + + _instance = None + _lock = threading.Lock() + + def __new__(cls, model_name=DEFAULT_MODEL_NAME, device=None): + with cls._lock: + if cls._instance is None: + cls._instance = super(CLIPImageEncoder, cls).__new__(cls) + logger.info(f"[CLIPImageEncoder] Creating new instance with model: {model_name}") + cls._instance._initialize_model(model_name, device) + return cls._instance + + def _initialize_model(self, model_name, device): + """Initialize the CLIP model using cn_clip""" + try: + self.device = device if device else ("cuda" if torch.cuda.is_available() else "cpu") + self.model, self.preprocess = load_from_name(model_name, device=self.device, download_root=MODEL_DOWNLOAD_DIR) + self.model.eval() + self.model_name = model_name + logger.info(f"[CLIPImageEncoder] Model {model_name} initialized successfully on device {self.device}") + + except Exception as e: + logger.error(f"[CLIPImageEncoder] Failed to initialize model: {str(e)}") + raise + + def validate_image(self, image_data: bytes) -> Image.Image: + """Validate image data and return PIL Image if valid""" + try: + image_stream = io.BytesIO(image_data) + image = Image.open(image_stream) + image.verify() + image_stream.seek(0) + image = Image.open(image_stream) + if image.mode != 'RGB': + image = image.convert('RGB') + return image + except Exception as e: + raise ValueError(f"Invalid image data: {str(e)}") + + def download_image(self, url: str, timeout: int = 10) -> bytes: + """Download image from URL""" + try: + if url.startswith(('http://', 'https://')): + response = requests.get(url, timeout=timeout) + if response.status_code != 200: + raise ValueError(f"HTTP {response.status_code}") + return response.content + else: + # Local file path + with open(url, 'rb') as f: + return f.read() + except Exception as e: + raise ValueError(f"Failed to download image from {url}: {str(e)}") + + def preprocess_image(self, image: Image.Image, max_size: int = 1024) -> Image.Image: + """Preprocess image for CLIP model""" + # Resize if too large + if max(image.size) > max_size: + ratio = max_size / max(image.size) + new_size = tuple(int(dim * ratio) for dim in image.size) + image = image.resize(new_size, Image.Resampling.LANCZOS) + return image + + def encode_text(self, text): + """Encode text to embedding vector using cn_clip""" + text_data = clip.tokenize([text] if type(text) == str else text).to(self.device) + with torch.no_grad(): + text_features = self.model.encode_text(text_data) + text_features /= text_features.norm(dim=-1, keepdim=True) + return text_features + + def encode_image(self, image: Image.Image) -> Optional[np.ndarray]: + """Encode image to embedding vector using cn_clip""" + if not isinstance(image, Image.Image): + raise ValueError("CLIPImageEncoder.encode_image Input must be a PIL.Image") + + try: + infer_data = self.preprocess(image).unsqueeze(0).to(self.device) + with torch.no_grad(): + image_features = self.model.encode_image(infer_data) + image_features /= image_features.norm(dim=-1, keepdim=True) + return image_features.cpu().numpy().astype('float32')[0] + except Exception as e: + logger.error(f"Failed to process image. Reason: {str(e)}") + return None + + def encode_image_from_url(self, url: str) -> Optional[np.ndarray]: + """Complete pipeline: download, validate, preprocess and encode image from URL""" + try: + # Download image + image_data = self.download_image(url) + + # Validate image + image = self.validate_image(image_data) + + # Preprocess image + image = self.preprocess_image(image) + + # Encode image + embedding = self.encode_image(image) + + return embedding + + except Exception as e: + logger.error(f"Error processing image from URL {url}: {str(e)}") + return None \ No newline at end of file diff --git a/docs/reference/阿里opensearch电商行业.md b/docs/reference/阿里opensearch电商行业.md new file mode 100644 index 0000000..2e54e03 --- /dev/null +++ b/docs/reference/阿里opensearch电商行业.md @@ -0,0 +1,47 @@ +https://help.aliyun.com/zh/open-search/industry-algorithm-edition/e-commerce?spm=a2c4g.11186623.help-menu-29102.d_3_2_1.5a903cfbxOsaHt&scm=20140722.H_99739._.OR_help-T_cn~zh-V_1 + + +## 定义应用结构 +示例如下: +| 字段名称 | 主键 | 字段标签 | 类型 | +|----------------|------|------------|--------------| +| title | | 商品标题 | TEXT | +| text_embedding | | 文本向量 | EMBEDDING | +| image_embedding | | 图片向量 | EMBEDDING | +| category_name | | 类目名称 | TEXT | +| image_url | | | LITERAL_ARRAY| +| description | | 商品描述 | TEXT | +| brand_name | | 品牌名称 | TEXT | +| thumbnail_url | | | LITERAL_ARRAY| +| is_onsale | | | INT | +| url | | | LITERAL | +| brand_id | | | LITERAL | +| series_id | | | LITERAL | +| sold_num | | 商品销量 | INT | +| category_id | | | INT | +| onsale_time | | 上架时间 | INT | +| price | | | DOUBLE | +| series_name | | | TEXT | +| discount_price | | DOUBLE | +| pid | ● | INT | +| sale_price | | DOUBLE | +| act_price | | DOUBLE | + + +## 定义索引结构 + +| 索引名称 | 索引标签 | 包含字段 | 分析方式 | 使用示例 | +| --- | --- | --- | --- | --- | +| default | 默认索引 | category_name, description, brand_name, title, create_by, update_by | 行业 - 电商通用分析 | query=default:“云搜索” | +| category_name | 类目名称索引 | category_name | 行业 - 电商通用分析 | query=category_name:“云搜索” | +| category_id | | category_id | 关键字 | query=category_id:“云搜索” | +| series_name | | series_name | 中文 - 通用分析 | query=series_name:“云搜索” | +| brand_name | | brand_name | 中文 - 通用分析 | query=brand_name:“云搜索” | +| id | | id | 关键字 | query=id:“云搜索” | +| title | 标题索引 | title | 行业 - 电商通用分析 | query=title:“云搜索” | +| seller_id | | seller_id | 关键字 | query=seller_id:“云搜索” | +| brand_id | | brand_id | 关键字 | query=brand_id:“云搜索” | +| series_id | | series_id | 关键字 | query=series_id:“云搜索” | + +上面的只是阿里云的opensearch的例子,我们也要有同样的一套配置,这里支持的“字分析方式” 为ES预先支持的 多种分析器,我们要支持的分析方式参考 @商品数据源入ES配置规范.md + diff --git a/docs/基础配置指南.md b/docs/基础配置指南.md new file mode 100644 index 0000000..013ec98 --- /dev/null +++ b/docs/基础配置指南.md @@ -0,0 +1,257 @@ +# Base Configuration Guide + +店匠通用配置(Base Configuration)使用指南 + +## 概述 + +Base配置是店匠(Shoplazza)通用配置,适用于所有使用店匠标准表的客户。该配置采用SPU级别的索引结构,所有客户共享同一个Elasticsearch索引(`search_products`),通过`tenant_id`字段实现数据隔离。 + +## 核心特性 + +- **SPU级别索引**:每个ES文档代表一个SPU,包含嵌套的variants数组 +- **统一索引**:所有客户共享`search_products`索引 +- **租户隔离**:通过`tenant_id`字段实现数据隔离 +- **配置简化**:配置只包含ES搜索相关配置,不包含MySQL数据源配置 +- **外部友好格式**:API返回格式不包含ES内部字段(`_id`, `_score`, `_source`) + +## 配置说明 + +### 配置文件位置 + +`config/schema/base/config.yaml` + +### 配置内容 + +Base配置**不包含**以下内容: +- `mysql_config` - MySQL数据库配置 +- `main_table` - 主表配置 +- `extension_table` - 扩展表配置 +- `source_table` / `source_column` - 字段数据源映射 + +Base配置**只包含**: +- ES字段定义(字段类型、分析器、boost等) +- 查询域(indexes)配置 +- 查询处理配置(query_config) +- 排序和打分配置(function_score) +- SPU配置(spu_config) + +### 必需字段 + +- `tenant_id` (KEYWORD, required) - 租户隔离字段 + +### 主要字段 + +- `product_id` - 商品ID +- `title`, `brief`, `description` - 文本搜索字段 +- `seo_title`, `seo_description`, `seo_keywords` - SEO字段 +- `vendor`, `product_type`, `tags`, `category` - 分类和标签字段 +- `min_price`, `max_price`, `compare_at_price` - 价格字段 +- `variants` (nested) - 嵌套变体数组 + +## 数据导入流程 + +### 1. 生成测试数据 + +```bash +python scripts/generate_test_data.py \ + --num-spus 100 \ + --tenant-id "1" \ + --start-spu-id 1 \ + --start-sku-id 1 \ + --output test_data.sql +``` + +### 2. 导入测试数据到MySQL + +```bash +python scripts/import_test_data.py \ + --db-host localhost \ + --db-port 3306 \ + --db-database saas \ + --db-username root \ + --db-password password \ + --sql-file test_data.sql \ + --tenant-id "1" +``` + +### 3. 导入数据到Elasticsearch + +```bash +python scripts/ingest_shoplazza.py \ + --db-host localhost \ + --db-port 3306 \ + --db-database saas \ + --db-username root \ + --db-password password \ + --tenant-id "1" \ + --config base \ + --es-host http://localhost:9200 \ + --recreate \ + --batch-size 500 +``` + +## API使用 + +### 搜索接口 + +**端点**: `POST /search/` + +**请求头**: +``` +X-Tenant-ID: 1 +Content-Type: application/json +``` + +**请求体**: +```json +{ + "query": "耳机", + "size": 10, + "from": 0, + "filters": { + "category_keyword": "电子产品" + }, + "facets": ["category_keyword", "vendor_keyword"] +} +``` + +**响应格式**: +```json +{ + "results": [ + { + "product_id": "1", + "title": "蓝牙耳机 Sony", + "handle": "product-1", + "description": "高品质无线蓝牙耳机", + "vendor": "Sony", + "product_type": "电子产品", + "price": 199.99, + "compare_at_price": 299.99, + "currency": "USD", + "image_url": "//cdn.example.com/products/1.jpg", + "in_stock": true, + "variants": [ + { + "variant_id": "1", + "title": "黑色", + "price": 199.99, + "compare_at_price": 299.99, + "sku": "SKU-1-1", + "stock": 50, + "options": { + "option1": "黑色" + } + } + ], + "relevance_score": 0.95 + } + ], + "total": 10, + "max_score": 1.0, + "facets": [ + { + "field": "category_keyword", + "label": "category_keyword", + "type": "terms", + "values": [ + { + "value": "电子产品", + "label": "电子产品", + "count": 5, + "selected": false + } + ] + } + ], + "suggestions": [], + "related_searches": [], + "took_ms": 15, + "query_info": {} +} +``` + +### 响应格式说明 + +#### 主要变化 + +1. **`results`替代`hits`**:返回字段从`hits`改为`results` +2. **结构化结果**:每个结果包含`product_id`, `title`, `variants`, `relevance_score`等字段 +3. **无ES内部字段**:不包含`_id`, `_score`, `_source`等ES内部字段 +4. **嵌套variants**:每个商品包含variants数组,每个variant包含完整的变体信息 +5. **相关性分数**:`relevance_score`是ES原始分数(不进行归一化) + +#### ProductResult字段 + +- `product_id` - 商品ID +- `title` - 商品标题 +- `handle` - 商品handle +- `description` - 商品描述 +- `vendor` - 供应商/品牌 +- `product_type` - 商品类型 +- `tags` - 标签 +- `price` - 最低价格(min_price) +- `compare_at_price` - 原价 +- `currency` - 货币单位(默认USD) +- `image_url` - 主图URL +- `in_stock` - 是否有库存 +- `variants` - 变体列表 +- `relevance_score` - 相关性分数(ES原始分数) + +#### VariantResult字段 + +- `variant_id` - 变体ID +- `title` - 变体标题 +- `price` - 价格 +- `compare_at_price` - 原价 +- `sku` - SKU编码 +- `stock` - 库存数量 +- `options` - 选项(颜色、尺寸等) + +## 测试 + +### 运行测试脚本 + +```bash +python scripts/test_base.py \ + --api-url http://localhost:8000 \ + --tenant-id "1" \ + --test-tenant-2 "2" +``` + +### 测试内容 + +1. **基本搜索**:测试搜索API基本功能 +2. **响应格式验证**:验证返回格式是否符合要求 +3. **Facets聚合**:测试分面搜索功能 +4. **租户隔离**:验证不同租户的数据隔离 + +## 常见问题 + +### Q: 为什么配置中没有MySQL相关配置? + +A: 数据源配置和数据导入流程是写死的脚本,不在搜索配置中。搜索配置只关注ES搜索相关的内容。 + +### Q: 如何为新的租户导入数据? + +A: 使用`ingest_shoplazza.py`脚本,指定不同的`--tenant-id`参数即可。 + +### Q: 如何验证租户隔离是否生效? + +A: 使用`test_base.py`脚本,指定两个不同的`--tenant-id`,检查搜索结果是否隔离。 + +### Q: API返回格式中为什么没有`_id`和`_score`? + +A: 为了提供外部友好的API格式,我们移除了ES内部字段,使用`product_id`和`relevance_score`替代。 + +### Q: 如何添加新的搜索字段? + +A: 在`config/schema/base/config.yaml`中添加字段定义,然后重新生成索引映射并重新导入数据。 + +## 注意事项 + +1. **tenant_id必需**:所有API请求必须提供`tenant_id`(通过请求头`X-Tenant-ID`或查询参数`tenant_id`) +2. **索引共享**:所有客户共享`search_products`索引,确保`tenant_id`字段正确设置 +3. **数据导入**:数据导入脚本是写死的,不依赖配置中的MySQL设置 +4. **配置分离**:搜索配置和数据源配置完全分离,提高可维护性 + diff --git a/docs/搜索API对接指南.md b/docs/搜索API对接指南.md new file mode 100644 index 0000000..3cb0546 --- /dev/null +++ b/docs/搜索API对接指南.md @@ -0,0 +1,1018 @@ +# 搜索API接口对接指南 + +本文档为搜索服务的使用方提供完整的API对接指南,包括接口说明、请求参数、响应格式和使用示例。 + +## 目录 + +1. [快速开始](#快速开始) +2. [接口概览](#接口概览) +3. [文本搜索接口](#文本搜索接口) +4. [图片搜索接口](#图片搜索接口) +5. [响应格式说明](#响应格式说明) +6. [常见场景示例](#常见场景示例) +7. [错误处理](#错误处理) +8. [最佳实践](#最佳实践) + +--- + +## 快速开始 + +### 基础信息 + +- **Base URL**: `http://your-domain:6002` 或 `http://120.76.41.98:6002` +- **协议**: HTTP/HTTPS +- **数据格式**: JSON +- **字符编码**: UTF-8 +- **请求方法**: POST(搜索接口) + +### 最简单的搜索请求 + +```bash +curl -X POST "http://localhost:6002/search/" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "芭比娃娃" + }' +``` + +### Python示例 + +```python +import requests + +url = "http://localhost:6002/search/" +response = requests.post(url, json={"query": "芭比娃娃"}) +data = response.json() +print(f"找到 {data['total']} 个结果") +``` + +### JavaScript示例 + +```javascript +const response = await fetch('http://localhost:6002/search/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query: '芭比娃娃' + }) +}); +const data = await response.json(); +console.log(`找到 ${data.total} 个结果`); +``` + +--- + +## 接口概览 + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 文本搜索 | POST | `/search/` | 执行文本搜索查询 | +| 图片搜索 | POST | `/search/image` | 基于图片相似度搜索 | +| 搜索建议 | GET | `/search/suggestions` | 获取搜索建议(框架,暂未实现) | +| 获取文档 | GET | `/search/{doc_id}` | 根据ID获取单个文档 | +| 健康检查 | GET | `/admin/health` | 检查服务状态 | + +--- + +## 文本搜索接口 + +### 接口信息 + +- **端点**: `POST /search/` +- **描述**: 执行文本搜索查询,支持多语言、布尔表达式、过滤器和分面搜索 + +### 请求参数 + +#### 完整请求体结构 + +```json +{ + "query": "string (required)", + "size": 10, + "from": 0, + "filters": {}, + "range_filters": {}, + "facets": [], + "sort_by": "string", + "sort_order": "desc", + "min_score": 0.0, + "debug": false, + "user_id": "string", + "session_id": "string" +} +``` + +#### 参数详细说明 + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `query` | string | ✅ | - | 搜索查询字符串,支持布尔表达式(AND, OR, RANK, ANDNOT) | +| `size` | integer | ❌ | 10 | 返回结果数量(1-100) | +| `from` | integer | ❌ | 0 | 分页偏移量(用于分页) | +| `filters` | object | ❌ | null | 精确匹配过滤器(见下文) | +| `range_filters` | object | ❌ | null | 数值范围过滤器(见下文) | +| `facets` | array | ❌ | null | 分面配置(见下文) | +| `sort_by` | string | ❌ | null | 排序字段名(如 `min_price`, `max_price`, `title`) | +| `sort_order` | string | ❌ | "desc" | 排序方向:`asc`(升序)或 `desc`(降序) | +| `min_score` | float | ❌ | null | 最小相关性分数阈值 | +| `debug` | boolean | ❌ | false | 是否返回调试信息 | +| `user_id` | string | ❌ | null | 用户ID(用于个性化,预留) | +| `session_id` | string | ❌ | null | 会话ID(用于分析,预留) | + +### 过滤器详解 + +#### 1. 精确匹配过滤器 (filters) + +用于精确匹配或多值匹配(OR 逻辑)。 + +**格式**: +```json +{ + "filters": { + "category_keyword": "玩具", // 单值:精确匹配 + "vendor_keyword": ["乐高", "孩之宝"], // 数组:匹配任意值(OR) + "product_type_keyword": "益智玩具" // 单值:精确匹配 + } +} +``` + +**支持的值类型**: +- 字符串:精确匹配 +- 整数:精确匹配 +- 布尔值:精确匹配 +- 数组:匹配任意值(OR 逻辑) + +**常用过滤字段**: +- `category_keyword`: 类目 +- `vendor_keyword`: 品牌/供应商 +- `product_type_keyword`: 商品类型 +- `tags_keyword`: 标签 + +#### 2. 范围过滤器 (range_filters) + +用于数值字段的范围过滤。 + +**格式**: +```json +{ + "range_filters": { + "min_price": { + "gte": 50, // 大于等于 + "lte": 200 // 小于等于 + }, + "max_price": { + "gt": 100 // 大于 + }, + "create_time": { + "gte": "2024-01-01T00:00:00Z" // 日期时间字符串 + } + } +} +``` + +**支持的操作符**: +- `gte`: 大于等于 (>=) +- `gt`: 大于 (>) +- `lte`: 小于等于 (<=) +- `lt`: 小于 (<) + +**注意**: 至少需要指定一个操作符。 + +**常用范围字段**: +- `min_price`: 最低价格 +- `max_price`: 最高价格 +- `compare_at_price`: 原价 +- `create_time`: 创建时间 +- `update_time`: 更新时间 + +#### 3. 分面配置 (facets) + +用于生成分面统计(分组聚合),常用于构建筛选器UI。 + +**简单模式**(字符串数组): +```json +{ + "facets": ["category_keyword", "vendor_keyword"] +} +``` + +**高级模式**(配置对象数组): +```json +{ + "facets": [ + { + "field": "category_keyword", + "size": 15, + "type": "terms" + }, + { + "field": "min_price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + } + ] +} +``` + +**分面配置参数**: +- `field`: 字段名(必填) +- `size`: 返回的分组数量(默认:10,范围:1-100) +- `type`: 分面类型,`terms`(分组统计)或 `range`(范围统计) +- `ranges`: 范围定义(仅当 type='range' 时需要) + +### 布尔表达式语法 + +搜索查询支持布尔表达式,提供更灵活的搜索能力。 + +**支持的操作符**: + +| 操作符 | 描述 | 示例 | +|--------|------|------| +| `AND` | 所有词必须匹配 | `玩具 AND 乐高` | +| `OR` | 任意词匹配 | `芭比 OR 娃娃` | +| `ANDNOT` | 排除特定词 | `玩具 ANDNOT 电动` | +| `RANK` | 排序加权(不强制匹配) | `玩具 RANK 乐高` | +| `()` | 分组 | `玩具 AND (乐高 OR 芭比)` | + +**操作符优先级**(从高到低): +1. `()` - 括号 +2. `ANDNOT` - 排除 +3. `AND` - 与 +4. `OR` - 或 +5. `RANK` - 排序 + +**示例**: +``` +"芭比娃娃" // 简单查询 +"玩具 AND 乐高" // AND 查询 +"芭比 OR 娃娃" // OR 查询 +"玩具 ANDNOT 电动" // 排除查询 +"玩具 AND (乐高 OR 芭比)" // 复杂查询 +``` + +--- + +## 图片搜索接口 + +### 接口信息 + +- **端点**: `POST /search/image` +- **描述**: 基于图片相似度进行搜索,使用图片向量进行语义匹配 + +### 请求参数 + +```json +{ + "image_url": "string (required)", + "size": 10, + "filters": {}, + "range_filters": {} +} +``` + +### 参数说明 + +| 参数 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| `image_url` | string | ✅ | - | 查询图片的 URL | +| `size` | integer | ❌ | 10 | 返回结果数量(1-100) | +| `filters` | object | ❌ | null | 精确匹配过滤器 | +| `range_filters` | object | ❌ | null | 数值范围过滤器 | + +### 请求示例 + +```bash +curl -X POST "http://localhost:6002/search/image" \ + -H "Content-Type: application/json" \ + -d '{ + "image_url": "https://example.com/barbie.jpg", + "size": 20, + "filters": { + "category_keyword": "玩具" + }, + "range_filters": { + "min_price": { + "lte": 100 + } + } + }' +``` + +--- + +## 响应格式说明 + +### 标准响应结构 + +```json +{ + "results": [ + { + "product_id": "12345", + "title": "芭比时尚娃娃", + "handle": "barbie-doll", + "description": "高品质芭比娃娃", + "vendor": "美泰", + "product_type": "玩具", + "tags": "娃娃, 玩具, 女孩", + "price": 89.99, + "compare_at_price": 129.99, + "currency": "USD", + "image_url": "https://example.com/image.jpg", + "in_stock": true, + "variants": [ + { + "variant_id": "67890", + "title": "粉色款", + "price": 89.99, + "compare_at_price": 129.99, + "sku": "BARBIE-001", + "stock": 100, + "options": { + "option1": "粉色", + "option2": "标准款" + } + } + ], + "relevance_score": 8.5 + } + ], + "total": 118, + "max_score": 8.5, + "facets": [ + { + "field": "category_keyword", + "label": "category_keyword", + "type": "terms", + "values": [ + { + "value": "玩具", + "label": "玩具", + "count": 85, + "selected": false + } + ] + } + ], + "query_info": { + "original_query": "芭比娃娃", + "detected_language": "zh", + "translations": { + "en": "barbie doll" + } + }, + "suggestions": [], + "related_searches": [], + "took_ms": 45, + "performance_info": null, + "debug_info": null +} +``` + +### 响应字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `results` | array | 搜索结果列表(ProductResult对象数组) | +| `results[].product_id` | string | 商品ID | +| `results[].title` | string | 商品标题 | +| `results[].price` | float | 价格(min_price) | +| `results[].variants` | array | 变体列表(SKU列表) | +| `results[].relevance_score` | float | 相关性分数 | +| `total` | integer | 匹配的总文档数 | +| `max_score` | float | 最高相关性分数 | +| `facets` | array | 分面统计结果 | +| `query_info` | object | 查询处理信息 | +| `took_ms` | integer | 搜索耗时(毫秒) | + +### ProductResult字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `product_id` | string | 商品ID(SPU ID) | +| `title` | string | 商品标题 | +| `handle` | string | 商品URL handle | +| `description` | string | 商品描述 | +| `vendor` | string | 供应商/品牌 | +| `product_type` | string | 商品类型 | +| `tags` | string | 标签 | +| `price` | float | 价格(min_price) | +| `compare_at_price` | float | 原价 | +| `currency` | string | 货币单位(默认USD) | +| `image_url` | string | 主图URL | +| `in_stock` | boolean | 是否有库存(任意变体有库存即为true) | +| `variants` | array | 变体列表 | +| `relevance_score` | float | 相关性分数 | + +### VariantResult字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `variant_id` | string | 变体ID(SKU ID) | +| `title` | string | 变体标题 | +| `price` | float | 价格 | +| `compare_at_price` | float | 原价 | +| `sku` | string | SKU编码 | +| `stock` | integer | 库存数量 | +| `options` | object | 选项(颜色、尺寸等) | + +--- + +## 常见场景示例 + +### 场景1:商品列表页搜索 + +**需求**: 搜索"玩具",按价格从低到高排序,显示前20个结果 + +```json +{ + "query": "玩具", + "size": 20, + "from": 0, + "sort_by": "min_price", + "sort_order": "asc" +} +``` + +### 场景2:带筛选的商品搜索 + +**需求**: 搜索"玩具",筛选类目为"益智玩具",价格在50-200之间 + +```json +{ + "query": "玩具", + "size": 20, + "filters": { + "category_keyword": "益智玩具" + }, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + } + } +} +``` + +### 场景3:带分面的商品搜索 + +**需求**: 搜索"玩具",获取类目和品牌的分面统计,用于构建筛选器 + +```json +{ + "query": "玩具", + "size": 20, + "facets": [ + "category_keyword", + "vendor_keyword" + ] +} +``` + +### 场景4:多条件组合搜索 + +**需求**: 搜索"玩具",筛选多个品牌,价格范围,并获取分面统计 + +```json +{ + "query": "玩具", + "size": 20, + "filters": { + "vendor_keyword": ["乐高", "孩之宝", "美泰"] + }, + "range_filters": { + "min_price": { + "gte": 50, + "lte": 200 + } + }, + "facets": [ + { + "field": "category_keyword", + "size": 15 + }, + { + "field": "min_price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100}, + {"key": "100-200", "from": 100, "to": 200}, + {"key": "200+", "from": 200} + ] + } + ], + "sort_by": "min_price", + "sort_order": "asc" +} +``` + +### 场景5:布尔表达式搜索 + +**需求**: 搜索包含"玩具"和"乐高"的商品,排除"电动" + +```json +{ + "query": "玩具 AND 乐高 ANDNOT 电动", + "size": 20 +} +``` + +### 场景6:分页查询 + +**需求**: 获取第2页结果(每页20条) + +```json +{ + "query": "玩具", + "size": 20, + "from": 20 +} +``` + +--- + +## 错误处理 + +### 错误响应格式 + +```json +{ + "error": "错误信息", + "detail": "详细错误信息(可选)" +} +``` + +### 常见错误码 + +| HTTP状态码 | 说明 | 处理建议 | +|-----------|------|---------| +| 200 | 成功 | - | +| 400 | 请求参数错误 | 检查请求参数格式和必填字段 | +| 404 | 接口不存在 | 检查接口路径 | +| 500 | 服务器内部错误 | 联系技术支持 | + +### 错误处理示例 + +**Python**: +```python +import requests + +try: + response = requests.post(url, json=payload, timeout=10) + response.raise_for_status() + data = response.json() +except requests.exceptions.HTTPError as e: + print(f"HTTP错误: {e}") + if response.status_code == 400: + error_data = response.json() + print(f"错误详情: {error_data.get('detail')}") +except requests.exceptions.RequestException as e: + print(f"请求异常: {e}") +``` + +**JavaScript**: +```javascript +try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || `HTTP ${response.status}`); + } + + const data = await response.json(); +} catch (error) { + console.error('搜索失败:', error.message); +} +``` + +--- + +### 5. 代码示例 + +**完整的搜索函数(Python)**: + +```python +import requests +from typing import Dict, Any, Optional, List + +class SearchClient: + def __init__(self, base_url: str = "http://localhost:6002"): + self.base_url = base_url + self.timeout = 10 + + def search( + self, + query: str, + size: int = 20, + from_: int = 0, + filters: Optional[Dict] = None, + range_filters: Optional[Dict] = None, + facets: Optional[List] = None, + sort_by: Optional[str] = None, + sort_order: str = "desc" + ) -> Dict[str, Any]: + """ + 执行搜索查询 + + Args: + query: 搜索查询字符串 + size: 返回结果数量 + from_: 分页偏移量 + filters: 精确匹配过滤器 + range_filters: 范围过滤器 + facets: 分面配置 + sort_by: 排序字段 + sort_order: 排序方向 + + Returns: + 搜索结果字典 + """ + url = f"{self.base_url}/search/" + payload = { + "query": query, + "size": size, + "from": from_, + } + + if filters: + payload["filters"] = filters + if range_filters: + payload["range_filters"] = range_filters + if facets: + payload["facets"] = facets + if sort_by: + payload["sort_by"] = sort_by + payload["sort_order"] = sort_order + + try: + response = requests.post( + url, + json=payload, + timeout=self.timeout + ) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"搜索请求失败: {e}") + +# 使用示例 +client = SearchClient() +result = client.search( + query="玩具", + size=20, + filters={"category_keyword": "益智玩具"}, + range_filters={"min_price": {"gte": 50, "lte": 200}}, + facets=["category_keyword", "vendor_keyword"], + sort_by="min_price", + sort_order="asc" +) + +print(f"找到 {result['total']} 个结果") +for product in result['results']: + print(f"{product['title']} - ¥{product['price']}") +``` + +**完整的搜索函数(JavaScript)**: + +```javascript +class SearchClient { + constructor(baseUrl = 'http://localhost:6002') { + this.baseUrl = baseUrl; + this.timeout = 10000; + } + + async search({ + query, + size = 20, + from = 0, + filters = null, + rangeFilters = null, + facets = null, + sortBy = null, + sortOrder = 'desc' + }) { + const url = `${this.baseUrl}/search/`; + const payload = { + query, + size, + from, + }; + + if (filters) payload.filters = filters; + if (rangeFilters) payload.range_filters = rangeFilters; + if (facets) payload.facets = facets; + if (sortBy) { + payload.sort_by = sortBy; + payload.sort_order = sortOrder; + } + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(this.timeout) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || `HTTP ${response.status}`); + } + + return await response.json(); + } catch (error) { + throw new Error(`搜索请求失败: ${error.message}`); + } + } +} + +// 使用示例 +const client = new SearchClient(); +const result = await client.search({ + query: '玩具', + size: 20, + filters: { category_keyword: '益智玩具' }, + rangeFilters: { min_price: { gte: 50, lte: 200 } }, + facets: ['category_keyword', 'vendor_keyword'], + sortBy: 'min_price', + sortOrder: 'asc' +}); + +console.log(`找到 ${result.total} 个结果`); +result.results.forEach(product => { + console.log(`${product.title} - ¥${product.price}`); +}); +``` + +--- + +## 其他接口 + +### 搜索建议(框架) + +- **端点**: `GET /search/suggestions` +- **描述**: 返回搜索建议(自动补全/热词)。当前为框架实现,接口和响应格式已经固定,可平滑扩展。 + +#### 查询参数 + +| 参数 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| `q` | string | ✅ | - | 查询字符串(至少 1 个字符) | +| `size` | integer | ❌ | 5 | 返回建议数量(1-20) | +| `types` | string | ❌ | `query` | 建议类型(逗号分隔):`query`, `product`, `category`, `brand` | + +#### 响应示例 + +```json +{ + "query": "芭", + "suggestions": [ + { + "text": "芭比娃娃", + "type": "query", + "highlight": "比娃娃", + "popularity": 850 + } + ], + "took_ms": 5 +} +``` + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/suggestions?q=芭&size=5&types=query,product" +``` + +--- + +### 即时搜索(框架) + +- **端点**: `GET /search/instant` +- **描述**: 边输入边搜索,采用轻量参数响应当前输入。底层复用标准搜索能力。 + +#### 查询参数 + +| 参数 | 类型 | 必填 | 默认值 | 描述 | +|------|------|------|--------|------| +| `q` | string | ✅ | - | 搜索查询(至少 2 个字符) | +| `size` | integer | ❌ | 5 | 返回结果数量(1-20) | + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/instant?q=玩具&size=5" +``` + +--- + +### 获取单个文档 + +- **端点**: `GET /search/{doc_id}` +- **描述**: 根据文档 ID 获取单个商品详情,用于点击结果后的详情页或排查问题。 + +#### 路径参数 + +| 参数 | 类型 | 描述 | +|------|------|------| +| `doc_id` | string | 商品或文档 ID | + +#### 响应示例 + +```json +{ + "id": "12345", + "source": { + "title": "芭比时尚娃娃", + "min_price": 89.99, + "category_keyword": "玩具" + } +} +``` + +#### 请求示例 + +```bash +curl "http://localhost:6002/search/12345" +``` + +--- + +## 管理接口 + +### 健康检查 + +- **端点**: `GET /admin/health` +- **描述**: 检查服务与依赖(如 Elasticsearch)状态。 + +```json +{ + "status": "healthy", + "elasticsearch": "connected", + "tenant_id": "tenant1" +} +``` + +--- + +### 获取配置 + +- **端点**: `GET /admin/config` +- **描述**: 返回当前租户的脱敏配置,便于核对索引及排序表达式。 + +```json +{ + "tenant_id": "tenant1", + "tenant_name": "Tenant1 Test Instance", + "es_index_name": "search_tenant1", + "num_fields": 20, + "num_indexes": 4, + "supported_languages": ["zh", "en", "ru"], + "ranking_expression": "bm25() + 0.2*text_embedding_relevance()", + "spu_enabled": false +} +``` + +--- + +### 索引统计 + +- **端点**: `GET /admin/stats` +- **描述**: 获取索引文档数量与磁盘大小,方便监控。 + +```json +{ + "index_name": "search_tenant1", + "document_count": 10000, + "size_mb": 523.45 +} +``` + +--- + +## 数据模型 + +### 商品字段 + +| 字段名 | 类型 | 描述 | +|--------|------|------| +| `product_id` | keyword | 商品 ID(SPU) | +| `sku_id` | keyword/long | SKU ID(主键) | +| `title` | text | 商品名称(中文) | +| `en_title` | text | 商品名称(英文) | +| `ru_title` | text | 商品名称(俄文) | +| `category_keyword` | keyword | 类目(精确匹配) | +| `vendor_keyword` | keyword | 品牌/供应商(精确匹配) | +| `product_type_keyword` | keyword | 商品类型 | +| `tags_keyword` | keyword | 标签 | +| `min_price` | double | 最低价格 | +| `max_price` | double | 最高价格 | +| `compare_at_price` | double | 原价 | +| `create_time` | date | 创建时间 | +| `update_time` | date | 更新时间 | +| `in_stock` | boolean | 是否有库存 | +| `text_embedding` | dense_vector | 文本向量(1024 维) | +| `image_embedding` | dense_vector | 图片向量(1024 维) | + +> 不同租户可自定义字段名称,但最佳实践是对可过滤字段建立 `*_keyword` 版本,对可排序字段显式建 keyword/数值映射。 + +--- + +## 常见问题(FAQ) + +**Q1: 如何判断一个字段应该用哪种过滤器?** +`filters` 针对 keyword/布尔/整数字段做精确匹配;`range_filters` 针对数值或日期字段做区间查询。 + +**Q2: 可以同时使用多个过滤器吗?** +可以,所有过滤条件为 AND 关系。例如: + +```json +{ + "filters": { + "category_keyword": "玩具", + "vendor_keyword": "乐高" + }, + "range_filters": { + "min_price": {"gte": 50, "lte": 200} + } +} +``` + +**Q4: 分面结果里的 `selected` 字段含义是什么?** +指示该分面值是否已在当前过滤条件中,前端可据此高亮。 + +**Q5: 如何自定义排序?** +设置 `sort_by` 和 `sort_order`,常用字段包括 `min_price`, `max_price`, `title`, `create_time`, `update_time`, `relevance_score`。 + +**Q6: 如何启用调试模式?** +添加 `debug: true`,即可在响应中看到 `debug_info`(ES DSL、阶段耗时、打分细节)。 + +--- + +## 附录 + +### 常用字段列表 + +#### 过滤字段(使用 `*_keyword` 后缀) + +- `category_keyword`: 类目 +- `vendor_keyword`: 品牌/供应商 +- `product_type_keyword`: 商品类型 +- `tags_keyword`: 标签 + +#### 范围字段 + +- `min_price`: 最低价格 +- `max_price`: 最高价格 +- `compare_at_price`: 原价 +- `create_time`: 创建时间 +- `update_time`: 更新时间 + +#### 排序字段 + +- `min_price`: 最低价格 +- `max_price`: 最高价格 +- `title`: 标题(字母序) +- `create_time`: 创建时间 +- `update_time`: 更新时间 +- `relevance_score`: 相关性分数(默认) + +### 支持的分析器 + +| 分析器 | 语言 | 描述 | +|--------|------|------| +| `chinese_ecommerce` | 中文 | 基于 Ansj 的电商优化中文分析器 | +| `english` | 英文 | 标准英文分析器 | +| `russian` | 俄文 | 俄文分析器 | +| `arabic` | 阿拉伯文 | 阿拉伯文分析器 | +| `spanish` | 西班牙文 | 西班牙文分析器 | +| `japanese` | 日文 | 日文分析器 | + +### 字段类型速查 + +| 类型 | ES Mapping | 用途 | +|------|------------|------| +| `TEXT` | `text` | 全文检索 | +| `KEYWORD` | `keyword` | 精确匹配、聚合、排序 | +| `LONG` | `long` | 整数 | +| `DOUBLE` | `double` | 浮点数 | +| `DATE` | `date` | 日期时间 | +| `BOOLEAN` | `boolean` | 布尔值 | +| `TEXT_EMBEDDING` | `dense_vector` | 文本语义向量 | +| `IMAGE_EMBEDDING` | `dense_vector` | 图片语义向量 | + diff --git a/docs/搜索API速查表.md b/docs/搜索API速查表.md new file mode 100644 index 0000000..ba9f8d5 --- /dev/null +++ b/docs/搜索API速查表.md @@ -0,0 +1,233 @@ +# API 快速参考 (v3.0) + +## 基础搜索 + +```bash +POST /search/ +{ + "query": "芭比娃娃", + "size": 20 +} +``` + +--- + +## 精确匹配过滤 + +```bash +{ + "filters": { + "categoryName_keyword": "玩具", // 单值 + "brandName_keyword": ["乐高", "美泰"] // 多值(OR) + } +} +``` + +--- + +## 范围过滤 + +```bash +{ + "range_filters": { + "price": { + "gte": 50, // >= + "lte": 200 // <= + } + } +} +``` + +**操作符**: `gte` (>=), `gt` (>), `lte` (<=), `lt` (<) + +--- + +## 分面搜索 + +### 简单模式 + +```bash +{ + "facets": ["categoryName_keyword", "brandName_keyword"] +} +``` + +### 高级模式 + +```bash +{ + "facets": [ + {"field": "categoryName_keyword", "size": 15}, + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100} + ] + } + ] +} +``` + +--- + +## 排序 + +```bash +{ + "sort_by": "min_price", + "sort_order": "asc" // asc 或 desc +} +``` + +--- + +## 布尔表达式 + +```bash +{ + "query": "玩具 AND (乐高 OR 芭比) ANDNOT 电动" +} +``` + +**操作符优先级**: `()` > `ANDNOT` > `AND` > `OR` > `RANK` + +--- + +## 分页 + +```bash +{ + "size": 20, // 每页数量 + "from": 0 // 偏移量(第1页=0,第2页=20) +} +``` + +--- + +## 完整示例 + +```bash +POST /search/ +{ + "query": "玩具", + "size": 20, + "from": 0, + "filters": { + "categoryName_keyword": ["玩具", "益智玩具"] + }, + "range_filters": { + "price": {"gte": 50, "lte": 200} + }, + "facets": [ + {"field": "brandName_keyword", "size": 15}, + {"field": "categoryName_keyword", "size": 15} + ], + "sort_by": "min_price", + "sort_order": "asc" +} +``` + +--- + +## 响应格式 + +```json +{ + "hits": [ + { + "_id": "12345", + "_score": 8.5, + "_source": {...} + } + ], + "total": 118, + "max_score": 8.5, + "took_ms": 45, + "facets": [ + { + "field": "categoryName_keyword", + "label": "商品类目", + "type": "terms", + "values": [ + { + "value": "玩具", + "label": "玩具", + "count": 85, + "selected": false + } + ] + } + ] +} +``` + +--- + +## 其他端点 + +```bash +POST /search/image +{ + "image_url": "https://example.com/image.jpg", + "size": 20 +} + +GET /search/suggestions?q=芭&size=5 + +GET /search/instant?q=玩具&size=5 + +GET /search/{doc_id} + +GET /admin/health +GET /admin/config +GET /admin/stats +``` + +--- + +## Python 快速示例 + +```python +import requests + +result = requests.post('http://localhost:6002/search/', json={ + "query": "玩具", + "filters": {"categoryName_keyword": "玩具"}, + "range_filters": {"price": {"gte": 50, "lte": 200}}, + "facets": ["brandName_keyword"], + "sort_by": "min_price", + "sort_order": "asc" +}).json() + +print(f"找到 {result['total']} 个结果") +``` + +--- + +## JavaScript 快速示例 + +```javascript +const result = await fetch('http://localhost:6002/search/', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + query: "玩具", + filters: {categoryName_keyword: "玩具"}, + range_filters: {price: {gte: 50, lte: 200}}, + facets: ["brandName_keyword"], + sort_by: "min_price", + sort_order: "asc" + }) +}).then(r => r.json()); + +console.log(`找到 ${result.total} 个结果`); +``` + +--- + +**详细文档**: [API_DOCUMENTATION.md](API_DOCUMENTATION.md) +**更多示例**: [API_EXAMPLES.md](API_EXAMPLES.md) +**在线文档**: http://localhost:6002/docs + diff --git a/docs/测试Pipeline说明.md b/docs/测试Pipeline说明.md new file mode 100644 index 0000000..06ca2c1 --- /dev/null +++ b/docs/测试Pipeline说明.md @@ -0,0 +1,459 @@ +# 搜索引擎测试流水线指南 + +## 概述 + +本文档介绍了搜索引擎项目的完整测试流水线,包括测试环境搭建、测试执行、结果分析等内容。测试流水线设计用于commit前的自动化质量保证。 + +## 🏗️ 测试架构 + +### 测试层次 + +``` +测试流水线 +├── 代码质量检查 (Code Quality) +│ ├── 代码格式化检查 (Black, isort) +│ ├── 静态分析 (Flake8, MyPy, Pylint) +│ └── 安全扫描 (Safety, Bandit) +│ +├── 单元测试 (Unit Tests) +│ ├── RequestContext测试 +│ ├── Searcher测试 +│ ├── QueryParser测试 +│ └── BooleanParser测试 +│ +├── 集成测试 (Integration Tests) +│ ├── 端到端搜索流程测试 +│ ├── 多组件协同测试 +│ └── 错误处理测试 +│ +├── API测试 (API Tests) +│ ├── REST API接口测试 +│ ├── 参数验证测试 +│ ├── 并发请求测试 +│ └── 错误响应测试 +│ +└── 性能测试 (Performance Tests) + ├── 响应时间测试 + ├── 并发性能测试 + └── 资源使用测试 +``` + +### 核心组件 + +1. **RequestContext**: 请求级别的上下文管理器,用于跟踪测试过程中的所有数据 +2. **测试环境管理**: 自动化启动/停止测试依赖服务 +3. **测试执行引擎**: 统一的测试运行和结果收集 +4. **报告生成系统**: 多格式的测试报告生成 + +## 🚀 快速开始 + +### 本地测试环境 + +1. **启动测试环境** + ```bash + # 启动所有必要的测试服务 + ./scripts/start_test_environment.sh + ``` + +2. **运行完整测试套件** + ```bash + # 运行所有测试 + python scripts/run_tests.py + + # 或者使用pytest直接运行 + pytest tests/ -v + ``` + +3. **停止测试环境** + ```bash + ./scripts/stop_test_environment.sh + ``` + +### CI/CD测试 + +1. **GitHub Actions** + - Push到主分支自动触发 + - Pull Request自动运行 + - 手动触发支持 + +2. **测试报告** + - 自动生成并上传 + - PR评论显示测试摘要 + - 详细报告下载 + +## 📋 测试类型详解 + +### 1. 单元测试 (Unit Tests) + +**位置**: `tests/unit/` + +**目的**: 测试单个函数、类、模块的功能 + +**覆盖范围**: +- `test_context.py`: RequestContext功能测试 +- `test_searcher.py`: Searcher核心功能测试 +- `test_query_parser.py`: QueryParser处理逻辑测试 + +**运行方式**: +```bash +# 运行所有单元测试 +pytest tests/unit/ -v + +# 运行特定测试 +pytest tests/unit/test_context.py -v + +# 生成覆盖率报告 +pytest tests/unit/ --cov=. --cov-report=html +``` + +### 2. 集成测试 (Integration Tests) + +**位置**: `tests/integration/` + +**目的**: 测试多个组件协同工作的功能 + +**覆盖范围**: +- `test_search_integration.py`: 完整搜索流程集成 +- 数据库、ES、搜索器集成测试 +- 错误传播和处理测试 + +**运行方式**: +```bash +# 运行集成测试(需要启动测试环境) +pytest tests/integration/ -v -m "not slow" + +# 运行包含慢速测试的集成测试 +pytest tests/integration/ -v +``` + +### 3. API测试 (API Tests) + +**位置**: `tests/integration/test_api_integration.py` + +**目的**: 测试HTTP API接口的功能和性能 + +**覆盖范围**: +- 基本搜索API +- 参数验证 +- 错误处理 +- 并发请求 +- Unicode支持 + +**运行方式**: +```bash +# 运行API测试 +pytest tests/integration/test_api_integration.py -v +``` + +### 4. 性能测试 (Performance Tests) + +**目的**: 验证系统性能指标 + +**测试内容**: +- 搜索响应时间 +- API并发处理能力 +- 资源使用情况 + +**运行方式**: +```bash +# 运行性能测试 +python scripts/run_performance_tests.py +``` + +## 🛠️ 环境配置 + +### 测试环境要求 + +1. **Python环境** + ```bash + # 创建测试环境 + conda create -n searchengine-test python=3.9 + conda activate searchengine-test + + # 安装依赖 + pip install -r requirements.txt + pip install pytest pytest-cov pytest-json-report + ``` + +2. **Elasticsearch** + ```bash + # 使用Docker启动ES + docker run -d \ + --name elasticsearch \ + -p 9200:9200 \ + -e "discovery.type=single-node" \ + -e "xpack.security.enabled=false" \ + elasticsearch:8.8.0 + ``` + +3. **环境变量** + ```bash + export ES_HOST="http://localhost:9200" + export ES_USERNAME="elastic" + export ES_PASSWORD="changeme" + export API_HOST="127.0.0.1" + export API_PORT="6003" + export TENANT_ID="test_tenant" + export TESTING_MODE="true" + ``` + +### 服务依赖 + +测试环境需要以下服务: + +1. **Elasticsearch** (端口9200) + - 存储和搜索测试数据 + - 支持中文和英文索引 + +2. **API服务** (端口6003) + - FastAPI测试服务 + - 提供搜索接口 + +3. **测试数据库** + - 预配置的测试索引 + - 包含测试数据 + +## 📊 测试报告 + +### 报告类型 + +1. **实时控制台输出** + - 测试进度显示 + - 失败详情 + - 性能摘要 + +2. **JSON格式报告** + ```json + { + "timestamp": "2024-01-01T10:00:00", + "summary": { + "total_tests": 150, + "passed": 148, + "failed": 2, + "success_rate": 98.7 + }, + "suites": { ... } + } + ``` + +3. **文本格式报告** + - 人类友好的格式 + - 包含测试摘要和详情 + - 适合PR评论 + +4. **HTML覆盖率报告** + - 代码覆盖率可视化 + - 分支和行覆盖率 + - 缺失测试高亮 + +### 报告位置 + +``` +test_logs/ +├── unit_test_results.json # 单元测试结果 +├── integration_test_results.json # 集成测试结果 +├── api_test_results.json # API测试结果 +├── test_report_20240101_100000.txt # 文本格式摘要 +├── test_report_20240101_100000.json # JSON格式详情 +└── htmlcov/ # HTML覆盖率报告 +``` + +## 🔄 CI/CD集成 + +### GitHub Actions工作流 + +**触发条件**: +- Push到主分支 +- Pull Request创建/更新 +- 手动触发 + +**工作流阶段**: + +1. **代码质量检查** + - 代码格式验证 + - 静态代码分析 + - 安全漏洞扫描 + +2. **单元测试** + - 多Python版本矩阵测试 + - 代码覆盖率收集 + - 自动上传到Codecov + +3. **集成测试** + - 服务依赖启动 + - 端到端功能测试 + - 错误处理验证 + +4. **API测试** + - 接口功能验证 + - 参数校验测试 + - 并发请求测试 + +5. **性能测试** + - 响应时间检查 + - 资源使用监控 + - 性能回归检测 + +6. **测试报告生成** + - 结果汇总 + - 报告上传 + - PR评论更新 + +### 工作流配置 + +**文件**: `.github/workflows/test.yml` + +**关键特性**: +- 并行执行提高效率 +- 服务容器化隔离 +- 自动清理资源 +- 智能缓存依赖 + +## 🧪 测试最佳实践 + +### 1. 测试编写原则 + +- **独立性**: 每个测试应该独立运行 +- **可重复性**: 测试结果应该一致 +- **快速执行**: 单元测试应该快速完成 +- **清晰命名**: 测试名称应该描述测试内容 + +### 2. 测试数据管理 + +```python +# 使用fixture提供测试数据 +@pytest.fixture +def sample_tenant_config(): + return TenantConfig( + tenant_id="test_tenant", + es_index_name="test_products" + ) + +# 使用mock避免外部依赖 +@patch('search.searcher.ESClient') +def test_search_with_mock_es(mock_es_client, test_searcher): + mock_es_client.search.return_value = mock_response + result = test_searcher.search("test query") + assert result is not None +``` + +### 3. RequestContext集成 + +```python +def test_with_context(test_searcher): + context = create_request_context("test-req", "test-user") + + result = test_searcher.search("test query", context=context) + + # 验证context被正确更新 + assert context.query_analysis.original_query == "test query" + assert context.get_stage_duration("elasticsearch_search") > 0 +``` + +### 4. 性能测试指南 + +```python +def test_search_performance(client): + start_time = time.time() + response = client.get("/search", params={"q": "test query"}) + response_time = (time.time() - start_time) * 1000 + + assert response.status_code == 200 + assert response_time < 2000 # 2秒内响应 +``` + +## 🚨 故障排除 + +### 常见问题 + +1. **Elasticsearch连接失败** + ```bash + # 检查ES状态 + curl http://localhost:9200/_cluster/health + + # 重启ES服务 + docker restart elasticsearch + ``` + +2. **测试端口冲突** + ```bash + # 检查端口占用 + lsof -i :6003 + + # 修改API端口 + export API_PORT="6004" + ``` + +3. **依赖包缺失** + ```bash + # 重新安装依赖 + pip install -r requirements.txt + pip install pytest pytest-cov pytest-json-report + ``` + +4. **测试数据问题** + ```bash + # 重新创建测试索引 + curl -X DELETE http://localhost:9200/test_products + ./scripts/start_test_environment.sh + ``` + +### 调试技巧 + +1. **详细日志输出** + ```bash + pytest tests/unit/test_context.py -v -s --tb=long + ``` + +2. **运行单个测试** + ```bash + pytest tests/unit/test_context.py::TestRequestContext::test_create_context -v + ``` + +3. **调试模式** + ```python + import pdb; pdb.set_trace() + ``` + +4. **性能分析** + ```bash + pytest --profile tests/ + ``` + +## 📈 持续改进 + +### 测试覆盖率目标 + +- **单元测试**: > 90% +- **集成测试**: > 80% +- **API测试**: > 95% + +### 性能基准 + +- **搜索响应时间**: < 2秒 +- **API并发处理**: 100 QPS +- **系统资源使用**: < 80% CPU, < 4GB RAM + +### 质量门禁 + +- **所有测试必须通过** +- **代码覆盖率不能下降** +- **性能不能显著退化** +- **不能有安全漏洞** + +## 📚 相关文档 + +- [RequestContext使用指南](RequestContext_README.md) +- [API文档](../api/README.md) +- [配置指南](../config/README.md) +- [部署指南](Deployment_README.md) + +## 🤝 贡献指南 + +1. 为新功能编写对应的测试 +2. 确保测试覆盖率不下降 +3. 遵循测试命名约定 +4. 更新相关文档 +5. 运行完整测试套件后提交 + +通过这套完整的测试流水线,我们可以确保搜索引擎代码的质量、性能和可靠性,为用户提供稳定高效的搜索服务。 \ No newline at end of file diff --git a/docs/测试数据指南.md b/docs/测试数据指南.md new file mode 100644 index 0000000..2da4375 --- /dev/null +++ b/docs/测试数据指南.md @@ -0,0 +1,527 @@ +# 测试数据构造指南 - SearchEngine + +本文档说明如何构造测试数据,包括两种数据源的准备和导入流程。 + +--- + +## 快速开始 + +### 1. 构造 Mock 数据(tenant_id=1 和 tenant_id=2) + +```bash +./scripts/mock_data.sh +``` + +功能:自动生成 tenant_id=1 的Mock数据,并从CSV导入 tenant_id=2 的数据到MySQL + +--- + +### 2. 从 MySQL → Elasticsearch + +```bash +# 导入 tenant_id=1 的数据(重建索引) +./scripts/ingest.sh 1 true + +# 导入 tenant_id=2 的数据(重建索引) +./scripts/ingest.sh 2 true +``` + + +**用法**:`./scripts/ingest.sh [recreate_index]` +- `tenant_id`: 租户ID(1 或 2) +- `recreate_index`: 是否重建索引(`true`/`false`,默认:`false`) + +--- + +## 完整工作流程 + +```bash +# 1. 构造并导入测试数据到MySQL +./scripts/mock_data.sh + +# 2. 导入 tenant_id=1 的数据到ES +./scripts/ingest.sh 1 true + +# 3. 导入 tenant_id=2 的数据到ES +./scripts/ingest.sh 2 true +``` + +--- + +## 目录 + +1. [数据说明](#数据说明) +2. [构造Mock数据(tenant_id=1)](#构造mock数据tenant_id1) +3. [从CSV导入数据(tenant_id=2)](#从csv导入数据tenant_id2) +4. [从MySQL导入到Elasticsearch](#从mysql导入到elasticsearch) +5. [完整工作流程](#完整工作流程) +6. [常见问题](#常见问题) + +--- + +## 数据说明 + +系统支持两种测试数据源: + +1. **Tenant ID = 1**: 自动生成的Mock数据(使用 `generate_test_data.py` 生成) +2. **Tenant ID = 2**: 从CSV文件导入的真实数据(使用 `import_tenant2_csv.py` 导入) + +### 数据表结构 + +系统使用店匠标准表结构: + +- **SPU表**: `shoplazza_product_spu` - 商品SPU数据 +- **SKU表**: `shoplazza_product_sku` - 商品SKU数据 + +表结构详见 `INDEX_FIELDS_DOCUMENTATION.md`。 + +--- + +## 构造Mock数据(tenant_id=1) + +### 使用一键脚本(推荐) + +`mock_data.sh` 脚本会自动生成并导入 tenant_id=1 的Mock数据: + +```bash +cd /home/tw/SearchEngine +./scripts/mock_data.sh +``` + +脚本会自动: +- 生成 1000 个SPU的Mock数据 +- 导入数据到MySQL +- 自动计算起始ID,避免主键冲突 + +### 手动分步执行 + +如果需要自定义参数,可以分步执行: + +#### 步骤1: 生成Mock测试数据 + +```bash +python scripts/generate_test_data.py \ + --num-spus 1000 \ + --tenant-id "1" \ + --output test_data_tenant1.sql \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> +``` + +参数说明: +- `--num-spus`: 生成的SPU数量(默认:1000) +- `--tenant-id`: 租户ID(默认:1) +- `--output`: 输出的SQL文件路径 +- `--db-host`, `--db-port`, `--db-database`, `--db-username`, `--db-password`: 数据库连接信息 + +#### 步骤2: 导入数据到MySQL + +```bash +python scripts/import_test_data.py \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> \ + --sql-file test_data_tenant1.sql \ + --tenant-id "1" +``` + +参数说明: +- `--sql-file`: SQL文件路径 +- `--tenant-id`: 租户ID(用于清理旧数据) +- 其他参数:数据库连接信息 + +**注意**: 导入会先清理该 tenant_id 的旧数据,再导入新数据。 + +--- + +## 从CSV导入数据(tenant_id=2) + +### 使用一键脚本(推荐) + +`mock_data.sh` 脚本会自动从CSV文件导入 tenant_id=2 的数据: + +```bash +cd /home/tw/SearchEngine +./scripts/mock_data.sh +``` + +**前提条件**: 确保CSV文件存在于以下路径: +``` +data/customer1/goods_with_pic.5years_congku.csv.shuf.1w +``` + +如果CSV文件路径不同,需要修改 `scripts/mock_data.sh` 中的 `TENANT2_CSV_FILE` 变量。 + +### CSV文件格式要求 + +CSV文件需要包含以下列(列名不区分大小写): + +- `skuId` - SKU ID +- `name` - 商品名称 +- `name_pinyin` - 拼音(可选) +- `create_time` - 创建时间(格式:YYYY-MM-DD HH:MM:SS) +- `ruSkuName` - 俄文SKU名称(可选) +- `enSpuName` - 英文SPU名称(可选) +- `categoryName` - 类别名称 +- `supplierName` - 供应商名称 +- `brandName` - 品牌名称 +- `file_id` - 文件ID(可选) +- `days_since_last_update` - 更新天数(可选) +- `id` - 商品ID(可选) +- `imageUrl` - 图片URL(可选) + +### 手动分步执行 + +如果需要自定义参数,可以分步执行: + +#### 步骤1: 从CSV生成SQL文件 + +```bash +python scripts/import_tenant2_csv.py \ + --csv-file data/customer1/goods_with_pic.5years_congku.csv.shuf.1w \ + --tenant-id "2" \ + --output customer1_data.sql \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> +``` + +参数说明: +- `--csv-file`: CSV文件路径 +- `--tenant-id`: 租户ID(默认:2) +- `--output`: 输出的SQL文件路径 +- 其他参数:数据库连接信息 + +#### 步骤2: 导入数据到MySQL + +```bash +python scripts/import_test_data.py \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> \ + --sql-file customer1_data.sql \ + --tenant-id "2" +``` + +**注意**: +- CSV导入会先清理该 tenant_id 的旧数据,再导入新数据 +- 脚本会自动计算起始ID,避免主键冲突 + +--- + +## 从MySQL导入到Elasticsearch + +数据导入到MySQL后,需要使用 `ingest.sh` 脚本将数据从MySQL导入到Elasticsearch。 + +### 基本用法 + +```bash +./scripts/ingest.sh [recreate_index] +``` + +参数说明: +- `tenant_id`: **必需**,租户ID,用于筛选数据库中的数据 +- `recreate_index`: 可选,是否删除并重建索引(true/false,默认:false) + +### 使用示例 + +#### 重建索引并导入数据(推荐首次导入) + +```bash +# 导入tenant_id=1的数据并重建索引 +./scripts/ingest.sh 1 true + +# 导入tenant_id=2的数据并重建索引 +./scripts/ingest.sh 2 true +``` + +#### 增量导入(不重建索引) + +```bash +# 增量导入tenant_id=1的数据 +./scripts/ingest.sh 1 false + +# 增量导入tenant_id=2的数据 +./scripts/ingest.sh 2 false +``` + +### 手动执行 + +如果需要自定义参数,可以手动执行: + +```bash +python scripts/ingest_shoplazza.py \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> \ + --tenant-id 1 \ + --es-host http://localhost:9200 \ + --recreate \ + --batch-size 500 +``` + +参数说明: +- `--db-host`, `--db-port`, `--db-database`, `--db-username`, `--db-password`: MySQL连接信息 +- `--tenant-id`: 租户ID(必需) +- `--es-host`: Elasticsearch地址 +- `--recreate`: 是否重建索引 +- `--batch-size`: 批量处理大小(默认:500) + +### 检查可用的 tenant_id + +如果导入时显示 "No documents to index",脚本会自动显示调试信息,包括: +- 该 tenant_id 的统计信息(总数、活跃数、已删除数) +- 数据库中存在的其他 tenant_id 列表 + +也可以直接查询数据库: + +```sql +-- 查看有哪些 tenant_id +SELECT tenant_id, COUNT(*) as count, + SUM(CASE WHEN deleted = 0 THEN 1 ELSE 0 END) as active +FROM shoplazza_product_spu +GROUP BY tenant_id; + +-- 检查特定 tenant_id 的数据 +SELECT COUNT(*) FROM shoplazza_product_spu +WHERE tenant_id = 2 AND deleted = 0; +``` + +**注意**: +- 只有 `deleted=0` 的记录会被导入 +- 首次运行会下载模型文件(BGE-M3和CN-CLIP),大约需要10-30分钟 +- 确保MySQL中存在对应 tenant_id 的数据 + +--- + +## 完整工作流程 + +### 完整示例:构造并导入所有测试数据 + +```bash +# 1. 构造并导入 tenant_id=1 的Mock数据到MySQL +./scripts/mock_data.sh + +# 脚本会自动完成: +# - 生成 tenant_id=1 的Mock数据(1000个SPU) +# - 从CSV导入 tenant_id=2 的数据 +# - 导入数据到MySQL + +# 2. 从MySQL导入 tenant_id=1 的数据到ES +./scripts/ingest.sh 1 true + +# 3. 从MySQL导入 tenant_id=2 的数据到ES +./scripts/ingest.sh 2 true + +# 4. 验证数据导入 +curl http://localhost:9200/search_products/_count +``` + +### 分步执行示例 + +如果需要更细粒度的控制,可以分步执行: + +```bash +# ===== Part 1: 构造 tenant_id=1 的Mock数据 ===== + +# 1.1 生成Mock数据 +python scripts/generate_test_data.py \ + --num-spus 1000 \ + --tenant-id "1" \ + --output test_data_tenant1.sql \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> + +# 1.2 导入到MySQL +python scripts/import_test_data.py \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> \ + --sql-file test_data_tenant1.sql \ + --tenant-id "1" + +# ===== Part 2: 从CSV导入 tenant_id=2 的数据 ===== + +# 2.1 从CSV生成SQL +python scripts/import_tenant2_csv.py \ + --csv-file data/customer1/goods_with_pic.5years_congku.csv.shuf.1w \ + --tenant-id "2" \ + --output customer1_data.sql \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> + +# 2.2 导入到MySQL +python scripts/import_test_data.py \ + --db-host 120.79.247.228 \ + --db-port 3316 \ + --db-database saas \ + --db-username saas \ + --db-password <密码> \ + --sql-file customer1_data.sql \ + --tenant-id "2" + +# ===== Part 3: 从MySQL导入到ES ===== + +# 3.1 导入 tenant_id=1 的数据到ES +./scripts/ingest.sh 1 true + +# 3.2 导入 tenant_id=2 的数据到ES +./scripts/ingest.sh 2 true + +# ===== Part 4: 验证 ===== + +# 4.1 检查ES中的数据量 +curl http://localhost:9200/search_products/_count + +# 4.2 测试搜索 +curl -X POST http://localhost:6002/search/ \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: 1" \ + -d '{"query": "玩具", "size": 10}' +``` + +--- + +## 常见问题 + +### Q1: 数据导入失败 + +**症状**: `Error during data ingestion` + +**解决方案**: +```bash +# 检查MySQL数据是否存在 +mysql -h 120.79.247.228 -P 3316 -u saas -p saas -e \ + "SELECT COUNT(*) FROM shoplazza_product_spu WHERE tenant_id=1" + +# 检查ES索引是否存在 +curl http://localhost:9200/search_products + +# 查看详细错误日志 +python scripts/ingest_shoplazza.py --tenant-id 1 --recreate +``` + +### Q2: CSV文件找不到 + +**症状**: `ERROR: CSV file not found` + +**解决方案**: +```bash +# 检查CSV文件是否存在 +ls -lh data/customer1/goods_with_pic.5years_congku.csv.shuf.1w + +# 如果路径不同,修改 scripts/mock_data.sh 中的 TENANT2_CSV_FILE 变量 +``` + +### Q3: 导入时没有数据 + +**症状**: `WARNING: No documents to index` 或 `Transformed 0 SPU documents` + +**可能原因**: +1. 数据库中不存在该 tenant_id 的数据 +2. 数据都被标记为 `deleted=1` +3. tenant_id 类型不匹配 + +**解决步骤**: + +1. **查看调试信息**: 脚本会自动显示调试信息,包括: + ``` + DEBUG: tenant_id=1000: total=0, active=0, deleted=0 + DEBUG: Available tenant_ids in shoplazza_product_spu: + tenant_id=1: total=100, active=100 + tenant_id=2: total=50, active=50 + ``` + +2. **检查数据库**: 直接查询MySQL确认数据 + ```sql + -- 查看有哪些 tenant_id + SELECT tenant_id, COUNT(*) as count, + SUM(CASE WHEN deleted = 0 THEN 1 ELSE 0 END) as active + FROM shoplazza_product_spu + GROUP BY tenant_id; + + -- 检查特定 tenant_id 的数据 + SELECT COUNT(*) FROM shoplazza_product_spu + WHERE tenant_id = 2 AND deleted = 0; + ``` + +3. **如果数据库中没有数据,需要先导入数据**: + - 如果有CSV文件,使用CSV导入脚本 + - 如果没有CSV文件,可以使用mock数据生成脚本 + +4. **使用正确的 tenant_id**: 根据调试信息显示的可用 tenant_id,使用正确的值重新导入 + ```bash + ./scripts/ingest.sh 2 true # 使用调试信息中显示的 tenant_id + ``` + +### Q4: 模型下载慢或失败 + +**症状**: 首次运行时模型下载很慢或超时 + +**解决方案**: +```bash +# 跳过embedding快速测试(不推荐,但可以快速验证流程) +# 注意:这会导致搜索功能不完整 + +# 或手动下载模型到指定目录 +# TEXT_MODEL_DIR=/data/tw/models/bge-m3 +# IMAGE_MODEL_DIR=/data/tw/models/cn-clip +``` + +### Q5: 内存不足 + +**症状**: `Out of memory` + +**解决方案**: +```bash +# 减少批量大小 +python scripts/ingest_shoplazza.py \ + --tenant-id 1 \ + --batch-size 200 # 默认500,可以减少到100-200 +``` + +### Q6: 主键冲突 + +**症状**: `Duplicate entry` 错误 + +**解决方案**: +- Mock数据脚本会自动计算起始ID,避免冲突 +- 如果仍有冲突,可以手动清理旧数据: + ```sql + DELETE FROM shoplazza_product_spu WHERE tenant_id = 1; + DELETE FROM shoplazza_product_sku WHERE tenant_id = 1; + ``` + +--- + +## 相关文档 + +- **使用文档**: `USAGE_GUIDE.md` - 环境、启动、配置、日志查看 +- **字段说明文档**: `INDEX_FIELDS_DOCUMENTATION.md` - 索引字段详细说明 +- **API接口文档**: `API_INTEGRATION_GUIDE.md` - 完整的API对接指南 +- **README**: `README.md` - 项目概述和快速开始 + +--- + +**文档版本**: v2.0 +**最后更新**: 2024-12 + diff --git a/docs/环境配置说明.md b/docs/环境配置说明.md new file mode 100644 index 0000000..841674a --- /dev/null +++ b/docs/环境配置说明.md @@ -0,0 +1,123 @@ + + + +## 2. Python 运行环境 + +```bash +# 1. 激活 Conda +source /home/tw/miniconda3/etc/profile.d/conda.sh +conda activate searchengine + +# 如果部署到新机器,不存在 searchengine 环境时,需要初始化环境: +cd /home/tw/SearchEngine +pip install -r requirements.txt +``` + +--- + +## 3. 外部服务与端口 + +| 服务 | 默认地址 | 说明 | +|------|----------|------| +| Elasticsearch | `http://localhost:9200` | 可通过 Docker 单节点启动 | +| MySQL | `120.79.247.228:3316` | 存放店匠 SPU/SKU 数据 | +| Redis(可选) | `localhost:6479` | Embedding/翻译缓存 | + +示例:使用 Docker 启动 Elasticsearch + +```bash +docker run -d \ + --name elasticsearch \ + -p 9200:9200 \ + -e "discovery.type=single-node" \ + -e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \ + elasticsearch:8.11.0 +``` + +--- + +## 4. 环境变量与 `.env` 模板 + +在项目根目录创建 `.env`,并根据环境替换敏感信息: + +```env +# MySQL +DB_HOST=120.79.247.228 +DB_PORT=3316 +DB_DATABASE=saas +DB_USERNAME=saas +DB_PASSWORD=P89cZHS5d7dFyc9R + +# Elasticsearch +ES_HOST=http://localhost:9200 +ES_USERNAME=essa +ES_PASSWORD=4hOaLaf41y2VuI8y + +# Redis(可选) +REDIS_HOST=localhost +REDIS_PORT=6479 +REDIS_PASSWORD=BMfv5aI31kgHWtlx + +# DeepL 翻译 +DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed + +# API +API_HOST=0.0.0.0 +API_PORT=6002 +``` + +--- + +## 5. 服务凭证速查 + +| 项目 | 值 | +|------|----| +| **MySQL** | host `120.79.247.228`, port `3316`, user `saas`, password `P89cZHS5d7dFyc9R` | +| **Elasticsearch** | host `http://localhost:9200`, user `essa`, password `4hOaLaf41y2VuI8y` | +| **Redis(可选)** | host `localhost`, port `6479`, password `BMfv5aI31kgHWtlx` | +| **DeepL** | `c9293ab4-ad25-479b-919f-ab4e63b429ed` | + +> 所有凭证仅用于本地/测试环境,生产环境需替换并妥善保管。 + +--- + +## 6. 店匠数据源说明 + +SearchEngine 以 MySQL 中的店匠标准表为权威数据源: + +- `shoplazza_product_spu`:SPU 商品主表 +- `shoplazza_product_sku`:SKU 变体表 + +### `shoplazza_product_sku` 字段节选 + +| 字段 | 类型 | 描述 | +|------|------|------| +| `id` | bigint(20) | SKU 主键 | +| `spu_id` | bigint(20) | 对应 SPU | +| `shop_id` | bigint(20) | 店铺 ID | +| `shoplazza_product_id` | varchar(64) | 店匠商品 ID | +| `title` | varchar(500) | 变体标题 | +| `sku` | varchar(100) | SKU 编码 | +| `price` | decimal(10,2) | 售价 | +| `compare_at_price` | decimal(10,2) | 原价 | +| `option1/2/3` | varchar(255) | 颜色/尺码等选项 | +| `inventory_quantity` | int(11) | 库存 | +| `image_src` | varchar(500) | 图片 | +| `tenant_id` | bigint(20) | 租户 | +| `create_time` | datetime | 创建时间 | +| `update_time` | datetime | 更新时间 | +| `deleted` | bit(1) | 逻辑删除标记 | + +> 完整字段、索引映射与 ES 对应关系详见 `INDEX_FIELDS_DOCUMENTATION.md`。 + +--- + +## 7. 相关脚本 + +- `scripts/mock_data.sh`:一次性生成 Tenant1 Mock + Tenant2 CSV 数据并导入 MySQL +- `scripts/ingest.sh [recreate]`:从 MySQL 写入 Elasticsearch +- `run.sh` / `restart.sh`:服务启动/重启 + +更多脚本参数、日志与验证命令参见 `USAGE_GUIDE.md` 与 `TEST_DATA_GUIDE.md`。 + + diff --git a/docs/系统设计文档.md b/docs/系统设计文档.md new file mode 100644 index 0000000..df25f92 --- /dev/null +++ b/docs/系统设计文档.md @@ -0,0 +1,724 @@ +# 搜索引擎通用化开发进度 + +## 项目概述 + +对后端搜索技术 做通用化。 +通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 + + +**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 + +--- + +## 1. 原始数据层的约定 + +### 1.1 店匠主表 + +所有租户共用以下主表: +- `shoplazza_product_sku` - SKU级别商品数据 +- `shoplazza_product_spu` - SPU级别商品数据 + +### 1.2 索引结构(SPU维度) + +**统一索引架构**: +- 所有客户共享同一个Elasticsearch索引:`search_products` +- 索引粒度:SPU级别(每个文档代表一个SPU) +- 数据隔离:通过`tenant_id`字段实现租户隔离 +- 嵌套结构:每个SPU文档包含嵌套的`variants`数组(SKU变体) + +**索引文档结构**: +```json +{ + "tenant_id": "1", + "product_id": "123", + "title": "蓝牙耳机", + "variants": [ + { + "variant_id": "456", + "title": "黑色", + "price": 199.99, + "sku": "SKU-123-1", + "stock": 50 + } + ], + "min_price": 199.99, + "max_price": 299.99 +} +``` + +### 1.3 配置化方案 + +**配置分离原则**: +- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置 +- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定 +- **数据导入流程**:写死的脚本,不依赖配置 + +统一通过配置文件定义: +1. ES 字段定义(字段类型、分析器、boost等) +2. ES mapping 结构生成 +3. 查询域配置(indexes) +4. 排序和打分配置(function_score) + +**注意**:配置中**不包含**以下内容: +- `mysql_config` - MySQL数据库配置 +- `main_table` / `extension_table` - 数据表配置 +- `source_table` / `source_column` - 字段数据源映射 + +--- + +## 2. 配置系统实现 + +### 2.1 应用结构配置(字段定义) + +**配置文件位置**:`config/schema/{tenant_id}_config.yaml` + +**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。 + +**实现情况**: + +#### 字段类型支持 +- **TEXT**:文本字段,支持多语言分析器 +- **KEYWORD**:关键词字段,用于精确匹配和聚合 +- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度) +- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度) +- **INT/LONG**:整数类型 +- **FLOAT/DOUBLE**:浮点数类型 +- **DATE**:日期类型 +- **BOOLEAN**:布尔类型 + +#### 分析器支持 +- **chinese_ecommerce**:中文电商分词器(index_ansj/query_ansj) +- **english**:英文分析器 +- **russian**:俄文分析器 +- **arabic**:阿拉伯文分析器 +- **spanish**:西班牙文分析器 +- **japanese**:日文分析器 +- **standard**:标准分析器 +- **keyword**:关键词分析器 + +#### 字段配置示例(Base配置) + +```yaml +fields: + # 租户隔离字段(必需) + - name: "tenant_id" + type: "KEYWORD" + required: true + index: true + store: true + + # 商品标识字段 + - name: "product_id" + type: "KEYWORD" + required: true + index: true + store: true + + # 文本搜索字段 + - name: "title" + type: "TEXT" + analyzer: "chinese_ecommerce" + boost: 3.0 + index: true + store: true + + - name: "seo_keywords" + type: "TEXT" + analyzer: "chinese_ecommerce" + boost: 2.0 + index: true + store: true + + # 嵌套variants字段 + - name: "variants" + type: "JSON" + nested: true + nested_properties: + variant_id: + type: "keyword" + price: + type: "float" + sku: + type: "keyword" +``` + +**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。 + +**实现模块**: +- `config/config_loader.py` - 配置加载器 +- `config/field_types.py` - 字段类型定义 +- `indexer/mapping_generator.py` - ES mapping 生成器 +- `indexer/data_transformer.py` - 数据转换器 + +### 2.2 索引结构配置(查询域配置) + +**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 + +**实现情况**: + +#### 域(Domain)配置 +每个域定义了: +- 域名称(如 `default`, `title`, `category`, `brand`) +- 域标签(中文描述) +- 搜索字段列表 +- 默认分析器 +- 权重(boost) +- **多语言字段映射**(`language_field_mapping`) + +#### 多语言字段映射 + +支持将不同语言的查询路由到对应的字段: + +```yaml +indexes: + - name: "default" + label: "默认索引" + fields: + - "name" + - "enSpuName" + - "ruSkuName" + - "categoryName" + - "brandName" + analyzer: "chinese_ecommerce" + boost: 1.0 + language_field_mapping: + zh: + - "name" + - "categoryName" + - "brandName" + en: + - "enSpuName" + ru: + - "ruSkuName" + + - name: "title" + label: "标题索引" + fields: + - "name" + - "enSpuName" + - "ruSkuName" + analyzer: "chinese_ecommerce" + boost: 2.0 + language_field_mapping: + zh: + - "name" + en: + - "enSpuName" + ru: + - "ruSkuName" +``` + +**工作原理**: +1. 检测查询语言(中文、英文、俄文等) +2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 +3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 +4. 组合多个语言查询的结果,提高召回率 + +**实现模块**: +- `search/multilang_query_builder.py` - 多语言查询构建器 +- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) + +--- + +## 3. 数据导入流程 + +### 3.1 数据源 + +**店匠标准表**(Base配置使用): +- `shoplazza_product_spu` - SPU级别商品数据 +- `shoplazza_product_sku` - SKU级别商品数据 + +**其他客户表**(tenant1等): +- 使用各自的数据源表和扩展表 + +### 3.2 数据导入方式 + +**Pipeline层决定数据源**: +- 数据导入流程是写死的脚本,不依赖配置 +- 配置只关注ES搜索相关的内容 +- 数据源映射逻辑写死在转换器代码中 + +#### Base配置数据导入(店匠通用) + +**脚本**:`scripts/ingest_shoplazza.py` + +**数据流程**: +1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表 +2. **数据转换**(`indexer/spu_transformer.py`): + - 按`spu_id`和`tenant_id`关联SPU和SKU数据 + - 将SKU数据聚合为嵌套的`variants`数组 + - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`) + - 字段映射(写死在代码中,不依赖配置) + - 注入`tenant_id`字段 +3. **索引创建**: + - 根据配置生成ES mapping + - 创建或更新`search_products`索引 +4. **批量入库**: + - 批量写入ES(默认每批500条) + - 错误处理和重试机制 + +**命令行工具**: +```bash +python scripts/ingest_shoplazza.py \ + --db-host localhost \ + --db-port 3306 \ + --db-database saas \ + --db-username root \ + --db-password password \ + --tenant-id "1" \ + --config base \ + --es-host http://localhost:9200 \ + --recreate \ + --batch-size 500 +``` + +#### 其他客户数据导入 + +- 使用各自的数据转换器(如`indexer/data_transformer.py`) +- 数据源映射逻辑写死在各自的转换器中 +- 共享`search_products`索引,通过`tenant_id`隔离 + +**实现模块**: +- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) +- `indexer/data_transformer.py` - 通用数据转换器(其他客户) +- `indexer/bulk_indexer.py` - 批量索引器 +- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 + +--- + +## 4. QueryParser 实现 + + +### 4.1 查询改写(Query Rewriting) + +配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 +**实现情况**: + +#### 配置方式 +在 `query_config.rewrite_dictionary` 中配置查询改写规则: + +```yaml +query_config: + enable_query_rewrite: true + rewrite_dictionary: + "芭比": "brand:芭比 OR name:芭比娃娃" + "玩具": "category:玩具" + "消防": "category:消防 OR name:消防" +``` + +#### 功能特性 +- **精确匹配**:查询完全匹配词典 key 时,替换为 value +- **部分匹配**:查询包含词典 key 时,替换该部分 +- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) + +#### 实现模块 +- `query/query_rewriter.py` - 查询改写器 +- `query/query_parser.py` - 查询解析器(集成改写功能) + +### 4.2 翻译(Translation) + +**实现情况**: + +#### 配置方式 +```yaml +query_config: + supported_languages: + - "zh" + - "en" + - "ru" + default_language: "zh" + enable_translation: true + translation_service: "deepl" + translation_api_key: null # 通过环境变量设置 +``` + +#### 功能特性 +1. **语言检测**:自动检测查询语言 +2. **智能翻译**: + - 如果查询是中文,翻译为英文、俄文 + - 如果查询是英文,翻译为中文、俄文 + - 如果查询是其他语言,翻译为所有支持的语言 +3. **域感知翻译**: + - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 + - 避免不必要的翻译,提高效率 +4. **翻译缓存**:缓存翻译结果,避免重复调用 API + +#### 工作流程 +``` +查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建 +``` + +#### 实现模块 +- `query/language_detector.py` - 语言检测器 +- `query/translator.py` - 翻译器(DeepL API) +- `query/query_parser.py` - 查询解析器(集成翻译功能) + +### 4.3 文本向量化(Text Embedding) + +如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 + +**实现情况**: + +#### 配置方式 +```yaml +query_config: + enable_text_embedding: true +``` + +#### 功能特性 +1. **条件生成**: + - 仅当 `enable_text_embedding=true` 时生成向量 + - 仅对 `default` 域查询生成向量 +2. **向量模型**:BGE-M3 模型(1024维向量) +3. **用途**:用于语义搜索(KNN 检索) + +#### 实现模块 +- `embeddings/bge_encoder.py` - BGE 文本编码器 +- `query/query_parser.py` - 查询解析器(集成向量生成) + +--- + +## 5. Searcher 实现 + +参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 +比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 + +查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 + +### 5.1 布尔表达式解析 + +**实现情况**: + +#### 支持的运算符 +- **AND**:所有项必须匹配 +- **OR**:任意项匹配 +- **RANK**:排序增强(类似 OR 但影响排序) +- **ANDNOT**:排除(第一项匹配,第二项不匹配) +- **()**:括号分组 + +#### 优先级(从高到低) +1. `()` - 括号 +2. `ANDNOT` - 排除 +3. `AND` - 与 +4. `OR` - 或 +5. `RANK` - 排序 + +#### 示例 +``` +laptop AND (gaming OR professional) ANDNOT cheap +``` + +#### 实现模块 +- `search/boolean_parser.py` - 布尔表达式解析器 +- `search/searcher.py` - 搜索器(集成布尔解析) + +### 5.2 多语言搜索 + +**实现情况**: + +#### 工作原理 +1. **查询解析**: + - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) + - 检测查询语言 + - 生成翻译 +2. **多语言查询构建**: + - 如果域有 `language_field_mapping`: + - 使用检测到的语言查询对应字段(boost * 1.5) + - 使用翻译后的查询搜索其他语言字段(boost * 1.0) + - 如果域没有 `language_field_mapping`: + - 使用所有字段进行搜索 +3. **查询组合**: + - 多个语言查询组合为 `should` 子句 + - 提高召回率 + +#### 示例 +``` +查询: "芭比娃娃" +域: default +检测语言: zh + +生成的查询: +- 中文查询 "芭比娃娃" → 搜索 name, categoryName, brandName (boost * 1.5) +- 英文翻译 "Barbie doll" → 搜索 enSpuName (boost * 1.0) +- 俄文翻译 "Кукла Барби" → 搜索 ruSkuName (boost * 1.0) +``` + +#### 实现模块 +- `search/multilang_query_builder.py` - 多语言查询构建器 +- `search/searcher.py` - 搜索器(使用多语言构建器) + +### 5.3 相关性计算(Ranking) + +**实现情况**: + +#### 当前实现 +**公式**:`bm25() + 0.2 * text_embedding_relevance()` + +- **bm25()**:BM25 文本相关性得分 + - 包括多语言打分 + - 内部通过配置翻译为多种语言 + - 分别到对应的字段搜索 + - 中文字段使用中文分词器,英文字段使用英文分词器 +- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) + - 权重:0.2 + +#### 配置方式 +```yaml +ranking: + expression: "bm25() + 0.2*text_embedding_relevance()" + description: "BM25 text relevance combined with semantic embedding similarity" +``` + +#### 扩展性 +- 支持表达式配置(未来可扩展) +- 支持自定义函数(如 `timeliness()`, `field_value()`) + +#### 实现模块 +- `search/ranking_engine.py` - 排序引擎 +- `search/searcher.py` - 搜索器(集成排序功能) + +--- + +## 6. 已完成功能总结 + +### 6.1 配置系统 +- ✅ 字段定义配置(类型、分析器、来源表/列) +- ✅ 索引域配置(多域查询、多语言映射) +- ✅ 查询配置(改写词典、翻译配置) +- ✅ 排序配置(表达式配置) +- ✅ 配置验证(字段存在性、类型检查、分析器匹配) + +### 6.2 数据索引 +- ✅ 数据转换(字段映射、类型转换) +- ✅ 向量生成(文本向量、图片向量) +- ✅ 向量缓存(避免重复计算) +- ✅ 批量索引(错误处理、重试机制) +- ✅ ES mapping 自动生成 + +### 6.3 查询处理 +- ✅ 查询改写(词典配置) +- ✅ 语言检测 +- ✅ 多语言翻译(DeepL API) +- ✅ 文本向量化(BGE-M3) +- ✅ 域提取(支持 `domain:query` 语法) + +### 6.4 搜索功能 +- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) +- ✅ 多语言查询构建(语言路由、字段映射) +- ✅ 语义搜索(KNN 检索) +- ✅ 相关性排序(BM25 + 向量相似度) +- ✅ 结果聚合(Faceted Search) + +### 6.5 API 服务 +- ✅ RESTful API(FastAPI) +- ✅ 搜索接口(文本搜索、图片搜索) +- ✅ 文档查询接口 +- ✅ 前端界面(HTML + JavaScript) +- ✅ 租户隔离(tenant_id过滤) + +### 6.6 Base配置(店匠通用) +- ✅ SPU级别索引结构 +- ✅ 嵌套variants字段 +- ✅ 统一索引(search_products) +- ✅ 租户隔离(tenant_id) +- ✅ 配置简化(移除MySQL相关配置) + +--- + +## 7. 技术栈 + +- **后端**:Python 3.6+ +- **搜索引擎**:Elasticsearch +- **数据库**:MySQL(Shoplazza) +- **向量模型**:BGE-M3(文本)、CN-CLIP(图片) +- **翻译服务**:DeepL API +- **API 框架**:FastAPI +- **前端**:HTML + JavaScript + +--- + +## 8. API响应格式 + +### 8.1 外部友好格式 + +API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: + +**响应结构**: +```json +{ + "results": [ + { + "product_id": "123", + "title": "蓝牙耳机", + "variants": [ + { + "variant_id": "456", + "price": 199.99, + "sku": "SKU-123-1", + "stock": 50 + } + ], + "relevance_score": 0.95 + } + ], + "total": 10, + "facets": [...], + "suggestions": [], + "related_searches": [] +} +``` + +**主要变化**: +- 结构化结果(`ProductResult`和`VariantResult`) +- 嵌套variants数组 +- 无ES内部字段 + +### 8.2 租户隔离 + +所有API请求必须提供`tenant_id`: +- 请求头:`X-Tenant-ID: 1` +- 或查询参数:`?tenant_id=1` + +搜索时自动添加`tenant_id`过滤,确保数据隔离。 + +### 8.3 数据接口约定 + +**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 + +#### 8.3.1 数据流模式 + +系统采用统一的数据流模式,确保数据在各层之间的一致性: + +**数据流转路径**: +``` +API Request (JSON) + ↓ +Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) + ↓ +Searcher(透传) + ↓ +ES Query Builder → model_dump() 转换为字典 + ↓ +ES Query (字典) + ↓ +Elasticsearch +``` + +#### 8.3.2 Facets 配置数据流 + +**输入格式**:`List[Union[str, FacetConfig]]` + +- **简单模式**:字符串列表(字段名),使用默认配置 + ```json + ["categoryName_keyword", "brandName_keyword"] + ``` + +- **高级模式**:FacetConfig 对象列表,支持自定义配置 + ```json + [ + { + "field": "categoryName_keyword", + "size": 15, + "type": "terms" + }, + { + "field": "price", + "type": "range", + "ranges": [ + {"key": "0-50", "to": 50}, + {"key": "50-100", "from": 50, "to": 100} + ] + } + ] + ``` + +**数据流**: +1. API 层:接收 `List[Union[str, FacetConfig]]` +2. Searcher 层:透传,不做转换 +3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 +4. 输出:转换为 ES 聚合查询 + +#### 8.3.3 Range Filters 数据流 + +**输入格式**:`Dict[str, RangeFilter]` + +**RangeFilter 模型**: +```python +class RangeFilter(BaseModel): + gte: Optional[Union[float, str]] # 大于等于 + gt: Optional[Union[float, str]] # 大于 + lte: Optional[Union[float, str]] # 小于等于 + lt: Optional[Union[float, str]] # 小于 +``` + +**示例**: +```json +{ + "price": {"gte": 50, "lte": 200}, + "created_at": {"gte": "2023-01-01T00:00:00Z"} +} +``` + +**数据流**: +1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 +2. Searcher 层:透传 `Dict[str, RangeFilter]` +3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 +4. 输出:ES range 查询(支持数值和日期) + +**特性**: +- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) +- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) +- 统一约定:所有范围过滤都使用 RangeFilter 模型 + +#### 8.3.4 响应 Facets 数据流 + +**输出格式**:`List[FacetResult]` + +**FacetResult 模型**: +```python +class FacetResult(BaseModel): + field: str # 字段名 + label: str # 显示标签 + type: Literal["terms", "range"] # 分面类型 + values: List[FacetValue] # 分面值列表 + total_count: Optional[int] # 总文档数 +``` + +**数据流**: +1. ES Response:返回聚合结果(字典格式) +2. Searcher 层:构建 `List[FacetResult]` 对象 +3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) + +**优势**: +- 类型安全:使用 Pydantic 模型确保数据结构一致性 +- 自动序列化:模型自动转换为 JSON,无需手动处理 +- 统一约定:所有响应都使用标准化的 Pydantic 模型 + +#### 8.3.5 统一约定的好处 + +1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 +2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 +3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) +4. **易于维护**:修改数据结构只需更新模型定义 +5. **数据验证**:自动验证输入数据,减少错误处理代码 + +**实现模块**: +- `api/models.py` - 所有 Pydantic 模型定义 +- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型) +- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询) + +## 9. 配置文件示例 + +**Base配置**(店匠通用):`config/schema/base/config.yaml` + +**其他客户配置**:`config/schema/tenant1/config.yaml` + +--- + +## 9. 相关文档 + +- `MULTILANG_FEATURE.md` - 多语言功能详细说明 +- `QUICKSTART.md` - 快速开始指南 +- `HighLevelDesign.md` - 高层设计文档 +- `IMPLEMENTATION_SUMMARY.md` - 实现总结 +- `商品数据源入ES配置规范.md` - 数据源配置规范 diff --git a/docs/索引字段说明.md b/docs/索引字段说明.md new file mode 100644 index 0000000..3b85168 --- /dev/null +++ b/docs/索引字段说明.md @@ -0,0 +1,223 @@ +# 索引字段说明文档 + +本文档详细说明了 Elasticsearch 索引中所有字段的类型、索引方式、数据来源等信息。 + +## 索引基本信息 + +- **索引名称**: `search_products` +- **索引级别**: SPU级别(商品级别) +- **数据结构**: SPU文档包含嵌套的variants(SKU)数组 + +## 字段说明表 + +### 基础字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| +| tenant_id | KEYWORD | 是 | 精确匹配 | SPU表 | tenant_id | BIGINT | 租户ID,用于多租户隔离 | +| product_id | KEYWORD | 是 | 精确匹配 | SPU表 | id | BIGINT | 商品ID(SPU ID) | +| handle | KEYWORD | 是 | 精确匹配 | SPU表 | handle | VARCHAR(255) | 商品URL handle | + +### 文本搜索字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|------| +| title | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | title | VARCHAR(512) | 3.0 | 商品标题,权重最高 | +| brief | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | brief | VARCHAR(512) | 1.5 | 商品简介 | +| description | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | description | TEXT | 1.0 | 商品详细描述 | + +### SEO字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------| +| seo_title | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_title | VARCHAR(512) | 2.0 | 否 | SEO标题,用于提升相关性 | +| seo_description | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_description | TEXT | 1.5 | 否 | SEO描述 | +| seo_keywords | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | seo_keywords | VARCHAR(1024) | 2.0 | 否 | SEO关键词 | + +### 分类和标签字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | Boost权重 | 是否返回 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|-----------|---------|------| +| vendor | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | vendor | VARCHAR(255) | 1.5 | 是 | 供应商/品牌(文本搜索) | +| vendor_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | vendor | VARCHAR(255) | - | 否 | 供应商/品牌(精确匹配,用于过滤) | +| product_type | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 商品类型(文本搜索) | +| product_type_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 商品类型(精确匹配,用于过滤) | +| tags | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | tags | VARCHAR(1024) | 1.0 | 是 | 标签(文本搜索) | +| tags_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | tags | VARCHAR(1024) | - | 否 | 标签(精确匹配,用于过滤) | +| category | TEXT | 是 | chinese_ecommerce分析器 | SPU表 | category | VARCHAR(255) | 1.5 | 是 | 类目(文本搜索) | +| category_keyword | KEYWORD | 是 | 精确匹配 | SPU表 | category | VARCHAR(255) | - | 否 | 类目(精确匹配,用于过滤) | + +### 价格字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| +| min_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | price | DECIMAL(10,2) | 最低价格(从所有SKU中取最小值) | +| max_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | price | DECIMAL(10,2) | 最高价格(从所有SKU中取最大值) | +| compare_at_price | FLOAT | 是 | 数值范围 | SKU表(聚合计算) | compare_at_price | DECIMAL(10,2) | 原价(从所有SKU中取最大值) | + +**价格计算逻辑**: +- `min_price`: 取该SPU下所有SKU的price字段的最小值 +- `max_price`: 取该SPU下所有SKU的price字段的最大值 +- `compare_at_price`: 取该SPU下所有SKU的compare_at_price字段的最大值(如果存在) + +### 图片字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| +| image_url | KEYWORD | 否 | 不索引 | SPU表 | image_src | VARCHAR(500) | 商品主图URL,仅用于展示 | + +### 文本嵌入字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| +| title_embedding | TEXT_EMBEDDING | 是 | 向量相似度(dot_product) | 计算生成 | title | VARCHAR(512) | 标题的文本向量(1024维),用于语义搜索 | + +**说明**: +- 向量维度:1024 +- 相似度算法:dot_product(点积) +- 数据来源:基于title字段通过BGE-M3模型生成 + +### 时间字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 是否返回 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|---------|------| +| create_time | DATE | 是 | 日期范围 | SPU表 | create_time | DATETIME | 是 | 创建时间 | +| update_time | DATE | 是 | 日期范围 | SPU表 | update_time | DATETIME | 是 | 更新时间 | +| shoplazza_created_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_created_at | DATETIME | 否 | 店匠系统创建时间 | +| shoplazza_updated_at | DATE | 是 | 日期范围 | SPU表 | shoplazza_updated_at | DATETIME | 否 | 店匠系统更新时间 | + +### 嵌套Variants字段(SKU级别) + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| +| variants | JSON (nested) | 是 | 嵌套对象 | SKU表 | - | - | 商品变体数组(嵌套结构) | + +#### Variants子字段 + +| 索引字段名 | ES字段类型 | 是否索引 | 索引方式 | 数据来源表 | 表中字段名 | 表中字段类型 | 说明 | +|-----------|-----------|---------|---------|-----------|-----------|-------------|------| +| variants.variant_id | keyword | 是 | 精确匹配 | SKU表 | id | BIGINT | 变体ID(SKU ID) | +| variants.title | text | 是 | chinese_ecommerce分析器 | SKU表 | title | VARCHAR(500) | 变体标题 | +| variants.price | float | 是 | 数值范围 | SKU表 | price | DECIMAL(10,2) | 变体价格 | +| variants.compare_at_price | float | 是 | 数值范围 | SKU表 | compare_at_price | DECIMAL(10,2) | 变体原价 | +| variants.sku | keyword | 是 | 精确匹配 | SKU表 | sku | VARCHAR(100) | SKU编码 | +| variants.stock | long | 是 | 数值范围 | SKU表 | inventory_quantity | INT(11) | 库存数量 | +| variants.options | object | 是 | 对象 | SKU表 | option1/option2/option3 | VARCHAR(255) | 选项(颜色、尺寸等) | + +**Variants结构说明**: +- `variants` 是一个嵌套对象数组,每个元素代表一个SKU +- 使用ES的nested类型,支持对嵌套字段进行独立查询和过滤 +- `options` 对象包含 `option1`、`option2`、`option3` 三个字段,分别对应SKU表中的选项值 + +## 字段类型说明 + +### ES字段类型映射 + +| ES字段类型 | Elasticsearch映射 | 用途 | +|-----------|------------------|------| +| KEYWORD | keyword | 精确匹配、过滤、聚合、排序 | +| TEXT | text | 全文检索(支持分词) | +| FLOAT | float | 浮点数(价格、权重等) | +| LONG | long | 整数(库存、计数等) | +| DATE | date | 日期时间 | +| TEXT_EMBEDDING | dense_vector | 文本向量(1024维) | +| JSON | object/nested | 嵌套对象 | + +### 分析器说明 + +| 分析器名称 | 语言 | 说明 | +|-----------|------|------| +| chinese_ecommerce | 中文 | Ansj中文分词器(电商优化),用于中文文本的分词和搜索 | + +## 索引配置 + +### 索引设置 + +- **分片数**: 1 +- **副本数**: 0 +- **刷新间隔**: 30秒 + +### 查询域(Query Domains) + +系统定义了多个查询域,用于在不同场景下搜索不同的字段组合: + +1. **default(默认索引)**: 搜索所有文本字段 + - 包含字段:title, brief, description, seo_title, seo_description, seo_keywords, vendor, product_type, tags, category + - Boost: 1.0 + +2. **title(标题索引)**: 仅搜索标题相关字段 + - 包含字段:title, seo_title + - Boost: 2.0 + +3. **vendor(品牌索引)**: 仅搜索品牌字段 + - 包含字段:vendor + - Boost: 1.5 + +4. **category(类目索引)**: 仅搜索类目字段 + - 包含字段:category + - Boost: 1.5 + +5. **tags(标签索引)**: 搜索标签和SEO关键词 + - 包含字段:tags, seo_keywords + - Boost: 1.0 + +## 数据转换规则 + +### 数据类型转换 + +1. **BIGINT → KEYWORD**: 数字ID转换为字符串(如 `product_id`, `variant_id`) +2. **DECIMAL → FLOAT**: 价格字段从DECIMAL转换为FLOAT +3. **INT → LONG**: 库存数量从INT转换为LONG +4. **DATETIME → DATE**: 时间字段转换为ISO格式字符串 + +### 特殊处理 + +1. **价格聚合**: 从多个SKU的价格中计算min_price、max_price、compare_at_price +2. **图片URL处理**: 如果image_src不是完整URL,会自动添加协议前缀 +3. **选项合并**: 将SKU表的option1、option2、option3合并为options对象 + +## 注意事项 + +1. **多租户隔离**: 所有查询必须包含 `tenant_id` 过滤条件 +2. **嵌套查询**: 查询variants字段时需要使用nested查询语法 +3. **字段命名**: 用于过滤的字段应使用 `*_keyword` 后缀的字段 +4. **向量搜索**: title_embedding字段用于语义搜索,需要配合文本查询使用 +5. **Boost权重**: 不同字段的boost权重影响搜索结果的相关性排序 + +## 数据来源表结构 + +### SPU表(shoplazza_product_spu) + +主要字段: +- `id`: BIGINT - 主键ID +- `tenant_id`: BIGINT - 租户ID +- `handle`: VARCHAR(255) - URL handle +- `title`: VARCHAR(512) - 商品标题 +- `brief`: VARCHAR(512) - 商品简介 +- `description`: TEXT - 商品描述 +- `vendor`: VARCHAR(255) - 供应商/品牌 +- `category`: VARCHAR(255) - 类目 +- `tags`: VARCHAR(1024) - 标签 +- `seo_title`: VARCHAR(512) - SEO标题 +- `seo_description`: TEXT - SEO描述 +- `seo_keywords`: VARCHAR(1024) - SEO关键词 +- `image_src`: VARCHAR(500) - 图片URL +- `create_time`: DATETIME - 创建时间 +- `update_time`: DATETIME - 更新时间 +- `shoplazza_created_at`: DATETIME - 店匠创建时间 +- `shoplazza_updated_at`: DATETIME - 店匠更新时间 + +### SKU表(shoplazza_product_sku) + +主要字段: +- `id`: BIGINT - 主键ID(对应variant_id) +- `spu_id`: BIGINT - SPU ID(关联字段) +- `title`: VARCHAR(500) - 变体标题 +- `price`: DECIMAL(10,2) - 价格 +- `compare_at_price`: DECIMAL(10,2) - 原价 +- `sku`: VARCHAR(100) - SKU编码 +- `inventory_quantity`: INT(11) - 库存数量 +- `option1`: VARCHAR(255) - 选项1 +- `option2`: VARCHAR(255) - 选项2 +- `option3`: VARCHAR(255) - 选项3 + diff --git a/商品数据源入ES配置规范.md b/商品数据源入ES配置规范.md deleted file mode 100644 index a503d64..0000000 --- a/商品数据源入ES配置规范.md +++ /dev/null @@ -1,221 +0,0 @@ -根据您提供的内容,我将其整理为规范的Markdown格式: - -# ES索引配置文档 - -## 1. 全局配置 - -### 1.1 文本字段相关性设定 -需要修改所有text字段相关性算法-BM25算法的默认参数: -```json -"similarity": { - "default": { - "type": "BM25", - "b": "0.0", - "k1": "0.0" - } -} -``` - -### 1.2 索引分片设定 -- `number_of_replicas`:0/1 -- `number_of_shards`:设置建议 分片数 <= ES集群的总CPU核心个数/ (副本数 + 1) - -### 1.3 索引刷新时间设定 -- `refresh_interval`:默认30S,根据客户需要进行调整 -```json -"refresh_interval": "30s" -``` - -## 2. 单个字段配置 - -| 分析方式 | 字段预处理和ES输入格式要求 | 对应ES mapping配置 | 备注 | -|---------|--------------------------|-------------------|------| -| 电商通用分析-中文 | - | ```json { "type": "text", "analyzer": "index_ansj", "search_analyzer": "query_ansj" } ``` | - | -| 文本-多语言向量化 | 调用"文本向量化"模块得到1024维向量 | ```json { "type": "dense_vector", "dims": 1024, "index": true, "similarity": "dot_product" } ``` | 1. 依赖"文本向量化"模块
2. 如果定期全量,需要对向量化结果做缓存 | -| 图片-向量化 | 调用"图片向量化"模块得到1024维向量 | ```json { "type": "nested", "properties": { "vector": { "type": "dense_vector", "dims": 1024, "similarity": "dot_product" }, "url": { "type": "text" } } } ``` | 1. 依赖"图片向量化"模块
2. 如果定期全量,需要对向量化结果做缓存 | -| 关键词 | ES输入格式:list或者单个值 | ```json {"type": "keyword"} ``` | - | -| 电商通用分析-英文 | - | ```json {"type": "text", "analyzer": "english"} ``` | - | -| 电商通用分析-阿拉伯文 | - | ```json {"type": "text", "analyzer": "arabic"} ``` | - | -| 电商通用分析-西班牙文 | - | ```json {"type": "text", "analyzer": "spanish"} ``` | - | -| 电商通用分析-俄文 | - | ```json {"type": "text", "analyzer": "russian"} ``` | - | -| 电商通用分析-日文 | - | ```json {"type": "text", "analyzer": "japanese"} ``` | - | -| 数值-整数 | - | ```json {"type": "long"} ``` | - | -| 数值-浮点型 | - | ```json {"type": "float"} ``` | - | -| 分值 | 输入是float,配置处理方式:log, pow, sigmoid等 | TODO:给代码, log | - | -| 子串 | - | 暂时不支持 | - | -| ngram匹配或前缀匹配或边缘前缀匹配 | - | 暂时不支持 | 以后根据需要再添加 | - -这样整理后,文档结构更加清晰,表格格式规范,便于阅读和理解。 - - -参考 opensearch: - -数据接口 -文本相关性字段 -向量相关性字段 -3. 模块提取 -文本向量化 -import sys -import torch -from sentence_transformers import SentenceTransformer -import time -import threading -from modelscope import snapshot_download -from transformers import AutoModel -import os -from openai import OpenAI -from config.logging_config import get_app_logger - -# Get logger for this module -logger = get_app_logger(__name__) - -class BgeEncoder: - _instance = None - _lock = threading.Lock() - - def __new__(cls, model_dir='Xorbits/bge-m3'): - with cls._lock: - if cls._instance is None: - cls._instance = super(BgeEncoder, cls).__new__(cls) - logger.info("[BgeEncoder] Creating a new instance with model directory: %s", model_dir) - cls._instance.model = SentenceTransformer(snapshot_download(model_dir)) - logger.info("[BgeEncoder] New instance has been created") - return cls._instance - - def encode(self, sentences, normalize_embeddings=True, device='cuda'): - # Move model to specified device - if device == 'gpu': - device = 'cuda' - self.model = self.model.to(device) - embeddings = self.model.encode(sentences, normalize_embeddings=normalize_embeddings, device=device, show_progress_bar=False) - return embeddings -图片向量化 -import sys -import os -import io -import requests -import torch -import numpy as np -from PIL import Image -import logging -import threading -from typing import List, Optional, Union -from config.logging_config import get_app_logger -import cn_clip.clip as clip -from cn_clip.clip import load_from_name - -# Get logger for this module -logger = get_app_logger(__name__) - -# DEFAULT_MODEL_NAME = "ViT-L-14-336" # ["ViT-B-16", "ViT-L-14", "ViT-L-14-336", "ViT-H-14", "RN50"] -DEFAULT_MODEL_NAME = "ViT-H-14" -MODEL_DOWNLOAD_DIR = "/data/tw/uat/EsSearcher" - -class CLIPImageEncoder: - """CLIP Image Encoder for generating image embeddings using cn_clip""" - - _instance = None - _lock = threading.Lock() - - def __new__(cls, model_name=DEFAULT_MODEL_NAME, device=None): - with cls._lock: - if cls._instance is None: - cls._instance = super(CLIPImageEncoder, cls).__new__(cls) - logger.info(f"[CLIPImageEncoder] Creating new instance with model: {model_name}") - cls._instance._initialize_model(model_name, device) - return cls._instance - - def _initialize_model(self, model_name, device): - """Initialize the CLIP model using cn_clip""" - try: - self.device = device if device else ("cuda" if torch.cuda.is_available() else "cpu") - self.model, self.preprocess = load_from_name(model_name, device=self.device, download_root=MODEL_DOWNLOAD_DIR) - self.model.eval() - self.model_name = model_name - logger.info(f"[CLIPImageEncoder] Model {model_name} initialized successfully on device {self.device}") - - except Exception as e: - logger.error(f"[CLIPImageEncoder] Failed to initialize model: {str(e)}") - raise - - def validate_image(self, image_data: bytes) -> Image.Image: - """Validate image data and return PIL Image if valid""" - try: - image_stream = io.BytesIO(image_data) - image = Image.open(image_stream) - image.verify() - image_stream.seek(0) - image = Image.open(image_stream) - if image.mode != 'RGB': - image = image.convert('RGB') - return image - except Exception as e: - raise ValueError(f"Invalid image data: {str(e)}") - - def download_image(self, url: str, timeout: int = 10) -> bytes: - """Download image from URL""" - try: - if url.startswith(('http://', 'https://')): - response = requests.get(url, timeout=timeout) - if response.status_code != 200: - raise ValueError(f"HTTP {response.status_code}") - return response.content - else: - # Local file path - with open(url, 'rb') as f: - return f.read() - except Exception as e: - raise ValueError(f"Failed to download image from {url}: {str(e)}") - - def preprocess_image(self, image: Image.Image, max_size: int = 1024) -> Image.Image: - """Preprocess image for CLIP model""" - # Resize if too large - if max(image.size) > max_size: - ratio = max_size / max(image.size) - new_size = tuple(int(dim * ratio) for dim in image.size) - image = image.resize(new_size, Image.Resampling.LANCZOS) - return image - - def encode_text(self, text): - """Encode text to embedding vector using cn_clip""" - text_data = clip.tokenize([text] if type(text) == str else text).to(self.device) - with torch.no_grad(): - text_features = self.model.encode_text(text_data) - text_features /= text_features.norm(dim=-1, keepdim=True) - return text_features - - def encode_image(self, image: Image.Image) -> Optional[np.ndarray]: - """Encode image to embedding vector using cn_clip""" - if not isinstance(image, Image.Image): - raise ValueError("CLIPImageEncoder.encode_image Input must be a PIL.Image") - - try: - infer_data = self.preprocess(image).unsqueeze(0).to(self.device) - with torch.no_grad(): - image_features = self.model.encode_image(infer_data) - image_features /= image_features.norm(dim=-1, keepdim=True) - return image_features.cpu().numpy().astype('float32')[0] - except Exception as e: - logger.error(f"Failed to process image. Reason: {str(e)}") - return None - - def encode_image_from_url(self, url: str) -> Optional[np.ndarray]: - """Complete pipeline: download, validate, preprocess and encode image from URL""" - try: - # Download image - image_data = self.download_image(url) - - # Validate image - image = self.validate_image(image_data) - - # Preprocess image - image = self.preprocess_image(image) - - # Encode image - embedding = self.encode_image(image) - - return embedding - - except Exception as e: - logger.error(f"Error processing image from URL {url}: {str(e)}") - return None \ No newline at end of file diff --git a/环境相关.md b/环境相关.md deleted file mode 100644 index 841674a..0000000 --- a/环境相关.md +++ /dev/null @@ -1,123 +0,0 @@ - - - -## 2. Python 运行环境 - -```bash -# 1. 激活 Conda -source /home/tw/miniconda3/etc/profile.d/conda.sh -conda activate searchengine - -# 如果部署到新机器,不存在 searchengine 环境时,需要初始化环境: -cd /home/tw/SearchEngine -pip install -r requirements.txt -``` - ---- - -## 3. 外部服务与端口 - -| 服务 | 默认地址 | 说明 | -|------|----------|------| -| Elasticsearch | `http://localhost:9200` | 可通过 Docker 单节点启动 | -| MySQL | `120.79.247.228:3316` | 存放店匠 SPU/SKU 数据 | -| Redis(可选) | `localhost:6479` | Embedding/翻译缓存 | - -示例:使用 Docker 启动 Elasticsearch - -```bash -docker run -d \ - --name elasticsearch \ - -p 9200:9200 \ - -e "discovery.type=single-node" \ - -e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \ - elasticsearch:8.11.0 -``` - ---- - -## 4. 环境变量与 `.env` 模板 - -在项目根目录创建 `.env`,并根据环境替换敏感信息: - -```env -# MySQL -DB_HOST=120.79.247.228 -DB_PORT=3316 -DB_DATABASE=saas -DB_USERNAME=saas -DB_PASSWORD=P89cZHS5d7dFyc9R - -# Elasticsearch -ES_HOST=http://localhost:9200 -ES_USERNAME=essa -ES_PASSWORD=4hOaLaf41y2VuI8y - -# Redis(可选) -REDIS_HOST=localhost -REDIS_PORT=6479 -REDIS_PASSWORD=BMfv5aI31kgHWtlx - -# DeepL 翻译 -DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed - -# API -API_HOST=0.0.0.0 -API_PORT=6002 -``` - ---- - -## 5. 服务凭证速查 - -| 项目 | 值 | -|------|----| -| **MySQL** | host `120.79.247.228`, port `3316`, user `saas`, password `P89cZHS5d7dFyc9R` | -| **Elasticsearch** | host `http://localhost:9200`, user `essa`, password `4hOaLaf41y2VuI8y` | -| **Redis(可选)** | host `localhost`, port `6479`, password `BMfv5aI31kgHWtlx` | -| **DeepL** | `c9293ab4-ad25-479b-919f-ab4e63b429ed` | - -> 所有凭证仅用于本地/测试环境,生产环境需替换并妥善保管。 - ---- - -## 6. 店匠数据源说明 - -SearchEngine 以 MySQL 中的店匠标准表为权威数据源: - -- `shoplazza_product_spu`:SPU 商品主表 -- `shoplazza_product_sku`:SKU 变体表 - -### `shoplazza_product_sku` 字段节选 - -| 字段 | 类型 | 描述 | -|------|------|------| -| `id` | bigint(20) | SKU 主键 | -| `spu_id` | bigint(20) | 对应 SPU | -| `shop_id` | bigint(20) | 店铺 ID | -| `shoplazza_product_id` | varchar(64) | 店匠商品 ID | -| `title` | varchar(500) | 变体标题 | -| `sku` | varchar(100) | SKU 编码 | -| `price` | decimal(10,2) | 售价 | -| `compare_at_price` | decimal(10,2) | 原价 | -| `option1/2/3` | varchar(255) | 颜色/尺码等选项 | -| `inventory_quantity` | int(11) | 库存 | -| `image_src` | varchar(500) | 图片 | -| `tenant_id` | bigint(20) | 租户 | -| `create_time` | datetime | 创建时间 | -| `update_time` | datetime | 更新时间 | -| `deleted` | bit(1) | 逻辑删除标记 | - -> 完整字段、索引映射与 ES 对应关系详见 `INDEX_FIELDS_DOCUMENTATION.md`。 - ---- - -## 7. 相关脚本 - -- `scripts/mock_data.sh`:一次性生成 Tenant1 Mock + Tenant2 CSV 数据并导入 MySQL -- `scripts/ingest.sh [recreate]`:从 MySQL 写入 Elasticsearch -- `run.sh` / `restart.sh`:服务启动/重启 - -更多脚本参数、日志与验证命令参见 `USAGE_GUIDE.md` 与 `TEST_DATA_GUIDE.md`。 - - diff --git a/设计文档.md b/设计文档.md deleted file mode 100644 index df25f92..0000000 --- a/设计文档.md +++ /dev/null @@ -1,724 +0,0 @@ -# 搜索引擎通用化开发进度 - -## 项目概述 - -对后端搜索技术 做通用化。 -通用化的本质 是 对于各种业务数据、各种检索需求,都可以 用少量定制+配置化 来实现效果。 - - -**通用化的本质**:对于各种业务数据、各种检索需求,都可以用少量定制+配置化来实现效果。 - ---- - -## 1. 原始数据层的约定 - -### 1.1 店匠主表 - -所有租户共用以下主表: -- `shoplazza_product_sku` - SKU级别商品数据 -- `shoplazza_product_spu` - SPU级别商品数据 - -### 1.2 索引结构(SPU维度) - -**统一索引架构**: -- 所有客户共享同一个Elasticsearch索引:`search_products` -- 索引粒度:SPU级别(每个文档代表一个SPU) -- 数据隔离:通过`tenant_id`字段实现租户隔离 -- 嵌套结构:每个SPU文档包含嵌套的`variants`数组(SKU变体) - -**索引文档结构**: -```json -{ - "tenant_id": "1", - "product_id": "123", - "title": "蓝牙耳机", - "variants": [ - { - "variant_id": "456", - "title": "黑色", - "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 - } - ], - "min_price": 199.99, - "max_price": 299.99 -} -``` - -### 1.3 配置化方案 - -**配置分离原则**: -- **搜索配置**:只包含ES字段定义、查询域、排序规则等搜索相关配置 -- **数据源配置**:不在搜索配置中,由Pipeline层(脚本)决定 -- **数据导入流程**:写死的脚本,不依赖配置 - -统一通过配置文件定义: -1. ES 字段定义(字段类型、分析器、boost等) -2. ES mapping 结构生成 -3. 查询域配置(indexes) -4. 排序和打分配置(function_score) - -**注意**:配置中**不包含**以下内容: -- `mysql_config` - MySQL数据库配置 -- `main_table` / `extension_table` - 数据表配置 -- `source_table` / `source_column` - 字段数据源映射 - ---- - -## 2. 配置系统实现 - -### 2.1 应用结构配置(字段定义) - -**配置文件位置**:`config/schema/{tenant_id}_config.yaml` - -**配置内容**:定义了 ES 的输入数据有哪些字段、关联 MySQL 的哪些字段。 - -**实现情况**: - -#### 字段类型支持 -- **TEXT**:文本字段,支持多语言分析器 -- **KEYWORD**:关键词字段,用于精确匹配和聚合 -- **TEXT_EMBEDDING**:文本向量字段(1024维,dot_product相似度) -- **IMAGE_EMBEDDING**:图片向量字段(1024维,dot_product相似度) -- **INT/LONG**:整数类型 -- **FLOAT/DOUBLE**:浮点数类型 -- **DATE**:日期类型 -- **BOOLEAN**:布尔类型 - -#### 分析器支持 -- **chinese_ecommerce**:中文电商分词器(index_ansj/query_ansj) -- **english**:英文分析器 -- **russian**:俄文分析器 -- **arabic**:阿拉伯文分析器 -- **spanish**:西班牙文分析器 -- **japanese**:日文分析器 -- **standard**:标准分析器 -- **keyword**:关键词分析器 - -#### 字段配置示例(Base配置) - -```yaml -fields: - # 租户隔离字段(必需) - - name: "tenant_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 商品标识字段 - - name: "product_id" - type: "KEYWORD" - required: true - index: true - store: true - - # 文本搜索字段 - - name: "title" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 3.0 - index: true - store: true - - - name: "seo_keywords" - type: "TEXT" - analyzer: "chinese_ecommerce" - boost: 2.0 - index: true - store: true - - # 嵌套variants字段 - - name: "variants" - type: "JSON" - nested: true - nested_properties: - variant_id: - type: "keyword" - price: - type: "float" - sku: - type: "keyword" -``` - -**注意**:配置中**不包含**`source_table`和`source_column`,数据源映射由Pipeline层决定。 - -**实现模块**: -- `config/config_loader.py` - 配置加载器 -- `config/field_types.py` - 字段类型定义 -- `indexer/mapping_generator.py` - ES mapping 生成器 -- `indexer/data_transformer.py` - 数据转换器 - -### 2.2 索引结构配置(查询域配置) - -**配置内容**:定义了 ES 的字段索引 mapping 配置,支持各个域的查询,包括默认域的查询。 - -**实现情况**: - -#### 域(Domain)配置 -每个域定义了: -- 域名称(如 `default`, `title`, `category`, `brand`) -- 域标签(中文描述) -- 搜索字段列表 -- 默认分析器 -- 权重(boost) -- **多语言字段映射**(`language_field_mapping`) - -#### 多语言字段映射 - -支持将不同语言的查询路由到对应的字段: - -```yaml -indexes: - - name: "default" - label: "默认索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - - "categoryName" - - "brandName" - analyzer: "chinese_ecommerce" - boost: 1.0 - language_field_mapping: - zh: - - "name" - - "categoryName" - - "brandName" - en: - - "enSpuName" - ru: - - "ruSkuName" - - - name: "title" - label: "标题索引" - fields: - - "name" - - "enSpuName" - - "ruSkuName" - analyzer: "chinese_ecommerce" - boost: 2.0 - language_field_mapping: - zh: - - "name" - en: - - "enSpuName" - ru: - - "ruSkuName" -``` - -**工作原理**: -1. 检测查询语言(中文、英文、俄文等) -2. 如果查询语言在 `language_field_mapping` 中,使用原始查询搜索对应语言的字段 -3. 将查询翻译到其他支持的语言,分别搜索对应语言的字段 -4. 组合多个语言查询的结果,提高召回率 - -**实现模块**: -- `search/multilang_query_builder.py` - 多语言查询构建器 -- `query/query_parser.py` - 查询解析器(支持语言检测和翻译) - ---- - -## 3. 数据导入流程 - -### 3.1 数据源 - -**店匠标准表**(Base配置使用): -- `shoplazza_product_spu` - SPU级别商品数据 -- `shoplazza_product_sku` - SKU级别商品数据 - -**其他客户表**(tenant1等): -- 使用各自的数据源表和扩展表 - -### 3.2 数据导入方式 - -**Pipeline层决定数据源**: -- 数据导入流程是写死的脚本,不依赖配置 -- 配置只关注ES搜索相关的内容 -- 数据源映射逻辑写死在转换器代码中 - -#### Base配置数据导入(店匠通用) - -**脚本**:`scripts/ingest_shoplazza.py` - -**数据流程**: -1. **数据加载**:从MySQL读取`shoplazza_product_spu`和`shoplazza_product_sku`表 -2. **数据转换**(`indexer/spu_transformer.py`): - - 按`spu_id`和`tenant_id`关联SPU和SKU数据 - - 将SKU数据聚合为嵌套的`variants`数组 - - 计算扁平化价格字段(`min_price`, `max_price`, `compare_at_price`) - - 字段映射(写死在代码中,不依赖配置) - - 注入`tenant_id`字段 -3. **索引创建**: - - 根据配置生成ES mapping - - 创建或更新`search_products`索引 -4. **批量入库**: - - 批量写入ES(默认每批500条) - - 错误处理和重试机制 - -**命令行工具**: -```bash -python scripts/ingest_shoplazza.py \ - --db-host localhost \ - --db-port 3306 \ - --db-database saas \ - --db-username root \ - --db-password password \ - --tenant-id "1" \ - --config base \ - --es-host http://localhost:9200 \ - --recreate \ - --batch-size 500 -``` - -#### 其他客户数据导入 - -- 使用各自的数据转换器(如`indexer/data_transformer.py`) -- 数据源映射逻辑写死在各自的转换器中 -- 共享`search_products`索引,通过`tenant_id`隔离 - -**实现模块**: -- `indexer/spu_transformer.py` - SPU数据转换器(Base配置) -- `indexer/data_transformer.py` - 通用数据转换器(其他客户) -- `indexer/bulk_indexer.py` - 批量索引器 -- `scripts/ingest_shoplazza.py` - 店匠数据导入脚本 - ---- - -## 4. QueryParser 实现 - - -### 4.1 查询改写(Query Rewriting) - -配置词典的key是query,value是改写后的查询表达式,比如。比如品牌词 改写为在brand|query OR name|query,类别词、标签词等都可以放进去。纠错、规范化、查询改写等 都可以通过这个词典来配置。 -**实现情况**: - -#### 配置方式 -在 `query_config.rewrite_dictionary` 中配置查询改写规则: - -```yaml -query_config: - enable_query_rewrite: true - rewrite_dictionary: - "芭比": "brand:芭比 OR name:芭比娃娃" - "玩具": "category:玩具" - "消防": "category:消防 OR name:消防" -``` - -#### 功能特性 -- **精确匹配**:查询完全匹配词典 key 时,替换为 value -- **部分匹配**:查询包含词典 key 时,替换该部分 -- **支持布尔表达式**:value 可以是复杂的布尔表达式(AND, OR, 域查询等) - -#### 实现模块 -- `query/query_rewriter.py` - 查询改写器 -- `query/query_parser.py` - 查询解析器(集成改写功能) - -### 4.2 翻译(Translation) - -**实现情况**: - -#### 配置方式 -```yaml -query_config: - supported_languages: - - "zh" - - "en" - - "ru" - default_language: "zh" - enable_translation: true - translation_service: "deepl" - translation_api_key: null # 通过环境变量设置 -``` - -#### 功能特性 -1. **语言检测**:自动检测查询语言 -2. **智能翻译**: - - 如果查询是中文,翻译为英文、俄文 - - 如果查询是英文,翻译为中文、俄文 - - 如果查询是其他语言,翻译为所有支持的语言 -3. **域感知翻译**: - - 如果域有 `language_field_mapping`,只翻译到映射中存在的语言 - - 避免不必要的翻译,提高效率 -4. **翻译缓存**:缓存翻译结果,避免重复调用 API - -#### 工作流程 -``` -查询输入 → 语言检测 → 确定目标语言 → 翻译 → 多语言查询构建 -``` - -#### 实现模块 -- `query/language_detector.py` - 语言检测器 -- `query/translator.py` - 翻译器(DeepL API) -- `query/query_parser.py` - 查询解析器(集成翻译功能) - -### 4.3 文本向量化(Text Embedding) - -如果配置打开了text_embedding查询,并且query 包含了default域的查询,那么要把default域的查询词转向量,后面searcher会用这个向量参与查询。 - -**实现情况**: - -#### 配置方式 -```yaml -query_config: - enable_text_embedding: true -``` - -#### 功能特性 -1. **条件生成**: - - 仅当 `enable_text_embedding=true` 时生成向量 - - 仅对 `default` 域查询生成向量 -2. **向量模型**:BGE-M3 模型(1024维向量) -3. **用途**:用于语义搜索(KNN 检索) - -#### 实现模块 -- `embeddings/bge_encoder.py` - BGE 文本编码器 -- `query/query_parser.py` - 查询解析器(集成向量生成) - ---- - -## 5. Searcher 实现 - -参考opensearch,他们自己定义的一套索引结构配置、支持自定义的一套检索表达式、排序表达式,这是各个客户进行配置化的基础,包括索引结构配置、排序策略配置。 -比如各种业务过滤策略 可以简单的通过表达式满足,比如brand|耐克 AND cate2|xxx。指定字段排序可以通过排序的表达式实现。 - -查询默认在default域,相也会对这个域的查询做一些相关性的重点优化,包括融合语义相关性、多语言相关性(可以基于配置 将查询翻译到指定语言并在对应的语言的字段进行查询)来弥补传统查询分析手段(比如查询改写 纠错 词权重等)的不足,也支持通过配置一些词表转为泛查询模式来优化相关性。 - -### 5.1 布尔表达式解析 - -**实现情况**: - -#### 支持的运算符 -- **AND**:所有项必须匹配 -- **OR**:任意项匹配 -- **RANK**:排序增强(类似 OR 但影响排序) -- **ANDNOT**:排除(第一项匹配,第二项不匹配) -- **()**:括号分组 - -#### 优先级(从高到低) -1. `()` - 括号 -2. `ANDNOT` - 排除 -3. `AND` - 与 -4. `OR` - 或 -5. `RANK` - 排序 - -#### 示例 -``` -laptop AND (gaming OR professional) ANDNOT cheap -``` - -#### 实现模块 -- `search/boolean_parser.py` - 布尔表达式解析器 -- `search/searcher.py` - 搜索器(集成布尔解析) - -### 5.2 多语言搜索 - -**实现情况**: - -#### 工作原理 -1. **查询解析**: - - 提取域(如 `title:查询` → 域=`title`,查询=`查询`) - - 检测查询语言 - - 生成翻译 -2. **多语言查询构建**: - - 如果域有 `language_field_mapping`: - - 使用检测到的语言查询对应字段(boost * 1.5) - - 使用翻译后的查询搜索其他语言字段(boost * 1.0) - - 如果域没有 `language_field_mapping`: - - 使用所有字段进行搜索 -3. **查询组合**: - - 多个语言查询组合为 `should` 子句 - - 提高召回率 - -#### 示例 -``` -查询: "芭比娃娃" -域: default -检测语言: zh - -生成的查询: -- 中文查询 "芭比娃娃" → 搜索 name, categoryName, brandName (boost * 1.5) -- 英文翻译 "Barbie doll" → 搜索 enSpuName (boost * 1.0) -- 俄文翻译 "Кукла Барби" → 搜索 ruSkuName (boost * 1.0) -``` - -#### 实现模块 -- `search/multilang_query_builder.py` - 多语言查询构建器 -- `search/searcher.py` - 搜索器(使用多语言构建器) - -### 5.3 相关性计算(Ranking) - -**实现情况**: - -#### 当前实现 -**公式**:`bm25() + 0.2 * text_embedding_relevance()` - -- **bm25()**:BM25 文本相关性得分 - - 包括多语言打分 - - 内部通过配置翻译为多种语言 - - 分别到对应的字段搜索 - - 中文字段使用中文分词器,英文字段使用英文分词器 -- **text_embedding_relevance()**:文本向量相关性得分(KNN 检索的打分) - - 权重:0.2 - -#### 配置方式 -```yaml -ranking: - expression: "bm25() + 0.2*text_embedding_relevance()" - description: "BM25 text relevance combined with semantic embedding similarity" -``` - -#### 扩展性 -- 支持表达式配置(未来可扩展) -- 支持自定义函数(如 `timeliness()`, `field_value()`) - -#### 实现模块 -- `search/ranking_engine.py` - 排序引擎 -- `search/searcher.py` - 搜索器(集成排序功能) - ---- - -## 6. 已完成功能总结 - -### 6.1 配置系统 -- ✅ 字段定义配置(类型、分析器、来源表/列) -- ✅ 索引域配置(多域查询、多语言映射) -- ✅ 查询配置(改写词典、翻译配置) -- ✅ 排序配置(表达式配置) -- ✅ 配置验证(字段存在性、类型检查、分析器匹配) - -### 6.2 数据索引 -- ✅ 数据转换(字段映射、类型转换) -- ✅ 向量生成(文本向量、图片向量) -- ✅ 向量缓存(避免重复计算) -- ✅ 批量索引(错误处理、重试机制) -- ✅ ES mapping 自动生成 - -### 6.3 查询处理 -- ✅ 查询改写(词典配置) -- ✅ 语言检测 -- ✅ 多语言翻译(DeepL API) -- ✅ 文本向量化(BGE-M3) -- ✅ 域提取(支持 `domain:query` 语法) - -### 6.4 搜索功能 -- ✅ 布尔表达式解析(AND, OR, RANK, ANDNOT, 括号) -- ✅ 多语言查询构建(语言路由、字段映射) -- ✅ 语义搜索(KNN 检索) -- ✅ 相关性排序(BM25 + 向量相似度) -- ✅ 结果聚合(Faceted Search) - -### 6.5 API 服务 -- ✅ RESTful API(FastAPI) -- ✅ 搜索接口(文本搜索、图片搜索) -- ✅ 文档查询接口 -- ✅ 前端界面(HTML + JavaScript) -- ✅ 租户隔离(tenant_id过滤) - -### 6.6 Base配置(店匠通用) -- ✅ SPU级别索引结构 -- ✅ 嵌套variants字段 -- ✅ 统一索引(search_products) -- ✅ 租户隔离(tenant_id) -- ✅ 配置简化(移除MySQL相关配置) - ---- - -## 7. 技术栈 - -- **后端**:Python 3.6+ -- **搜索引擎**:Elasticsearch -- **数据库**:MySQL(Shoplazza) -- **向量模型**:BGE-M3(文本)、CN-CLIP(图片) -- **翻译服务**:DeepL API -- **API 框架**:FastAPI -- **前端**:HTML + JavaScript - ---- - -## 8. API响应格式 - -### 8.1 外部友好格式 - -API返回格式不包含ES内部字段(`_id`, `_score`, `_source`),使用外部友好的格式: - -**响应结构**: -```json -{ - "results": [ - { - "product_id": "123", - "title": "蓝牙耳机", - "variants": [ - { - "variant_id": "456", - "price": 199.99, - "sku": "SKU-123-1", - "stock": 50 - } - ], - "relevance_score": 0.95 - } - ], - "total": 10, - "facets": [...], - "suggestions": [], - "related_searches": [] -} -``` - -**主要变化**: -- 结构化结果(`ProductResult`和`VariantResult`) -- 嵌套variants数组 -- 无ES内部字段 - -### 8.2 租户隔离 - -所有API请求必须提供`tenant_id`: -- 请求头:`X-Tenant-ID: 1` -- 或查询参数:`?tenant_id=1` - -搜索时自动添加`tenant_id`过滤,确保数据隔离。 - -### 8.3 数据接口约定 - -**统一的数据约定格式**:所有API接口使用 Pydantic 模型进行数据验证和序列化。 - -#### 8.3.1 数据流模式 - -系统采用统一的数据流模式,确保数据在各层之间的一致性: - -**数据流转路径**: -``` -API Request (JSON) - ↓ -Pydantic 验证 → 结构化模型(RangeFilter, FacetConfig 等) - ↓ -Searcher(透传) - ↓ -ES Query Builder → model_dump() 转换为字典 - ↓ -ES Query (字典) - ↓ -Elasticsearch -``` - -#### 8.3.2 Facets 配置数据流 - -**输入格式**:`List[Union[str, FacetConfig]]` - -- **简单模式**:字符串列表(字段名),使用默认配置 - ```json - ["categoryName_keyword", "brandName_keyword"] - ``` - -- **高级模式**:FacetConfig 对象列表,支持自定义配置 - ```json - [ - { - "field": "categoryName_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100} - ] - } - ] - ``` - -**数据流**: -1. API 层:接收 `List[Union[str, FacetConfig]]` -2. Searcher 层:透传,不做转换 -3. ES Query Builder:只接受 `str` 或 `FacetConfig`,自动处理两种格式 -4. 输出:转换为 ES 聚合查询 - -#### 8.3.3 Range Filters 数据流 - -**输入格式**:`Dict[str, RangeFilter]` - -**RangeFilter 模型**: -```python -class RangeFilter(BaseModel): - gte: Optional[Union[float, str]] # 大于等于 - gt: Optional[Union[float, str]] # 大于 - lte: Optional[Union[float, str]] # 小于等于 - lt: Optional[Union[float, str]] # 小于 -``` - -**示例**: -```json -{ - "price": {"gte": 50, "lte": 200}, - "created_at": {"gte": "2023-01-01T00:00:00Z"} -} -``` - -**数据流**: -1. API 层:接收 `Dict[str, RangeFilter]`,Pydantic 自动验证 -2. Searcher 层:透传 `Dict[str, RangeFilter]` -3. ES Query Builder:调用 `range_filter.model_dump()` 转换为字典 -4. 输出:ES range 查询(支持数值和日期) - -**特性**: -- 自动验证:确保至少指定一个边界值(gte, gt, lte, lt) -- 类型支持:支持数值(float)和日期时间字符串(ISO 格式) -- 统一约定:所有范围过滤都使用 RangeFilter 模型 - -#### 8.3.4 响应 Facets 数据流 - -**输出格式**:`List[FacetResult]` - -**FacetResult 模型**: -```python -class FacetResult(BaseModel): - field: str # 字段名 - label: str # 显示标签 - type: Literal["terms", "range"] # 分面类型 - values: List[FacetValue] # 分面值列表 - total_count: Optional[int] # 总文档数 -``` - -**数据流**: -1. ES Response:返回聚合结果(字典格式) -2. Searcher 层:构建 `List[FacetResult]` 对象 -3. API 层:直接返回 `List[FacetResult]`(Pydantic 自动序列化为 JSON) - -**优势**: -- 类型安全:使用 Pydantic 模型确保数据结构一致性 -- 自动序列化:模型自动转换为 JSON,无需手动处理 -- 统一约定:所有响应都使用标准化的 Pydantic 模型 - -#### 8.3.5 统一约定的好处 - -1. **类型安全**:使用 Pydantic 模型提供运行时类型检查和验证 -2. **代码一致性**:所有层使用相同的数据模型,减少转换错误 -3. **自动文档**:FastAPI 自动生成 API 文档(基于 Pydantic 模型) -4. **易于维护**:修改数据结构只需更新模型定义 -5. **数据验证**:自动验证输入数据,减少错误处理代码 - -**实现模块**: -- `api/models.py` - 所有 Pydantic 模型定义 -- `api/result_formatter.py` - 结果格式化器(ES 响应 → Pydantic 模型) -- `search/es_query_builder.py` - ES 查询构建器(Pydantic 模型 → ES 查询) - -## 9. 配置文件示例 - -**Base配置**(店匠通用):`config/schema/base/config.yaml` - -**其他客户配置**:`config/schema/tenant1/config.yaml` - ---- - -## 9. 相关文档 - -- `MULTILANG_FEATURE.md` - 多语言功能详细说明 -- `QUICKSTART.md` - 快速开始指南 -- `HighLevelDesign.md` - 高层设计文档 -- `IMPLEMENTATION_SUMMARY.md` - 实现总结 -- `商品数据源入ES配置规范.md` - 数据源配置规范 diff --git a/阿里opensearch电商行业.md b/阿里opensearch电商行业.md deleted file mode 100644 index 2e54e03..0000000 --- a/阿里opensearch电商行业.md +++ /dev/null @@ -1,47 +0,0 @@ -https://help.aliyun.com/zh/open-search/industry-algorithm-edition/e-commerce?spm=a2c4g.11186623.help-menu-29102.d_3_2_1.5a903cfbxOsaHt&scm=20140722.H_99739._.OR_help-T_cn~zh-V_1 - - -## 定义应用结构 -示例如下: -| 字段名称 | 主键 | 字段标签 | 类型 | -|----------------|------|------------|--------------| -| title | | 商品标题 | TEXT | -| text_embedding | | 文本向量 | EMBEDDING | -| image_embedding | | 图片向量 | EMBEDDING | -| category_name | | 类目名称 | TEXT | -| image_url | | | LITERAL_ARRAY| -| description | | 商品描述 | TEXT | -| brand_name | | 品牌名称 | TEXT | -| thumbnail_url | | | LITERAL_ARRAY| -| is_onsale | | | INT | -| url | | | LITERAL | -| brand_id | | | LITERAL | -| series_id | | | LITERAL | -| sold_num | | 商品销量 | INT | -| category_id | | | INT | -| onsale_time | | 上架时间 | INT | -| price | | | DOUBLE | -| series_name | | | TEXT | -| discount_price | | DOUBLE | -| pid | ● | INT | -| sale_price | | DOUBLE | -| act_price | | DOUBLE | - - -## 定义索引结构 - -| 索引名称 | 索引标签 | 包含字段 | 分析方式 | 使用示例 | -| --- | --- | --- | --- | --- | -| default | 默认索引 | category_name, description, brand_name, title, create_by, update_by | 行业 - 电商通用分析 | query=default:“云搜索” | -| category_name | 类目名称索引 | category_name | 行业 - 电商通用分析 | query=category_name:“云搜索” | -| category_id | | category_id | 关键字 | query=category_id:“云搜索” | -| series_name | | series_name | 中文 - 通用分析 | query=series_name:“云搜索” | -| brand_name | | brand_name | 中文 - 通用分析 | query=brand_name:“云搜索” | -| id | | id | 关键字 | query=id:“云搜索” | -| title | 标题索引 | title | 行业 - 电商通用分析 | query=title:“云搜索” | -| seller_id | | seller_id | 关键字 | query=seller_id:“云搜索” | -| brand_id | | brand_id | 关键字 | query=brand_id:“云搜索” | -| series_id | | series_id | 关键字 | query=series_id:“云搜索” | - -上面的只是阿里云的opensearch的例子,我们也要有同样的一套配置,这里支持的“字分析方式” 为ES预先支持的 多种分析器,我们要支持的分析方式参考 @商品数据源入ES配置规范.md - -- libgit2 0.21.2