Commit 37e994bb5b8670a2e32c69f8cfdb9f45ce4b6972
1 parent
9cb7528e
命名修改、代码清理
Showing
16 changed files
with
3 additions
and
3006 deletions
Show diff stats
api/routes/admin.py
| @@ -41,7 +41,7 @@ async def health_check(): | @@ -41,7 +41,7 @@ async def health_check(): | ||
| 41 | @router.get("/config") | 41 | @router.get("/config") |
| 42 | async def get_configuration(): | 42 | async def get_configuration(): |
| 43 | """ | 43 | """ |
| 44 | - Get current customer configuration (sanitized). | 44 | + Get current search configuration (sanitized). |
| 45 | """ | 45 | """ |
| 46 | try: | 46 | try: |
| 47 | from ..app import get_config | 47 | from ..app import get_config |
indexer/bulk_indexer.py
| @@ -211,7 +211,7 @@ class IndexingPipeline: | @@ -211,7 +211,7 @@ class IndexingPipeline: | ||
| 211 | Initialize indexing pipeline. | 211 | Initialize indexing pipeline. |
| 212 | 212 | ||
| 213 | Args: | 213 | Args: |
| 214 | - config: Customer configuration | 214 | + config: Search configuration |
| 215 | es_client: Elasticsearch client | 215 | es_client: Elasticsearch client |
| 216 | data_transformer: Data transformer instance | 216 | data_transformer: Data transformer instance |
| 217 | recreate_index: Whether to recreate index if exists | 217 | recreate_index: Whether to recreate index if exists |
search/multilang_query_builder.py
| @@ -36,7 +36,7 @@ class MultiLanguageQueryBuilder(ESQueryBuilder): | @@ -36,7 +36,7 @@ class MultiLanguageQueryBuilder(ESQueryBuilder): | ||
| 36 | Initialize multi-language query builder. | 36 | Initialize multi-language query builder. |
| 37 | 37 | ||
| 38 | Args: | 38 | Args: |
| 39 | - config: Customer configuration | 39 | + config: Search configuration |
| 40 | index_name: ES index name | 40 | index_name: ES index name |
| 41 | text_embedding_field: Field name for text embeddings | 41 | text_embedding_field: Field name for text embeddings |
| 42 | image_embedding_field: Field name for image embeddings | 42 | image_embedding_field: Field name for image embeddings |
test_all.sh deleted
| @@ -1,155 +0,0 @@ | @@ -1,155 +0,0 @@ | ||
| 1 | -#!/bin/bash | ||
| 2 | - | ||
| 3 | -# Complete test script for SearchEngine | ||
| 4 | -# This script performs full testing including data ingestion and service restart | ||
| 5 | - | ||
| 6 | -cd "$(dirname "$0")" | ||
| 7 | - | ||
| 8 | -GREEN='\033[0;32m' | ||
| 9 | -YELLOW='\033[1;33m' | ||
| 10 | -RED='\033[0;31m' | ||
| 11 | -NC='\033[0m' | ||
| 12 | - | ||
| 13 | -echo -e "${GREEN}========================================${NC}" | ||
| 14 | -echo -e "${GREEN}SearchEngine完整测试脚本${NC}" | ||
| 15 | -echo -e "${GREEN}========================================${NC}" | ||
| 16 | - | ||
| 17 | -# Step 1: Setup environment | ||
| 18 | -echo -e "\n${YELLOW}Step 1/4: 设置环境${NC}" | ||
| 19 | -if [ -f "./setup.sh" ]; then | ||
| 20 | - ./setup.sh | ||
| 21 | - if [ $? -eq 0 ]; then | ||
| 22 | - echo -e "${GREEN}✓ 环境设置完成${NC}" | ||
| 23 | - else | ||
| 24 | - echo -e "${RED}✗ 环境设置失败${NC}" | ||
| 25 | - exit 1 | ||
| 26 | - fi | ||
| 27 | -else | ||
| 28 | - echo -e "${YELLOW}⚠ setup脚本不存在,跳过环境设置${NC}" | ||
| 29 | -fi | ||
| 30 | - | ||
| 31 | -# Step 2: Check and ingest data if needed | ||
| 32 | -echo -e "\n${YELLOW}Step 2/4: 检查并准备数据${NC}" | ||
| 33 | -source /home/tw/miniconda3/etc/profile.d/conda.sh | ||
| 34 | -conda activate searchengine | ||
| 35 | - | ||
| 36 | -# Check if index exists | ||
| 37 | -INDEX_EXISTS=$(python -c " | ||
| 38 | -from config.env_config import get_es_config | ||
| 39 | -from utils.es_client import ESClient | ||
| 40 | -from config import ConfigLoader | ||
| 41 | - | ||
| 42 | -try: | ||
| 43 | - es_config = get_es_config() | ||
| 44 | - es_client = ESClient(hosts=[es_config['host']], username=es_config.get('username'), password=es_config.get('password')) | ||
| 45 | - | ||
| 46 | - config_loader = ConfigLoader('config/config.yaml') | ||
| 47 | - config = config_loader.load_config() | ||
| 48 | - | ||
| 49 | - if es_client.index_exists(config.es_index_name): | ||
| 50 | - doc_count = es_client.count(config.es_index_name) | ||
| 51 | - print(f'{doc_count}') | ||
| 52 | - else: | ||
| 53 | - print('0') | ||
| 54 | -except Exception as e: | ||
| 55 | - print(f'0') | ||
| 56 | -" 2>/dev/null || echo "0") | ||
| 57 | - | ||
| 58 | -if [ "$INDEX_EXISTS" = "0" ]; then | ||
| 59 | - echo -e "${YELLOW}索引不存在,开始导入数据...${NC}" | ||
| 60 | - echo -e "${YELLOW}注意: 首次导入会下载模型文件,可能需要10-30分钟${NC}" | ||
| 61 | - echo -e "${YELLOW}导入1000条数据进行快速测试(跳过embedding以加快速度)${NC}" | ||
| 62 | - | ||
| 63 | - if [ -f "./scripts/ingest.sh" ]; then | ||
| 64 | - ./scripts/ingest.sh 1000 true | ||
| 65 | - if [ $? -eq 0 ]; then | ||
| 66 | - echo -e "${GREEN}✓ 数据导入完成${NC}" | ||
| 67 | - else | ||
| 68 | - echo -e "${RED}✗ 数据导入失败${NC}" | ||
| 69 | - exit 1 | ||
| 70 | - fi | ||
| 71 | - else | ||
| 72 | - echo -e "${RED}✗ 数据导入脚本不存在${NC}" | ||
| 73 | - exit 1 | ||
| 74 | - fi | ||
| 75 | -else | ||
| 76 | - echo -e "${GREEN}✓ 数据已存在,包含 $INDEX_EXISTS 条文档${NC}" | ||
| 77 | -fi | ||
| 78 | - | ||
| 79 | -# Step 3: Restart services (stop first, then start) | ||
| 80 | -echo -e "\n${YELLOW}Step 3/4: 重启服务${NC}" | ||
| 81 | -if [ -f "./restart.sh" ]; then | ||
| 82 | - ./restart.sh | ||
| 83 | - if [ $? -eq 0 ]; then | ||
| 84 | - echo -e "${GREEN}✓ 服务重启完成${NC}" | ||
| 85 | - else | ||
| 86 | - echo -e "${RED}✗ 服务重启失败${NC}" | ||
| 87 | - exit 1 | ||
| 88 | - fi | ||
| 89 | -else | ||
| 90 | - echo -e "${RED}✗ 重启脚本不存在${NC}" | ||
| 91 | - exit 1 | ||
| 92 | -fi | ||
| 93 | - | ||
| 94 | -# Step 4: Test the services | ||
| 95 | -echo -e "\n${YELLOW}Step 4/4: 测试服务${NC}" | ||
| 96 | -sleep 3 | ||
| 97 | - | ||
| 98 | -# Test backend health | ||
| 99 | -echo -e "${YELLOW}测试后端服务健康状态...${NC}" | ||
| 100 | -if curl -s http://localhost:6002/admin/health > /dev/null 2>&1; then | ||
| 101 | - echo -e "${GREEN}✓ 后端服务健康检查通过${NC}" | ||
| 102 | -else | ||
| 103 | - echo -e "${YELLOW}⚠ 后端服务健康检查失败,但服务可能仍在启动${NC}" | ||
| 104 | -fi | ||
| 105 | - | ||
| 106 | -# Test frontend | ||
| 107 | -echo -e "${YELLOW}测试前端服务...${NC}" | ||
| 108 | -if curl -s http://localhost:6003/ > /dev/null 2>&1; then | ||
| 109 | - echo -e "${GREEN}✓ 前端服务可访问${NC}" | ||
| 110 | -else | ||
| 111 | - echo -e "${YELLOW}⚠ 前端服务可能还在启动中${NC}" | ||
| 112 | -fi | ||
| 113 | - | ||
| 114 | -# Test API endpoint | ||
| 115 | -echo -e "${YELLOW}测试API端点...${NC}" | ||
| 116 | -API_TEST_RESULT=$(curl -s -X GET "http://localhost:6002/search?q=test&size=5" 2>/dev/null | python -c " | ||
| 117 | -import json | ||
| 118 | -import sys | ||
| 119 | -try: | ||
| 120 | - data = json.load(sys.stdin) | ||
| 121 | - if 'results' in data and len(data['results']) > 0: | ||
| 122 | - print('API_TEST_OK') | ||
| 123 | - else: | ||
| 124 | - print('API_TEST_EMPTY') | ||
| 125 | -except: | ||
| 126 | - print('API_TEST_ERROR') | ||
| 127 | -" 2>/dev/null || echo "API_TEST_ERROR") | ||
| 128 | - | ||
| 129 | -case $API_TEST_RESULT in | ||
| 130 | - "API_TEST_OK") | ||
| 131 | - echo -e "${GREEN}✓ API测试通过,返回搜索结果${NC}" | ||
| 132 | - ;; | ||
| 133 | - "API_TEST_EMPTY") | ||
| 134 | - echo -e "${YELLOW}⚠ API测试通过,但返回空结果${NC}" | ||
| 135 | - ;; | ||
| 136 | - *) | ||
| 137 | - echo -e "${YELLOW}⚠ API测试失败,服务可能还在启动中${NC}" | ||
| 138 | - ;; | ||
| 139 | -esac | ||
| 140 | - | ||
| 141 | -echo -e "${GREEN}========================================${NC}" | ||
| 142 | -echo -e "${GREEN}完整测试流程结束!${NC}" | ||
| 143 | -echo -e "${GREEN}========================================${NC}" | ||
| 144 | -echo "" | ||
| 145 | -echo -e "服务状态:" | ||
| 146 | -echo -e " ${GREEN}前端界面: http://localhost:6003${NC}" | ||
| 147 | -echo -e " ${GREEN}后端API: http://localhost:6002${NC}" | ||
| 148 | -echo -e " ${GREEN}API文档: http://localhost:6002/docs${NC}" | ||
| 149 | -echo "" | ||
| 150 | -echo -e "可用脚本:" | ||
| 151 | -echo -e " ${YELLOW}./run.sh${NC} - 仅启动服务(不导入数据)" | ||
| 152 | -echo -e " ${YELLOW}./stop.sh${NC} - 停止所有服务" | ||
| 153 | -echo -e " ${YELLOW}./restart.sh${NC} - 重启所有服务" | ||
| 154 | -echo -e " ${YELLOW}./test_all.sh${NC}- 完整测试(包含数据导入)" | ||
| 155 | -echo "" | ||
| 156 | \ No newline at end of file | 0 | \ No newline at end of file |
test_data_base.sql deleted
| @@ -1,69 +0,0 @@ | @@ -1,69 +0,0 @@ | ||
| 1 | --- SPU Test Data | ||
| 2 | -INSERT INTO shoplazza_product_spu ( | ||
| 3 | - id, shop_id, shoplazza_id, handle, title, brief, description, spu, | ||
| 4 | - vendor, vendor_url, seo_title, seo_description, seo_keywords, | ||
| 5 | - image_src, image_width, image_height, image_path, image_alt, | ||
| 6 | - inventory_policy, inventory_quantity, inventory_tracking, | ||
| 7 | - published, published_at, requires_shipping, taxable, | ||
| 8 | - fake_sales, display_fake_sales, mixed_wholesale, need_variant_image, | ||
| 9 | - has_only_default_variant, tags, note, category, | ||
| 10 | - shoplazza_created_at, shoplazza_updated_at, tenant_id, | ||
| 11 | - creator, create_time, updater, update_time, deleted | ||
| 12 | -) VALUES | ||
| 13 | -(1, 1, 'spu-1', 'product-1', '音响 海尔', '蓝牙无线音响', '<p>蓝牙无线音响,来自海尔品牌。Bluetooth wireless speaker</p>', '', '海尔', 'https://海尔.com', '音响 海尔 - 服装', '购买海尔音响,蓝牙无线音响', '音响,海尔,服装', '//cdn.example.com/products/1.jpg', 800, 600, 'products/1.jpg', '音响 海尔', '', 0, '0', 1, '2025-10-29 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,海尔,音响', '', '服装', '2025-10-29 12:17:09', '2025-11-05 12:17:09', '1', '1', '2025-10-29 12:17:09', '1', '2025-11-05 12:17:09', 0), | ||
| 14 | -(2, 1, 'spu-2', 'product-2', '无线鼠标 华为', '人体工学无线鼠标', '<p>人体工学无线鼠标,来自华为品牌。Ergonomic wireless mouse</p>', '', '华为', 'https://华为.com', '无线鼠标 华为 - 服装', '购买华为无线鼠标,人体工学无线鼠标', '无线鼠标,华为,服装', '//cdn.example.com/products/2.jpg', 800, 600, 'products/2.jpg', '无线鼠标 华为', '', 0, '0', 1, '2025-09-30 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,华为,无线鼠标', '', '服装', '2025-09-30 12:17:09', '2025-10-01 12:17:09', '1', '1', '2025-09-30 12:17:09', '1', '2025-10-01 12:17:09', 0), | ||
| 15 | -(3, 1, 'spu-3', 'product-3', '显示器 Sony', '4K高清显示器', '<p>4K高清显示器,来自Sony品牌。4K high-definition monitor</p>', '', 'Sony', 'https://sony.com', '显示器 Sony - 服装', '购买Sony显示器,4K高清显示器', '显示器,Sony,服装', '//cdn.example.com/products/3.jpg', 800, 600, 'products/3.jpg', '显示器 Sony', '', 0, '0', 1, '2025-03-30 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,Sony,显示器', '', '服装', '2025-03-30 12:17:09', '2025-04-05 12:17:09', '1', '1', '2025-03-30 12:17:09', '1', '2025-04-05 12:17:09', 0), | ||
| 16 | -(4, 1, 'spu-4', 'product-4', '蓝牙耳机 Apple', '高品质无线蓝牙耳机', '<p>高品质无线蓝牙耳机,来自Apple品牌。High-quality wireless Bluetooth headphone</p>', '', 'Apple', 'https://apple.com', '蓝牙耳机 Apple - 电子产品', '购买Apple蓝牙耳机,高品质无线蓝牙耳机', '蓝牙耳机,Apple,电子产品', '//cdn.example.com/products/4.jpg', 800, 600, 'products/4.jpg', '蓝牙耳机 Apple', '', 0, '0', 1, '2025-05-28 12:17:09', 1, 0, 0, 0, 0, 0, 0, '电子产品,Apple,蓝牙耳机', '', '电子产品', '2025-05-28 12:17:09', '2025-06-08 12:17:09', '1', '1', '2025-05-28 12:17:09', '1', '2025-06-08 12:17:09', 0), | ||
| 17 | -(5, 1, 'spu-5', 'product-5', '智能手表 海尔', '多功能智能手表', '<p>多功能智能手表,来自海尔品牌。Multi-function smart watch</p>', '', '海尔', 'https://海尔.com', '智能手表 海尔 - 图书', '购买海尔智能手表,多功能智能手表', '智能手表,海尔,图书', '//cdn.example.com/products/5.jpg', 800, 600, 'products/5.jpg', '智能手表 海尔', '', 0, '0', 1, '2025-08-13 12:17:09', 1, 0, 0, 0, 0, 0, 0, '图书,海尔,智能手表', '', '图书', '2025-08-13 12:17:09', '2025-08-26 12:17:09', '1', '1', '2025-08-13 12:17:09', '1', '2025-08-26 12:17:09', 0), | ||
| 18 | -(6, 1, 'spu-6', 'product-6', '无线鼠标 Samsung', '人体工学无线鼠标', '<p>人体工学无线鼠标,来自Samsung品牌。Ergonomic wireless mouse</p>', '', 'Samsung', 'https://samsung.com', '无线鼠标 Samsung - 服装', '购买Samsung无线鼠标,人体工学无线鼠标', '无线鼠标,Samsung,服装', '//cdn.example.com/products/6.jpg', 800, 600, 'products/6.jpg', '无线鼠标 Samsung', '', 0, '0', 1, '2025-05-28 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,Samsung,无线鼠标', '', '服装', '2025-05-28 12:17:09', '2025-06-03 12:17:09', '1', '1', '2025-05-28 12:17:09', '1', '2025-06-03 12:17:09', 0), | ||
| 19 | -(7, 1, 'spu-7', 'product-7', '显示器 美的', '4K高清显示器', '<p>4K高清显示器,来自美的品牌。4K high-definition monitor</p>', '', '美的', 'https://美的.com', '显示器 美的 - 电子产品', '购买美的显示器,4K高清显示器', '显示器,美的,电子产品', '//cdn.example.com/products/7.jpg', 800, 600, 'products/7.jpg', '显示器 美的', '', 0, '0', 1, '2025-02-03 12:17:09', 1, 0, 0, 0, 0, 0, 0, '电子产品,美的,显示器', '', '电子产品', '2025-02-03 12:17:09', '2025-02-28 12:17:09', '1', '1', '2025-02-03 12:17:09', '1', '2025-02-28 12:17:09', 0), | ||
| 20 | -(8, 1, 'spu-8', 'product-8', '智能手机 华为', '高性能智能手机', '<p>高性能智能手机,来自华为品牌。High-performance smartphone</p>', '', '华为', 'https://华为.com', '智能手机 华为 - 运动用品', '购买华为智能手机,高性能智能手机', '智能手机,华为,运动用品', '//cdn.example.com/products/8.jpg', 800, 600, 'products/8.jpg', '智能手机 华为', '', 0, '0', 1, '2025-08-21 12:17:09', 1, 0, 0, 0, 0, 0, 0, '运动用品,华为,智能手机', '', '运动用品', '2025-08-21 12:17:09', '2025-09-02 12:17:09', '1', '1', '2025-08-21 12:17:09', '1', '2025-09-02 12:17:09', 0), | ||
| 21 | -(9, 1, 'spu-9', 'product-9', '运动鞋 Apple', '舒适透气的运动鞋', '<p>舒适透气的运动鞋,来自Apple品牌。Comfortable and breathable running shoes</p>', '', 'Apple', 'https://apple.com', '运动鞋 Apple - 图书', '购买Apple运动鞋,舒适透气的运动鞋', '运动鞋,Apple,图书', '//cdn.example.com/products/9.jpg', 800, 600, 'products/9.jpg', '运动鞋 Apple', '', 0, '0', 1, '2025-07-22 12:17:09', 1, 0, 0, 0, 0, 0, 0, '图书,Apple,运动鞋', '', '图书', '2025-07-22 12:17:09', '2025-08-03 12:17:09', '1', '1', '2025-07-22 12:17:09', '1', '2025-08-03 12:17:09', 0), | ||
| 22 | -(10, 1, 'spu-10', 'product-10', '机械键盘 Sony', 'RGB背光机械键盘', '<p>RGB背光机械键盘,来自Sony品牌。RGB backlit mechanical keyboard</p>', '', 'Sony', 'https://sony.com', '机械键盘 Sony - 电子产品', '购买Sony机械键盘,RGB背光机械键盘', '机械键盘,Sony,电子产品', '//cdn.example.com/products/10.jpg', 800, 600, 'products/10.jpg', '机械键盘 Sony', '', 0, '0', 1, '2025-02-24 12:17:09', 1, 0, 0, 0, 0, 0, 0, '电子产品,Sony,机械键盘', '', '电子产品', '2025-02-24 12:17:09', '2025-03-25 12:17:09', '1', '1', '2025-02-24 12:17:09', '1', '2025-03-25 12:17:09', 0); | ||
| 23 | - | ||
| 24 | --- SKU Test Data | ||
| 25 | -INSERT INTO shoplazza_product_sku ( | ||
| 26 | - id, spu_id, shop_id, shoplazza_id, shoplazza_product_id, shoplazza_image_id, | ||
| 27 | - title, sku, barcode, position, price, compare_at_price, cost_price, | ||
| 28 | - option1, option2, option3, inventory_quantity, weight, weight_unit, | ||
| 29 | - image_src, wholesale_price, note, extend, | ||
| 30 | - shoplazza_created_at, shoplazza_updated_at, tenant_id, | ||
| 31 | - creator, create_time, updater, update_time, deleted | ||
| 32 | -) VALUES | ||
| 33 | -(1, 1, 1, 'sku-1', 'spu-1', '', '灰色 / S', 'SKU-1-1', 'BAR00000001', 1, 256.65, 315.84, 153.99, '灰色', 'S', '', 83, 2.19, 'kg', '', '[{"price": 205.32, "minQuantity": 10}]', '', NULL, '2025-02-01 12:17:09', '2025-02-26 12:17:09', '1', '1', '2025-02-01 12:17:09', '1', '2025-02-26 12:17:09', 0), | ||
| 34 | -(2, 1, 1, 'sku-2', 'spu-1', '', '绿色 / XXL', 'SKU-1-2', 'BAR00000002', 2, 274.8, 345.42, 164.88, '绿色', 'XXL', '', 81, 2.73, 'kg', '', '[{"price": 219.84, "minQuantity": 10}]', '', NULL, '2025-09-04 12:17:09', '2025-09-18 12:17:09', '1', '1', '2025-09-04 12:17:09', '1', '2025-09-18 12:17:09', 0), | ||
| 35 | -(3, 1, 1, 'sku-3', 'spu-1', '', '黑色 / XL', 'SKU-1-3', 'BAR00000003', 3, 245.37, 320.02, 147.22, '黑色', 'XL', '', 53, 0.28, 'kg', '', '[{"price": 196.3, "minQuantity": 10}]', '', NULL, '2025-07-20 12:17:09', '2025-07-23 12:17:09', '1', '1', '2025-07-20 12:17:09', '1', '2025-07-23 12:17:09', 0), | ||
| 36 | -(4, 1, 1, 'sku-4', 'spu-1', '', '红色 / M', 'SKU-1-4', 'BAR00000004', 4, 238.24, 332.91, 142.94, '红色', 'M', '', 71, 3.23, 'kg', '', '[{"price": 190.59, "minQuantity": 10}]', '', NULL, '2025-06-30 12:17:09', '2025-07-01 12:17:09', '1', '1', '2025-06-30 12:17:09', '1', '2025-07-01 12:17:09', 0), | ||
| 37 | -(5, 2, 1, 'sku-5', 'spu-2', '', '黑色 / L', 'SKU-2-1', 'BAR00000005', 1, 449.1, 659.64, 269.46, '黑色', 'L', '', 88, 1.54, 'kg', '', '[{"price": 359.28, "minQuantity": 10}]', '', NULL, '2025-07-30 12:17:09', '2025-08-04 12:17:09', '1', '1', '2025-07-30 12:17:09', '1', '2025-08-04 12:17:09', 0), | ||
| 38 | -(6, 2, 1, 'sku-6', 'spu-2', '', '绿色 / M', 'SKU-2-2', 'BAR00000006', 2, 385.8, 510.27, 231.48, '绿色', 'M', '', 90, 2.78, 'kg', '', '[{"price": 308.64, "minQuantity": 10}]', '', NULL, '2024-12-21 12:17:09', '2024-12-23 12:17:09', '1', '1', '2024-12-21 12:17:09', '1', '2024-12-23 12:17:09', 0), | ||
| 39 | -(7, 2, 1, 'sku-7', 'spu-2', '', '白色 / XXL', 'SKU-2-3', 'BAR00000007', 3, 444.82, 652.28, 266.89, '白色', 'XXL', '', 4, 1.1, 'kg', '', '[{"price": 355.86, "minQuantity": 10}]', '', NULL, '2025-07-23 12:17:09', '2025-07-25 12:17:09', '1', '1', '2025-07-23 12:17:09', '1', '2025-07-25 12:17:09', 0), | ||
| 40 | -(8, 2, 1, 'sku-8', 'spu-2', '', '蓝色 / M', 'SKU-2-4', 'BAR00000008', 4, 412.17, 574.41, 247.3, '蓝色', 'M', '', 90, 4.34, 'kg', '', '[{"price": 329.73, "minQuantity": 10}]', '', NULL, '2025-04-01 12:17:09', '2025-04-15 12:17:09', '1', '1', '2025-04-01 12:17:09', '1', '2025-04-15 12:17:09', 0), | ||
| 41 | -(9, 3, 1, 'sku-9', 'spu-3', '', '白色 / S', 'SKU-3-1', 'BAR00000009', 1, 424.04, 542.91, 254.42, '白色', 'S', '', 75, 1.6, 'kg', '', '[{"price": 339.23, "minQuantity": 10}]', '', NULL, '2025-07-08 12:17:09', '2025-07-24 12:17:09', '1', '1', '2025-07-08 12:17:09', '1', '2025-07-24 12:17:09', 0), | ||
| 42 | -(10, 3, 1, 'sku-10', 'spu-3', '', '蓝色 / XXL', 'SKU-3-2', 'BAR00000010', 2, 446.94, 555.31, 268.16, '蓝色', 'XXL', '', 36, 4.5, 'kg', '', '[{"price": 357.55, "minQuantity": 10}]', '', NULL, '2025-06-20 12:17:09', '2025-06-22 12:17:09', '1', '1', '2025-06-20 12:17:09', '1', '2025-06-22 12:17:09', 0), | ||
| 43 | -(11, 3, 1, 'sku-11', 'spu-3', '', '灰色 / S', 'SKU-3-3', 'BAR00000011', 3, 423.72, 606.22, 254.23, '灰色', 'S', '', 77, 3.42, 'kg', '', '[{"price": 338.97, "minQuantity": 10}]', '', NULL, '2025-09-17 12:17:09', '2025-09-21 12:17:09', '1', '1', '2025-09-17 12:17:09', '1', '2025-09-21 12:17:09', 0), | ||
| 44 | -(12, 3, 1, 'sku-12', 'spu-3', '', '灰色 / S', 'SKU-3-4', 'BAR00000012', 4, 416.76, 525.39, 250.06, '灰色', 'S', '', 79, 4.83, 'kg', '', '[{"price": 333.41, "minQuantity": 10}]', '', NULL, '2025-07-26 12:17:09', '2025-08-10 12:17:09', '1', '1', '2025-07-26 12:17:09', '1', '2025-08-10 12:17:09', 0), | ||
| 45 | -(13, 4, 1, 'sku-13', 'spu-4', '', '灰色 / M', 'SKU-4-1', 'BAR00000013', 1, 452.46, 549.77, 271.48, '灰色', 'M', '', 16, 1.68, 'kg', '', '[{"price": 361.97, "minQuantity": 10}]', '', NULL, '2025-10-26 12:17:09', '2025-11-05 12:17:09', '1', '1', '2025-10-26 12:17:09', '1', '2025-11-05 12:17:09', 0), | ||
| 46 | -(14, 4, 1, 'sku-14', 'spu-4', '', '绿色 / L', 'SKU-4-2', 'BAR00000014', 2, 425.48, 514.03, 255.29, '绿色', 'L', '', 24, 3.86, 'kg', '', '[{"price": 340.38, "minQuantity": 10}]', '', NULL, '2025-07-10 12:17:09', '2025-07-27 12:17:09', '1', '1', '2025-07-10 12:17:09', '1', '2025-07-27 12:17:09', 0), | ||
| 47 | -(15, 4, 1, 'sku-15', 'spu-4', '', '黑色 / S', 'SKU-4-3', 'BAR00000015', 3, 454.51, 652.31, 272.71, '黑色', 'S', '', 50, 0.15, 'kg', '', '[{"price": 363.61, "minQuantity": 10}]', '', NULL, '2025-05-15 12:17:09', '2025-05-21 12:17:09', '1', '1', '2025-05-15 12:17:09', '1', '2025-05-21 12:17:09', 0), | ||
| 48 | -(16, 4, 1, 'sku-16', 'spu-4', '', '蓝色 / S', 'SKU-4-4', 'BAR00000016', 4, 428.14, 613.36, 256.88, '蓝色', 'S', '', 36, 4.19, 'kg', '', '[{"price": 342.51, "minQuantity": 10}]', '', NULL, '2025-09-24 12:17:09', '2025-10-15 12:17:09', '1', '1', '2025-09-24 12:17:09', '1', '2025-10-15 12:17:09', 0), | ||
| 49 | -(17, 5, 1, 'sku-17', 'spu-5', '', '白色 / L', 'SKU-5-1', 'BAR00000017', 1, 250.44, 304.72, 150.26, '白色', 'L', '', 82, 3.73, 'kg', '', '[{"price": 200.35, "minQuantity": 10}]', '', NULL, '2025-05-19 12:17:09', '2025-05-29 12:17:09', '1', '1', '2025-05-19 12:17:09', '1', '2025-05-29 12:17:09', 0), | ||
| 50 | -(18, 5, 1, 'sku-18', 'spu-5', '', '绿色 / S', 'SKU-5-2', 'BAR00000018', 2, 276.79, 404.72, 166.07, '绿色', 'S', '', 81, 2.9, 'kg', '', '[{"price": 221.43, "minQuantity": 10}]', '', NULL, '2025-05-05 12:17:09', '2025-05-16 12:17:09', '1', '1', '2025-05-05 12:17:09', '1', '2025-05-16 12:17:09', 0), | ||
| 51 | -(19, 5, 1, 'sku-19', 'spu-5', '', '蓝色 / XXL', 'SKU-5-3', 'BAR00000019', 3, 238.73, 336.81, 143.24, '蓝色', 'XXL', '', 8, 0.62, 'kg', '', '[{"price": 190.99, "minQuantity": 10}]', '', NULL, '2025-06-21 12:17:09', '2025-06-29 12:17:09', '1', '1', '2025-06-21 12:17:09', '1', '2025-06-29 12:17:09', 0), | ||
| 52 | -(20, 5, 1, 'sku-20', 'spu-5', '', '蓝色 / L', 'SKU-5-4', 'BAR00000020', 4, 279.88, 413.66, 167.93, '蓝色', 'L', '', 42, 2.78, 'kg', '', '[{"price": 223.9, "minQuantity": 10}]', '', NULL, '2025-09-11 12:17:09', '2025-10-09 12:17:09', '1', '1', '2025-09-11 12:17:09', '1', '2025-10-09 12:17:09', 0), | ||
| 53 | -(21, 6, 1, 'sku-21', 'spu-6', '', '蓝色 / L', 'SKU-6-1', 'BAR00000021', 1, 527.6, 710.02, 316.56, '蓝色', 'L', '', 32, 4.36, 'kg', '', '[{"price": 422.08, "minQuantity": 10}]', '', NULL, '2024-11-29 12:17:09', '2024-12-18 12:17:09', '1', '1', '2024-11-29 12:17:09', '1', '2024-12-18 12:17:09', 0), | ||
| 54 | -(22, 6, 1, 'sku-22', 'spu-6', '', '白色 / L', 'SKU-6-2', 'BAR00000022', 2, 516.8, 770.98, 310.08, '白色', 'L', '', 69, 1.83, 'kg', '', '[{"price": 413.44, "minQuantity": 10}]', '', NULL, '2025-05-22 12:17:09', '2025-06-12 12:17:09', '1', '1', '2025-05-22 12:17:09', '1', '2025-06-12 12:17:09', 0), | ||
| 55 | -(23, 6, 1, 'sku-23', 'spu-6', '', '红色 / S', 'SKU-6-3', 'BAR00000023', 3, 485.59, 598.04, 291.36, '红色', 'S', '', 79, 3.85, 'kg', '', '[{"price": 388.47, "minQuantity": 10}]', '', NULL, '2024-11-24 12:17:09', '2024-12-17 12:17:09', '1', '1', '2024-11-24 12:17:09', '1', '2024-12-17 12:17:09', 0), | ||
| 56 | -(24, 7, 1, 'sku-24', 'spu-7', '', '蓝色 / XXL', 'SKU-7-1', 'BAR00000024', 1, 161.95, 231.09, 97.17, '蓝色', 'XXL', '', 49, 4.62, 'kg', '', '[{"price": 129.56, "minQuantity": 10}]', '', NULL, '2025-04-20 12:17:09', '2025-04-24 12:17:09', '1', '1', '2025-04-20 12:17:09', '1', '2025-04-24 12:17:09', 0), | ||
| 57 | -(25, 7, 1, 'sku-25', 'spu-7', '', '黑色 / S', 'SKU-7-2', 'BAR00000025', 2, 148.66, 211.66, 89.2, '黑色', 'S', '', 20, 1.5, 'kg', '', '[{"price": 118.93, "minQuantity": 10}]', '', NULL, '2025-04-28 12:17:09', '2025-05-16 12:17:09', '1', '1', '2025-04-28 12:17:09', '1', '2025-05-16 12:17:09', 0), | ||
| 58 | -(26, 7, 1, 'sku-26', 'spu-7', '', '黑色 / XXL', 'SKU-7-3', 'BAR00000026', 3, 173.53, 213.88, 104.12, '黑色', 'XXL', '', 2, 4.43, 'kg', '', '[{"price": 138.82, "minQuantity": 10}]', '', NULL, '2024-12-16 12:17:09', '2024-12-17 12:17:09', '1', '1', '2024-12-16 12:17:09', '1', '2024-12-17 12:17:09', 0), | ||
| 59 | -(27, 7, 1, 'sku-27', 'spu-7', '', '黑色 / S', 'SKU-7-4', 'BAR00000027', 4, 177.7, 233.07, 106.62, '黑色', 'S', '', 73, 2.65, 'kg', '', '[{"price": 142.16, "minQuantity": 10}]', '', NULL, '2025-08-29 12:17:09', '2025-09-23 12:17:09', '1', '1', '2025-08-29 12:17:09', '1', '2025-09-23 12:17:09', 0), | ||
| 60 | -(28, 8, 1, 'sku-28', 'spu-8', '', '白色 / XL', 'SKU-8-1', 'BAR00000028', 1, 471.42, 690.0, 282.85, '白色', 'XL', '', 72, 1.76, 'kg', '', '[{"price": 377.13, "minQuantity": 10}]', '', NULL, '2025-01-31 12:17:09', '2025-02-17 12:17:09', '1', '1', '2025-01-31 12:17:09', '1', '2025-02-17 12:17:09', 0), | ||
| 61 | -(29, 8, 1, 'sku-29', 'spu-8', '', '黑色 / S', 'SKU-8-2', 'BAR00000029', 2, 445.7, 585.74, 267.42, '黑色', 'S', '', 62, 0.59, 'kg', '', '[{"price": 356.56, "minQuantity": 10}]', '', NULL, '2025-04-05 12:17:09', '2025-04-25 12:17:09', '1', '1', '2025-04-05 12:17:09', '1', '2025-04-25 12:17:09', 0), | ||
| 62 | -(30, 8, 1, 'sku-30', 'spu-8', '', '灰色 / L', 'SKU-8-3', 'BAR00000030', 3, 477.89, 605.71, 286.74, '灰色', 'L', '', 1, 2.19, 'kg', '', '[{"price": 382.31, "minQuantity": 10}]', '', NULL, '2025-09-19 12:17:09', '2025-10-06 12:17:09', '1', '1', '2025-09-19 12:17:09', '1', '2025-10-06 12:17:09', 0), | ||
| 63 | -(31, 9, 1, 'sku-31', 'spu-9', '', '红色 / XL', 'SKU-9-1', 'BAR00000031', 1, 432.85, 526.5, 259.71, '红色', 'XL', '', 44, 1.11, 'kg', '', '[{"price": 346.28, "minQuantity": 10}]', '', NULL, '2024-12-13 12:17:09', '2024-12-15 12:17:09', '1', '1', '2024-12-13 12:17:09', '1', '2024-12-15 12:17:09', 0), | ||
| 64 | -(32, 9, 1, 'sku-32', 'spu-9', '', '红色 / XXL', 'SKU-9-2', 'BAR00000032', 2, 448.02, 597.6, 268.81, '红色', 'XXL', '', 18, 4.56, 'kg', '', '[{"price": 358.42, "minQuantity": 10}]', '', NULL, '2025-08-19 12:17:09', '2025-09-17 12:17:09', '1', '1', '2025-08-19 12:17:09', '1', '2025-09-17 12:17:09', 0), | ||
| 65 | -(33, 9, 1, 'sku-33', 'spu-9', '', '黑色 / XXL', 'SKU-9-3', 'BAR00000033', 3, 423.8, 631.05, 254.28, '黑色', 'XXL', '', 21, 5.0, 'kg', '', '[{"price": 339.04, "minQuantity": 10}]', '', NULL, '2025-05-29 12:17:09', '2025-06-05 12:17:09', '1', '1', '2025-05-29 12:17:09', '1', '2025-06-05 12:17:09', 0), | ||
| 66 | -(34, 9, 1, 'sku-34', 'spu-9', '', '灰色 / XXL', 'SKU-9-4', 'BAR00000034', 4, 424.56, 557.45, 254.73, '灰色', 'XXL', '', 70, 0.17, 'kg', '', '[{"price": 339.64, "minQuantity": 10}]', '', NULL, '2025-05-30 12:17:09', '2025-06-11 12:17:09', '1', '1', '2025-05-30 12:17:09', '1', '2025-06-11 12:17:09', 0), | ||
| 67 | -(35, 9, 1, 'sku-35', 'spu-9', '', '绿色 / XXL', 'SKU-9-5', 'BAR00000035', 5, 441.55, 568.31, 264.93, '绿色', 'XXL', '', 44, 1.73, 'kg', '', '[{"price": 353.24, "minQuantity": 10}]', '', NULL, '2025-03-09 12:17:09', '2025-03-15 12:17:09', '1', '1', '2025-03-09 12:17:09', '1', '2025-03-15 12:17:09', 0), | ||
| 68 | -(36, 10, 1, 'sku-36', 'spu-10', '', '绿色', 'SKU-10-1', 'BAR00000036', 1, 99.88, 120.43, 59.93, '绿色', '', '', 98, 1.93, 'kg', '', '[{"price": 79.9, "minQuantity": 10}]', '', NULL, '2024-12-25 12:17:09', '2025-01-09 12:17:09', '1', '1', '2024-12-25 12:17:09', '1', '2025-01-09 12:17:09', 0), | ||
| 69 | -(37, 10, 1, 'sku-37', 'spu-10', '', '蓝色', 'SKU-10-2', 'BAR00000037', 2, 110.96, 140.29, 66.58, '蓝色', '', '', 100, 1.37, 'kg', '', '[{"price": 88.77, "minQuantity": 10}]', '', NULL, '2025-05-10 12:17:09', '2025-05-26 12:17:09', '1', '1', '2025-05-10 12:17:09', '1', '2025-05-26 12:17:09', 0); |
test_new_api.py deleted
| @@ -1,420 +0,0 @@ | @@ -1,420 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | -""" | ||
| 3 | -测试新的 API 接口(v3.0) | ||
| 4 | -验证重构后的过滤器、分面搜索等功能 | ||
| 5 | -""" | ||
| 6 | - | ||
| 7 | -import requests | ||
| 8 | -import json | ||
| 9 | - | ||
| 10 | -API_BASE_URL = 'http://120.76.41.98:6002' | ||
| 11 | - | ||
| 12 | -def print_section(title): | ||
| 13 | - """打印章节标题""" | ||
| 14 | - print("\n" + "="*60) | ||
| 15 | - print(f" {title}") | ||
| 16 | - print("="*60) | ||
| 17 | - | ||
| 18 | -def test_simple_search(): | ||
| 19 | - """测试1:简单搜索""" | ||
| 20 | - print_section("测试1:简单搜索") | ||
| 21 | - | ||
| 22 | - payload = { | ||
| 23 | - "query": "玩具", | ||
| 24 | - "size": 5 | ||
| 25 | - } | ||
| 26 | - | ||
| 27 | - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") | ||
| 28 | - | ||
| 29 | - try: | ||
| 30 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload) | ||
| 31 | - | ||
| 32 | - if response.ok: | ||
| 33 | - data = response.json() | ||
| 34 | - print(f"✓ 成功:找到 {data['total']} 个结果,耗时 {data['took_ms']}ms") | ||
| 35 | - print(f" 响应键:{list(data.keys())}") | ||
| 36 | - print(f" 是否有 facets 字段:{'facets' in data}") | ||
| 37 | - print(f" 是否有 aggregations 字段(应该没有):{'aggregations' in data}") | ||
| 38 | - else: | ||
| 39 | - print(f"✗ 失败:{response.status_code}") | ||
| 40 | - print(f" 错误:{response.text}") | ||
| 41 | - except Exception as e: | ||
| 42 | - print(f"✗ 异常:{e}") | ||
| 43 | - | ||
| 44 | - | ||
| 45 | -def test_range_filters(): | ||
| 46 | - """测试2:范围过滤器""" | ||
| 47 | - print_section("测试2:范围过滤器") | ||
| 48 | - | ||
| 49 | - payload = { | ||
| 50 | - "query": "玩具", | ||
| 51 | - "size": 5, | ||
| 52 | - "range_filters": { | ||
| 53 | - "price": { | ||
| 54 | - "gte": 50, | ||
| 55 | - "lte": 200 | ||
| 56 | - } | ||
| 57 | - } | ||
| 58 | - } | ||
| 59 | - | ||
| 60 | - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") | ||
| 61 | - | ||
| 62 | - try: | ||
| 63 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload) | ||
| 64 | - | ||
| 65 | - if response.ok: | ||
| 66 | - data = response.json() | ||
| 67 | - print(f"✓ 成功:找到 {data['total']} 个结果") | ||
| 68 | - | ||
| 69 | - # 检查价格范围 | ||
| 70 | - print(f"\n 前3个结果的价格:") | ||
| 71 | - for i, hit in enumerate(data['hits'][:3]): | ||
| 72 | - price = hit['_source'].get('price', 'N/A') | ||
| 73 | - print(f" {i+1}. {hit['_source'].get('name', 'N/A')}: ¥{price}") | ||
| 74 | - if isinstance(price, (int, float)) and (price < 50 or price > 200): | ||
| 75 | - print(f" ⚠️ 警告:价格 {price} 不在范围内") | ||
| 76 | - else: | ||
| 77 | - print(f"✗ 失败:{response.status_code}") | ||
| 78 | - print(f" 错误:{response.text}") | ||
| 79 | - except Exception as e: | ||
| 80 | - print(f"✗ 异常:{e}") | ||
| 81 | - | ||
| 82 | - | ||
| 83 | -def test_combined_filters(): | ||
| 84 | - """测试3:组合过滤器""" | ||
| 85 | - print_section("测试3:组合过滤器(精确+范围)") | ||
| 86 | - | ||
| 87 | - payload = { | ||
| 88 | - "query": "玩具", | ||
| 89 | - "size": 5, | ||
| 90 | - "filters": { | ||
| 91 | - "categoryName_keyword": ["玩具"] | ||
| 92 | - }, | ||
| 93 | - "range_filters": { | ||
| 94 | - "price": { | ||
| 95 | - "gte": 50, | ||
| 96 | - "lte": 100 | ||
| 97 | - } | ||
| 98 | - } | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") | ||
| 102 | - | ||
| 103 | - try: | ||
| 104 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload) | ||
| 105 | - | ||
| 106 | - if response.ok: | ||
| 107 | - data = response.json() | ||
| 108 | - print(f"✓ 成功:找到 {data['total']} 个结果") | ||
| 109 | - | ||
| 110 | - print(f"\n 前3个结果:") | ||
| 111 | - for i, hit in enumerate(data['hits'][:3]): | ||
| 112 | - source = hit['_source'] | ||
| 113 | - print(f" {i+1}. {source.get('name', 'N/A')}") | ||
| 114 | - print(f" 类目:{source.get('categoryName', 'N/A')}") | ||
| 115 | - print(f" 价格:¥{source.get('price', 'N/A')}") | ||
| 116 | - else: | ||
| 117 | - print(f"✗ 失败:{response.status_code}") | ||
| 118 | - print(f" 错误:{response.text}") | ||
| 119 | - except Exception as e: | ||
| 120 | - print(f"✗ 异常:{e}") | ||
| 121 | - | ||
| 122 | - | ||
| 123 | -def test_facets_simple(): | ||
| 124 | - """测试4:分面搜索(简单模式)""" | ||
| 125 | - print_section("测试4:分面搜索(简单模式)") | ||
| 126 | - | ||
| 127 | - payload = { | ||
| 128 | - "query": "玩具", | ||
| 129 | - "size": 10, | ||
| 130 | - "facets": ["categoryName_keyword", "brandName_keyword"] | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") | ||
| 134 | - | ||
| 135 | - try: | ||
| 136 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload) | ||
| 137 | - | ||
| 138 | - if response.ok: | ||
| 139 | - data = response.json() | ||
| 140 | - print(f"✓ 成功:找到 {data['total']} 个结果") | ||
| 141 | - | ||
| 142 | - if data.get('facets'): | ||
| 143 | - print(f"\n ✓ 分面结果(标准化格式):") | ||
| 144 | - for facet in data['facets']: | ||
| 145 | - print(f"\n {facet['label']} ({facet['field']}):") | ||
| 146 | - print(f" 类型:{facet['type']}") | ||
| 147 | - print(f" 分面值数量:{len(facet['values'])}") | ||
| 148 | - for value in facet['values'][:3]: | ||
| 149 | - selected_mark = "✓" if value['selected'] else " " | ||
| 150 | - print(f" [{selected_mark}] {value['label']}: {value['count']}") | ||
| 151 | - else: | ||
| 152 | - print(f" ⚠️ 警告:没有返回分面结果") | ||
| 153 | - else: | ||
| 154 | - print(f"✗ 失败:{response.status_code}") | ||
| 155 | - print(f" 错误:{response.text}") | ||
| 156 | - except Exception as e: | ||
| 157 | - print(f"✗ 异常:{e}") | ||
| 158 | - | ||
| 159 | - | ||
| 160 | -def test_facets_advanced(): | ||
| 161 | - """测试5:分面搜索(高级模式)""" | ||
| 162 | - print_section("测试5:分面搜索(高级模式)") | ||
| 163 | - | ||
| 164 | - payload = { | ||
| 165 | - "query": "玩具", | ||
| 166 | - "size": 10, | ||
| 167 | - "facets": [ | ||
| 168 | - { | ||
| 169 | - "field": "categoryName_keyword", | ||
| 170 | - "size": 15, | ||
| 171 | - "type": "terms" | ||
| 172 | - }, | ||
| 173 | - { | ||
| 174 | - "field": "brandName_keyword", | ||
| 175 | - "size": 15, | ||
| 176 | - "type": "terms" | ||
| 177 | - }, | ||
| 178 | - { | ||
| 179 | - "field": "price", | ||
| 180 | - "type": "range", | ||
| 181 | - "ranges": [ | ||
| 182 | - {"key": "0-50", "to": 50}, | ||
| 183 | - {"key": "50-100", "from": 50, "to": 100}, | ||
| 184 | - {"key": "100-200", "from": 100, "to": 200}, | ||
| 185 | - {"key": "200+", "from": 200} | ||
| 186 | - ] | ||
| 187 | - } | ||
| 188 | - ] | ||
| 189 | - } | ||
| 190 | - | ||
| 191 | - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") | ||
| 192 | - | ||
| 193 | - try: | ||
| 194 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload) | ||
| 195 | - | ||
| 196 | - if response.ok: | ||
| 197 | - data = response.json() | ||
| 198 | - print(f"✓ 成功:找到 {data['total']} 个结果") | ||
| 199 | - | ||
| 200 | - if data.get('facets'): | ||
| 201 | - print(f"\n ✓ 分面结果:") | ||
| 202 | - for facet in data['facets']: | ||
| 203 | - print(f"\n {facet['label']} ({facet['type']}):") | ||
| 204 | - for value in facet['values']: | ||
| 205 | - print(f" {value['value']}: {value['count']}") | ||
| 206 | - else: | ||
| 207 | - print(f" ⚠️ 警告:没有返回分面结果") | ||
| 208 | - else: | ||
| 209 | - print(f"✗ 失败:{response.status_code}") | ||
| 210 | - print(f" 错误:{response.text}") | ||
| 211 | - except Exception as e: | ||
| 212 | - print(f"✗ 异常:{e}") | ||
| 213 | - | ||
| 214 | - | ||
| 215 | -def test_complete_scenario(): | ||
| 216 | - """测试6:完整场景(过滤+分面+排序)""" | ||
| 217 | - print_section("测试6:完整场景") | ||
| 218 | - | ||
| 219 | - payload = { | ||
| 220 | - "query": "玩具", | ||
| 221 | - "size": 10, | ||
| 222 | - "filters": { | ||
| 223 | - "categoryName_keyword": ["玩具"] | ||
| 224 | - }, | ||
| 225 | - "range_filters": { | ||
| 226 | - "price": { | ||
| 227 | - "gte": 50, | ||
| 228 | - "lte": 200 | ||
| 229 | - } | ||
| 230 | - }, | ||
| 231 | - "facets": [ | ||
| 232 | - {"field": "brandName_keyword", "size": 10}, | ||
| 233 | - {"field": "supplierName_keyword", "size": 10} | ||
| 234 | - ], | ||
| 235 | - "sort_by": "price", | ||
| 236 | - "sort_order": "asc" | ||
| 237 | - } | ||
| 238 | - | ||
| 239 | - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") | ||
| 240 | - | ||
| 241 | - try: | ||
| 242 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload) | ||
| 243 | - | ||
| 244 | - if response.ok: | ||
| 245 | - data = response.json() | ||
| 246 | - print(f"✓ 成功:找到 {data['total']} 个结果") | ||
| 247 | - | ||
| 248 | - print(f"\n 前5个结果(按价格升序):") | ||
| 249 | - for i, hit in enumerate(data['hits'][:5]): | ||
| 250 | - source = hit['_source'] | ||
| 251 | - print(f" {i+1}. {source.get('name', 'N/A')}: ¥{source.get('price', 'N/A')}") | ||
| 252 | - | ||
| 253 | - if data.get('facets'): | ||
| 254 | - print(f"\n 分面统计:") | ||
| 255 | - for facet in data['facets']: | ||
| 256 | - print(f" {facet['label']}: {len(facet['values'])} 个值") | ||
| 257 | - else: | ||
| 258 | - print(f"✗ 失败:{response.status_code}") | ||
| 259 | - print(f" 错误:{response.text}") | ||
| 260 | - except Exception as e: | ||
| 261 | - print(f"✗ 异常:{e}") | ||
| 262 | - | ||
| 263 | - | ||
| 264 | -def test_search_suggestions(): | ||
| 265 | - """测试7:搜索建议(框架)""" | ||
| 266 | - print_section("测试7:搜索建议(框架)") | ||
| 267 | - | ||
| 268 | - url = f"{API_BASE_URL}/search/suggestions?q=芭&size=5" | ||
| 269 | - print(f"请求:GET {url}") | ||
| 270 | - | ||
| 271 | - try: | ||
| 272 | - response = requests.get(url) | ||
| 273 | - | ||
| 274 | - if response.ok: | ||
| 275 | - data = response.json() | ||
| 276 | - print(f"✓ 成功:返回 {len(data['suggestions'])} 个建议") | ||
| 277 | - print(f" 响应:{json.dumps(data, indent=2, ensure_ascii=False)}") | ||
| 278 | - print(f" ℹ️ 注意:此功能暂未实现,仅返回框架响应") | ||
| 279 | - else: | ||
| 280 | - print(f"✗ 失败:{response.status_code}") | ||
| 281 | - print(f" 错误:{response.text}") | ||
| 282 | - except Exception as e: | ||
| 283 | - print(f"✗ 异常:{e}") | ||
| 284 | - | ||
| 285 | - | ||
| 286 | -def test_instant_search(): | ||
| 287 | - """测试8:即时搜索(框架)""" | ||
| 288 | - print_section("测试8:即时搜索(框架)") | ||
| 289 | - | ||
| 290 | - url = f"{API_BASE_URL}/search/instant?q=玩具&size=5" | ||
| 291 | - print(f"请求:GET {url}") | ||
| 292 | - | ||
| 293 | - try: | ||
| 294 | - response = requests.get(url) | ||
| 295 | - | ||
| 296 | - if response.ok: | ||
| 297 | - data = response.json() | ||
| 298 | - print(f"✓ 成功:找到 {data['total']} 个结果") | ||
| 299 | - print(f" 响应键:{list(data.keys())}") | ||
| 300 | - print(f" ℹ️ 注意:此功能暂未实现,调用标准搜索") | ||
| 301 | - else: | ||
| 302 | - print(f"✗ 失败:{response.status_code}") | ||
| 303 | - print(f" 错误:{response.text}") | ||
| 304 | - except Exception as e: | ||
| 305 | - print(f"✗ 异常:{e}") | ||
| 306 | - | ||
| 307 | - | ||
| 308 | -def test_backward_compatibility(): | ||
| 309 | - """测试9:确认旧接口已移除""" | ||
| 310 | - print_section("测试9:确认旧接口已移除") | ||
| 311 | - | ||
| 312 | - # 测试旧的 price_ranges 参数 | ||
| 313 | - payload_old = { | ||
| 314 | - "query": "玩具", | ||
| 315 | - "filters": { | ||
| 316 | - "price_ranges": ["0-50", "50-100"] # 旧格式 | ||
| 317 | - } | ||
| 318 | - } | ||
| 319 | - | ||
| 320 | - print(f"测试旧的 price_ranges 格式:") | ||
| 321 | - print(f"请求:{json.dumps(payload_old, indent=2, ensure_ascii=False)}") | ||
| 322 | - | ||
| 323 | - try: | ||
| 324 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload_old) | ||
| 325 | - data = response.json() | ||
| 326 | - | ||
| 327 | - # 应该被当作普通过滤器处理(无效果)或报错 | ||
| 328 | - print(f" 状态:{response.status_code}") | ||
| 329 | - print(f" 结果数:{data.get('total', 'N/A')}") | ||
| 330 | - print(f" ℹ️ 旧的 price_ranges 已不再特殊处理") | ||
| 331 | - except Exception as e: | ||
| 332 | - print(f" 异常:{e}") | ||
| 333 | - | ||
| 334 | - # 测试旧的 aggregations 参数 | ||
| 335 | - payload_old_agg = { | ||
| 336 | - "query": "玩具", | ||
| 337 | - "aggregations": { | ||
| 338 | - "category_stats": { | ||
| 339 | - "terms": { | ||
| 340 | - "field": "categoryName_keyword", | ||
| 341 | - "size": 10 | ||
| 342 | - } | ||
| 343 | - } | ||
| 344 | - } | ||
| 345 | - } | ||
| 346 | - | ||
| 347 | - print(f"\n测试旧的 aggregations 格式:") | ||
| 348 | - print(f"请求:{json.dumps(payload_old_agg, indent=2, ensure_ascii=False)}") | ||
| 349 | - | ||
| 350 | - try: | ||
| 351 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload_old_agg) | ||
| 352 | - | ||
| 353 | - if response.ok: | ||
| 354 | - print(f" ⚠️ 警告:请求成功,但 aggregations 参数应该已被移除") | ||
| 355 | - else: | ||
| 356 | - print(f" ✓ 正确:旧参数已不被接受({response.status_code})") | ||
| 357 | - except Exception as e: | ||
| 358 | - print(f" 异常:{e}") | ||
| 359 | - | ||
| 360 | - | ||
| 361 | -def test_validation(): | ||
| 362 | - """测试10:参数验证""" | ||
| 363 | - print_section("测试10:参数验证") | ||
| 364 | - | ||
| 365 | - # 测试空的 range_filter | ||
| 366 | - print("测试空的 range_filter(应该报错):") | ||
| 367 | - payload_invalid = { | ||
| 368 | - "query": "玩具", | ||
| 369 | - "range_filters": { | ||
| 370 | - "price": {} # 空对象 | ||
| 371 | - } | ||
| 372 | - } | ||
| 373 | - | ||
| 374 | - try: | ||
| 375 | - response = requests.post(f"{API_BASE_URL}/search/", json=payload_invalid) | ||
| 376 | - if response.ok: | ||
| 377 | - print(f" ⚠️ 警告:应该验证失败但成功了") | ||
| 378 | - else: | ||
| 379 | - print(f" ✓ 正确:验证失败({response.status_code})") | ||
| 380 | - print(f" 错误信息:{response.json().get('detail', 'N/A')}") | ||
| 381 | - except Exception as e: | ||
| 382 | - print(f" 异常:{e}") | ||
| 383 | - | ||
| 384 | - | ||
| 385 | -def test_summary(): | ||
| 386 | - """测试总结""" | ||
| 387 | - print_section("测试总结") | ||
| 388 | - | ||
| 389 | - print("重构验证清单:") | ||
| 390 | - print(" ✓ 新的 range_filters 参数工作正常") | ||
| 391 | - print(" ✓ 新的 facets 参数工作正常") | ||
| 392 | - print(" ✓ 标准化的 facets 响应格式") | ||
| 393 | - print(" ✓ 旧的 price_ranges 硬编码已移除") | ||
| 394 | - print(" ✓ 旧的 aggregations 参数已移除") | ||
| 395 | - print(" ✓ 新的 /search/suggestions 端点已添加") | ||
| 396 | - print(" ✓ 新的 /search/instant 端点已添加") | ||
| 397 | - print("\n 🎉 API v3.0 重构完成!") | ||
| 398 | - | ||
| 399 | - | ||
| 400 | -if __name__ == "__main__": | ||
| 401 | - print("\n" + "🚀 开始测试新 API(v3.0)") | ||
| 402 | - print(f"API 地址:{API_BASE_URL}\n") | ||
| 403 | - | ||
| 404 | - # 运行所有测试 | ||
| 405 | - test_simple_search() | ||
| 406 | - test_range_filters() | ||
| 407 | - test_combined_filters() | ||
| 408 | - test_facets_simple() | ||
| 409 | - test_facets_advanced() | ||
| 410 | - test_complete_scenario() | ||
| 411 | - test_search_suggestions() | ||
| 412 | - test_instant_search() | ||
| 413 | - test_backward_compatibility() | ||
| 414 | - test_validation() | ||
| 415 | - test_summary() | ||
| 416 | - | ||
| 417 | - print("\n" + "="*60) | ||
| 418 | - print(" 测试完成!") | ||
| 419 | - print("="*60 + "\n") | ||
| 420 | - |
test_search_with_source_fields.py deleted
| @@ -1,147 +0,0 @@ | @@ -1,147 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | -""" | ||
| 3 | -测试实际搜索功能中的source_fields应用 | ||
| 4 | -""" | ||
| 5 | - | ||
| 6 | -import sys | ||
| 7 | -import os | ||
| 8 | -import json | ||
| 9 | -sys.path.append(os.path.dirname(os.path.abspath(__file__))) | ||
| 10 | - | ||
| 11 | -from config import ConfigLoader | ||
| 12 | - | ||
| 13 | -def test_search_query_structure(): | ||
| 14 | - """测试搜索查询是否正确应用了source_fields""" | ||
| 15 | - print("测试搜索查询中的source_fields应用...") | ||
| 16 | - | ||
| 17 | - try: | ||
| 18 | - from search.searcher import Searcher | ||
| 19 | - from utils.es_client import ESClient | ||
| 20 | - | ||
| 21 | - # 加载配置 | ||
| 22 | - config_loader = ConfigLoader("config/schema") | ||
| 23 | - config = config_loader.load_customer_config("customer1") | ||
| 24 | - | ||
| 25 | - print(f"✓ 配置加载成功: {config.customer_id}") | ||
| 26 | - print(f" source_fields配置数量: {len(config.query_config.source_fields)}") | ||
| 27 | - | ||
| 28 | - # 创建ES客户端(使用模拟客户端避免实际连接) | ||
| 29 | - class MockESClient: | ||
| 30 | - def search(self, index_name, body, size=10, from_=0): | ||
| 31 | - print(f"模拟ES搜索 - 索引: {index_name}") | ||
| 32 | - print(f"查询body结构:") | ||
| 33 | - print(json.dumps(body, indent=2, ensure_ascii=False)) | ||
| 34 | - | ||
| 35 | - # 检查_source配置 | ||
| 36 | - if "_source" in body: | ||
| 37 | - print("✓ 查询包含_source配置") | ||
| 38 | - source_config = body["_source"] | ||
| 39 | - if "includes" in source_config: | ||
| 40 | - print(f"✓ source includes字段: {source_config['includes']}") | ||
| 41 | - return { | ||
| 42 | - 'took': 5, | ||
| 43 | - 'hits': { | ||
| 44 | - 'total': {'value': 0}, | ||
| 45 | - 'max_score': 0.0, | ||
| 46 | - 'hits': [] | ||
| 47 | - } | ||
| 48 | - } | ||
| 49 | - else: | ||
| 50 | - print("✗ _source配置中缺少includes") | ||
| 51 | - return None | ||
| 52 | - else: | ||
| 53 | - print("✗ 查询中缺少_source配置") | ||
| 54 | - return None | ||
| 55 | - | ||
| 56 | - def client(self): | ||
| 57 | - return self | ||
| 58 | - | ||
| 59 | - # 创建Searcher实例 | ||
| 60 | - es_client = MockESClient() | ||
| 61 | - searcher = Searcher(config, es_client) | ||
| 62 | - | ||
| 63 | - print("\n测试文本搜索...") | ||
| 64 | - result = searcher.search("test query", size=5) | ||
| 65 | - | ||
| 66 | - if result: | ||
| 67 | - print("✓ 文本搜索测试成功") | ||
| 68 | - else: | ||
| 69 | - print("✗ 文本搜索测试失败") | ||
| 70 | - | ||
| 71 | - print("\n测试图像搜索...") | ||
| 72 | - try: | ||
| 73 | - result = searcher.search_by_image("http://example.com/image.jpg", size=3) | ||
| 74 | - if result: | ||
| 75 | - print("✓ 图像搜索测试成功") | ||
| 76 | - else: | ||
| 77 | - print("✗ 图像搜索测试失败") | ||
| 78 | - except Exception as e: | ||
| 79 | - print(f"✗ 图像搜索测试失败: {e}") | ||
| 80 | - | ||
| 81 | - return True | ||
| 82 | - | ||
| 83 | - except Exception as e: | ||
| 84 | - print(f"✗ 搜索测试失败: {e}") | ||
| 85 | - import traceback | ||
| 86 | - traceback.print_exc() | ||
| 87 | - return False | ||
| 88 | - | ||
| 89 | -def test_es_query_builder_integration(): | ||
| 90 | - """测试ES查询构建器的集成""" | ||
| 91 | - print("\n测试ES查询构建器集成...") | ||
| 92 | - | ||
| 93 | - try: | ||
| 94 | - from search.es_query_builder import ESQueryBuilder | ||
| 95 | - | ||
| 96 | - # 创建构建器,传入空的source_fields列表 | ||
| 97 | - builder = ESQueryBuilder( | ||
| 98 | - index_name="test_index", | ||
| 99 | - match_fields=["title", "content"], | ||
| 100 | - source_fields=None # 测试空配置的情况 | ||
| 101 | - ) | ||
| 102 | - | ||
| 103 | - query = builder.build_query("test query") | ||
| 104 | - | ||
| 105 | - if "_source" not in query: | ||
| 106 | - print("✓ 空source_fields配置下,查询不包含_source过滤") | ||
| 107 | - else: | ||
| 108 | - print("⚠ 空source_fields配置下,查询仍然包含_source过滤") | ||
| 109 | - | ||
| 110 | - # 测试非空配置 | ||
| 111 | - builder2 = ESQueryBuilder( | ||
| 112 | - index_name="test_index", | ||
| 113 | - match_fields=["title", "content"], | ||
| 114 | - source_fields=["id", "title"] | ||
| 115 | - ) | ||
| 116 | - | ||
| 117 | - query2 = builder2.build_query("test query") | ||
| 118 | - | ||
| 119 | - if "_source" in query2 and "includes" in query2["_source"]: | ||
| 120 | - print("✓ 非空source_fields配置下,查询正确包含_source过滤") | ||
| 121 | - else: | ||
| 122 | - print("✗ 非空source_fields配置下,查询缺少_source过滤") | ||
| 123 | - | ||
| 124 | - return True | ||
| 125 | - | ||
| 126 | - except Exception as e: | ||
| 127 | - print(f"✗ 查询构建器集成测试失败: {e}") | ||
| 128 | - return False | ||
| 129 | - | ||
| 130 | -if __name__ == "__main__": | ||
| 131 | - print("=" * 60) | ||
| 132 | - print("搜索功能source_fields应用测试") | ||
| 133 | - print("=" * 60) | ||
| 134 | - | ||
| 135 | - success = True | ||
| 136 | - | ||
| 137 | - # 运行所有测试 | ||
| 138 | - success &= test_es_query_builder_integration() | ||
| 139 | - success &= test_search_query_structure() | ||
| 140 | - | ||
| 141 | - print("\n" + "=" * 60) | ||
| 142 | - if success: | ||
| 143 | - print("✓ 所有测试通过!source_fields在搜索功能中正确应用。") | ||
| 144 | - print("✓ ES现在只返回配置中指定的字段,减少了网络传输和响应大小。") | ||
| 145 | - else: | ||
| 146 | - print("✗ 部分测试失败,请检查实现。") | ||
| 147 | - print("=" * 60) | ||
| 148 | \ No newline at end of file | 0 | \ No newline at end of file |
test_source_fields.py deleted
| @@ -1,132 +0,0 @@ | @@ -1,132 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | -""" | ||
| 3 | -测试ES source_fields配置的脚本 | ||
| 4 | -""" | ||
| 5 | - | ||
| 6 | -import sys | ||
| 7 | -import os | ||
| 8 | -sys.path.append(os.path.dirname(os.path.abspath(__file__))) | ||
| 9 | - | ||
| 10 | -from config import ConfigLoader, SearchConfig | ||
| 11 | - | ||
| 12 | -def test_source_fields_config(): | ||
| 13 | - """测试source_fields配置是否正确加载""" | ||
| 14 | - print("测试ES source_fields配置...") | ||
| 15 | - | ||
| 16 | - # 加载配置 | ||
| 17 | - config_loader = ConfigLoader("config/schema") | ||
| 18 | - | ||
| 19 | - try: | ||
| 20 | - # 加载customer1配置 | ||
| 21 | - config = config_loader.load_customer_config("customer1") | ||
| 22 | - print(f"✓ 成功加载配置: {config.customer_id}") | ||
| 23 | - | ||
| 24 | - # 检查source_fields配置 | ||
| 25 | - source_fields = config.query_config.source_fields | ||
| 26 | - print(f"✓ source_fields配置 ({len(source_fields)}个字段):") | ||
| 27 | - for i, field in enumerate(source_fields, 1): | ||
| 28 | - print(f" {i:2d}. {field}") | ||
| 29 | - | ||
| 30 | - # 检查默认字段列表是否包含预期字段 | ||
| 31 | - expected_fields = ["id", "title", "brandName", "price", "image"] | ||
| 32 | - for field in expected_fields: | ||
| 33 | - if field in source_fields: | ||
| 34 | - print(f"✓ 包含预期字段: {field}") | ||
| 35 | - else: | ||
| 36 | - print(f"⚠ 缺少预期字段: {field}") | ||
| 37 | - | ||
| 38 | - return True | ||
| 39 | - | ||
| 40 | - except Exception as e: | ||
| 41 | - print(f"✗ 配置加载失败: {e}") | ||
| 42 | - return False | ||
| 43 | - | ||
| 44 | -def test_es_query_builder(): | ||
| 45 | - """测试ES查询构建器是否正确应用source_fields""" | ||
| 46 | - print("\n测试ES查询构建器...") | ||
| 47 | - | ||
| 48 | - try: | ||
| 49 | - from search.es_query_builder import ESQueryBuilder | ||
| 50 | - | ||
| 51 | - # 测试基础查询构建器 | ||
| 52 | - builder = ESQueryBuilder( | ||
| 53 | - index_name="test_index", | ||
| 54 | - match_fields=["title", "content"], | ||
| 55 | - source_fields=["id", "title", "price"] | ||
| 56 | - ) | ||
| 57 | - | ||
| 58 | - # 构建查询 | ||
| 59 | - query = builder.build_query("test query") | ||
| 60 | - | ||
| 61 | - print("✓ ES查询构建成功") | ||
| 62 | - print(f"查询结构:") | ||
| 63 | - print(f" size: {query.get('size')}") | ||
| 64 | - print(f" _source: {query.get('_source')}") | ||
| 65 | - | ||
| 66 | - # 检查_source配置 | ||
| 67 | - if "_source" in query: | ||
| 68 | - source_config = query["_source"] | ||
| 69 | - if "includes" in source_config: | ||
| 70 | - print(f"✓ _source includes配置正确: {source_config['includes']}") | ||
| 71 | - else: | ||
| 72 | - print("✗ _source配置中缺少includes字段") | ||
| 73 | - else: | ||
| 74 | - print("✗ 查询中缺少_source配置") | ||
| 75 | - | ||
| 76 | - return True | ||
| 77 | - | ||
| 78 | - except Exception as e: | ||
| 79 | - print(f"✗ ES查询构建器测试失败: {e}") | ||
| 80 | - import traceback | ||
| 81 | - traceback.print_exc() | ||
| 82 | - return False | ||
| 83 | - | ||
| 84 | -def test_multilang_query_builder(): | ||
| 85 | - """测试多语言查询构建器""" | ||
| 86 | - print("\n测试多语言查询构建器...") | ||
| 87 | - | ||
| 88 | - try: | ||
| 89 | - from search.multilang_query_builder import MultiLanguageQueryBuilder | ||
| 90 | - | ||
| 91 | - # 加载配置 | ||
| 92 | - config_loader = ConfigLoader("config/schema") | ||
| 93 | - config = config_loader.load_customer_config("customer1") | ||
| 94 | - | ||
| 95 | - # 创建多语言查询构建器 | ||
| 96 | - builder = MultiLanguageQueryBuilder( | ||
| 97 | - config=config, | ||
| 98 | - index_name=config.es_index_name, | ||
| 99 | - text_embedding_field="text_embedding", | ||
| 100 | - image_embedding_field="image_embedding", | ||
| 101 | - source_fields=config.query_config.source_fields | ||
| 102 | - ) | ||
| 103 | - | ||
| 104 | - print("✓ 多语言查询构建器创建成功") | ||
| 105 | - print(f" source_fields配置: {builder.source_fields}") | ||
| 106 | - | ||
| 107 | - return True | ||
| 108 | - | ||
| 109 | - except Exception as e: | ||
| 110 | - print(f"✗ 多语言查询构建器测试失败: {e}") | ||
| 111 | - import traceback | ||
| 112 | - traceback.print_exc() | ||
| 113 | - return False | ||
| 114 | - | ||
| 115 | -if __name__ == "__main__": | ||
| 116 | - print("=" * 60) | ||
| 117 | - print("ES Source Fields 配置测试") | ||
| 118 | - print("=" * 60) | ||
| 119 | - | ||
| 120 | - success = True | ||
| 121 | - | ||
| 122 | - # 运行所有测试 | ||
| 123 | - success &= test_source_fields_config() | ||
| 124 | - success &= test_es_query_builder() | ||
| 125 | - success &= test_multilang_query_builder() | ||
| 126 | - | ||
| 127 | - print("\n" + "=" * 60) | ||
| 128 | - if success: | ||
| 129 | - print("✓ 所有测试通过!source_fields配置已正确实现。") | ||
| 130 | - else: | ||
| 131 | - print("✗ 部分测试失败,请检查配置和代码。") | ||
| 132 | - print("=" * 60) | ||
| 133 | \ No newline at end of file | 0 | \ No newline at end of file |
tests/integration/test_aggregation_api.py deleted
| @@ -1,256 +0,0 @@ | @@ -1,256 +0,0 @@ | ||
| 1 | -""" | ||
| 2 | -Tests for aggregation API functionality. | ||
| 3 | -""" | ||
| 4 | - | ||
| 5 | -import pytest | ||
| 6 | -from fastapi.testclient import TestClient | ||
| 7 | -from api.app import app | ||
| 8 | - | ||
| 9 | -client = TestClient(app) | ||
| 10 | - | ||
| 11 | - | ||
| 12 | -@pytest.mark.integration | ||
| 13 | -@pytest.mark.api | ||
| 14 | -def test_search_with_aggregations(): | ||
| 15 | - """Test search with dynamic aggregations.""" | ||
| 16 | - request_data = { | ||
| 17 | - "query": "芭比娃娃", | ||
| 18 | - "size": 10, | ||
| 19 | - "aggregations": { | ||
| 20 | - "category_name": { | ||
| 21 | - "type": "terms", | ||
| 22 | - "field": "categoryName_keyword", | ||
| 23 | - "size": 10 | ||
| 24 | - }, | ||
| 25 | - "brand_name": { | ||
| 26 | - "type": "terms", | ||
| 27 | - "field": "brandName_keyword", | ||
| 28 | - "size": 10 | ||
| 29 | - }, | ||
| 30 | - "price_ranges": { | ||
| 31 | - "type": "range", | ||
| 32 | - "field": "price", | ||
| 33 | - "ranges": [ | ||
| 34 | - {"key": "0-50", "to": 50}, | ||
| 35 | - {"key": "50-100", "from": 50, "to": 100}, | ||
| 36 | - {"key": "100-200", "from": 100, "to": 200}, | ||
| 37 | - {"key": "200+", "from": 200} | ||
| 38 | - ] | ||
| 39 | - } | ||
| 40 | - } | ||
| 41 | - } | ||
| 42 | - | ||
| 43 | - response = client.post("/search/", json=request_data) | ||
| 44 | - | ||
| 45 | - assert response.status_code == 200 | ||
| 46 | - data = response.json() | ||
| 47 | - | ||
| 48 | - # Check basic search response structure | ||
| 49 | - assert "hits" in data | ||
| 50 | - assert "total" in data | ||
| 51 | - assert "aggregations" in data | ||
| 52 | - assert "query_info" in data | ||
| 53 | - | ||
| 54 | - # Check aggregations structure | ||
| 55 | - aggregations = data["aggregations"] | ||
| 56 | - | ||
| 57 | - # Should have category aggregations | ||
| 58 | - if "category_name" in aggregations: | ||
| 59 | - assert "buckets" in aggregations["category_name"] | ||
| 60 | - assert isinstance(aggregations["category_name"]["buckets"], list) | ||
| 61 | - | ||
| 62 | - # Should have brand aggregations | ||
| 63 | - if "brand_name" in aggregations: | ||
| 64 | - assert "buckets" in aggregations["brand_name"] | ||
| 65 | - assert isinstance(aggregations["brand_name"]["buckets"], list) | ||
| 66 | - | ||
| 67 | - # Should have price range aggregations | ||
| 68 | - if "price_ranges" in aggregations: | ||
| 69 | - assert "buckets" in aggregations["price_ranges"] | ||
| 70 | - assert isinstance(aggregations["price_ranges"]["buckets"], list) | ||
| 71 | - | ||
| 72 | - | ||
| 73 | -@pytest.mark.integration | ||
| 74 | -@pytest.mark.api | ||
| 75 | -def test_search_with_sorting(): | ||
| 76 | - """Test search with different sorting options.""" | ||
| 77 | - | ||
| 78 | - # Test price ascending | ||
| 79 | - request_data = { | ||
| 80 | - "query": "玩具", | ||
| 81 | - "size": 5, | ||
| 82 | - "sort_by": "price_asc" | ||
| 83 | - } | ||
| 84 | - | ||
| 85 | - response = client.post("/search/", json=request_data) | ||
| 86 | - assert response.status_code == 200 | ||
| 87 | - data = response.json() | ||
| 88 | - | ||
| 89 | - if data["hits"] and len(data["hits"]) > 1: | ||
| 90 | - # Check if results are sorted by price (ascending) | ||
| 91 | - prices = [] | ||
| 92 | - for hit in data["hits"]: | ||
| 93 | - if "_source" in hit and "price" in hit["_source"]: | ||
| 94 | - prices.append(hit["_source"]["price"]) | ||
| 95 | - | ||
| 96 | - if len(prices) > 1: | ||
| 97 | - assert prices == sorted(prices), "Results should be sorted by price ascending" | ||
| 98 | - | ||
| 99 | - # Test price descending | ||
| 100 | - request_data["sort_by"] = "price_desc" | ||
| 101 | - response = client.post("/search/", json=request_data) | ||
| 102 | - assert response.status_code == 200 | ||
| 103 | - data = response.json() | ||
| 104 | - | ||
| 105 | - if data["hits"] and len(data["hits"]) > 1: | ||
| 106 | - prices = [] | ||
| 107 | - for hit in data["hits"]: | ||
| 108 | - if "_source" in hit and "price" in hit["_source"]: | ||
| 109 | - prices.append(hit["_source"]["price"]) | ||
| 110 | - | ||
| 111 | - if len(prices) > 1: | ||
| 112 | - assert prices == sorted(prices, reverse=True), "Results should be sorted by price descending" | ||
| 113 | - | ||
| 114 | - # Test time descending | ||
| 115 | - request_data["sort_by"] = "time_desc" | ||
| 116 | - response = client.post("/search/", json=request_data) | ||
| 117 | - assert response.status_code == 200 | ||
| 118 | - data = response.json() | ||
| 119 | - | ||
| 120 | - if data["hits"] and len(data["hits"]) > 1: | ||
| 121 | - times = [] | ||
| 122 | - for hit in data["hits"]: | ||
| 123 | - if "_source" in hit and "create_time" in hit["_source"]: | ||
| 124 | - times.append(hit["_source"]["create_time"]) | ||
| 125 | - | ||
| 126 | - if len(times) > 1: | ||
| 127 | - # Newer items should come first | ||
| 128 | - assert times == sorted(times, reverse=True), "Results should be sorted by time descending" | ||
| 129 | - | ||
| 130 | - | ||
| 131 | -@pytest.mark.integration | ||
| 132 | -@pytest.mark.api | ||
| 133 | -def test_search_with_filters_and_aggregations(): | ||
| 134 | - """Test search with filters and aggregations together.""" | ||
| 135 | - request_data = { | ||
| 136 | - "query": "玩具", | ||
| 137 | - "size": 10, | ||
| 138 | - "filters": { | ||
| 139 | - "category_name": ["芭比"] | ||
| 140 | - }, | ||
| 141 | - "aggregations": { | ||
| 142 | - "brand_name": { | ||
| 143 | - "type": "terms", | ||
| 144 | - "field": "brandName_keyword", | ||
| 145 | - "size": 10 | ||
| 146 | - } | ||
| 147 | - } | ||
| 148 | - } | ||
| 149 | - | ||
| 150 | - response = client.post("/search/", json=request_data) | ||
| 151 | - assert response.status_code == 200 | ||
| 152 | - data = response.json() | ||
| 153 | - | ||
| 154 | - # Check that results are filtered | ||
| 155 | - assert "hits" in data | ||
| 156 | - for hit in data["hits"]: | ||
| 157 | - if "_source" in hit and "categoryName" in hit["_source"]: | ||
| 158 | - assert "芭比" in hit["_source"]["categoryName"] | ||
| 159 | - | ||
| 160 | - # Check that aggregations are still present | ||
| 161 | - assert "aggregations" in data | ||
| 162 | - | ||
| 163 | - | ||
| 164 | -@pytest.mark.integration | ||
| 165 | -@pytest.mark.api | ||
| 166 | -def test_search_without_aggregations(): | ||
| 167 | - """Test search without aggregations (default behavior).""" | ||
| 168 | - request_data = { | ||
| 169 | - "query": "玩具", | ||
| 170 | - "size": 10 | ||
| 171 | - } | ||
| 172 | - | ||
| 173 | - response = client.post("/search/", json=request_data) | ||
| 174 | - assert response.status_code == 200 | ||
| 175 | - data = response.json() | ||
| 176 | - | ||
| 177 | - # Should still have basic response structure | ||
| 178 | - assert "hits" in data | ||
| 179 | - assert "total" in data | ||
| 180 | - assert "query_info" in data | ||
| 181 | - | ||
| 182 | - # Aggregations might be empty or not present without explicit request | ||
| 183 | - assert "aggregations" in data | ||
| 184 | - | ||
| 185 | - | ||
| 186 | -@pytest.mark.integration | ||
| 187 | -@pytest.mark.api | ||
| 188 | -def test_aggregation_edge_cases(): | ||
| 189 | - """Test aggregation edge cases.""" | ||
| 190 | - | ||
| 191 | - # Test with empty query | ||
| 192 | - request_data = { | ||
| 193 | - "query": "", | ||
| 194 | - "size": 10, | ||
| 195 | - "aggregations": { | ||
| 196 | - "category_name": { | ||
| 197 | - "type": "terms", | ||
| 198 | - "field": "categoryName_keyword", | ||
| 199 | - "size": 10 | ||
| 200 | - } | ||
| 201 | - } | ||
| 202 | - } | ||
| 203 | - | ||
| 204 | - response = client.post("/search/", json=request_data) | ||
| 205 | - # Should handle empty query gracefully | ||
| 206 | - assert response.status_code in [200, 422] | ||
| 207 | - | ||
| 208 | - # Test with invalid aggregation type | ||
| 209 | - request_data = { | ||
| 210 | - "query": "玩具", | ||
| 211 | - "size": 10, | ||
| 212 | - "aggregations": { | ||
| 213 | - "invalid_agg": { | ||
| 214 | - "type": "invalid_type", | ||
| 215 | - "field": "categoryName_keyword", | ||
| 216 | - "size": 10 | ||
| 217 | - } | ||
| 218 | - } | ||
| 219 | - } | ||
| 220 | - | ||
| 221 | - response = client.post("/search/", json=request_data) | ||
| 222 | - # Should handle invalid aggregation type gracefully | ||
| 223 | - assert response.status_code in [200, 422] | ||
| 224 | - | ||
| 225 | - | ||
| 226 | -@pytest.mark.unit | ||
| 227 | -def test_aggregation_spec_validation(): | ||
| 228 | - """Test aggregation specification validation.""" | ||
| 229 | - from api.models import AggregationSpec | ||
| 230 | - | ||
| 231 | - # Test valid aggregation spec | ||
| 232 | - agg_spec = AggregationSpec( | ||
| 233 | - field="categoryName_keyword", | ||
| 234 | - type="terms", | ||
| 235 | - size=10 | ||
| 236 | - ) | ||
| 237 | - assert agg_spec.field == "categoryName_keyword" | ||
| 238 | - assert agg_spec.type == "terms" | ||
| 239 | - assert agg_spec.size == 10 | ||
| 240 | - | ||
| 241 | - # Test range aggregation spec | ||
| 242 | - range_agg = AggregationSpec( | ||
| 243 | - field="price", | ||
| 244 | - type="range", | ||
| 245 | - ranges=[ | ||
| 246 | - {"key": "0-50", "to": 50}, | ||
| 247 | - {"key": "50-100", "from": 50, "to": 100} | ||
| 248 | - ] | ||
| 249 | - ) | ||
| 250 | - assert range_agg.field == "price" | ||
| 251 | - assert range_agg.type == "range" | ||
| 252 | - assert len(range_agg.ranges) == 2 | ||
| 253 | - | ||
| 254 | - | ||
| 255 | -if __name__ == "__main__": | ||
| 256 | - pytest.main([__file__]) | ||
| 257 | \ No newline at end of file | 0 | \ No newline at end of file |
tests/integration/test_api_integration.py deleted
| @@ -1,338 +0,0 @@ | @@ -1,338 +0,0 @@ | ||
| 1 | -""" | ||
| 2 | -API集成测试 | ||
| 3 | - | ||
| 4 | -测试API接口的完整集成,包括请求处理、响应格式、错误处理等 | ||
| 5 | -""" | ||
| 6 | - | ||
| 7 | -import pytest | ||
| 8 | -import json | ||
| 9 | -import asyncio | ||
| 10 | -from unittest.mock import patch, Mock, AsyncMock | ||
| 11 | -from fastapi.testclient import TestClient | ||
| 12 | - | ||
| 13 | -# 导入API应用 | ||
| 14 | -import sys | ||
| 15 | -import os | ||
| 16 | -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) | ||
| 17 | - | ||
| 18 | -from api.app import app | ||
| 19 | - | ||
| 20 | - | ||
| 21 | -@pytest.mark.integration | ||
| 22 | -@pytest.mark.api | ||
| 23 | -class TestAPIIntegration: | ||
| 24 | - """API集成测试""" | ||
| 25 | - | ||
| 26 | - @pytest.fixture | ||
| 27 | - def client(self): | ||
| 28 | - """创建测试客户端""" | ||
| 29 | - return TestClient(app) | ||
| 30 | - | ||
| 31 | - def test_search_api_basic(self, client): | ||
| 32 | - """测试基础搜索API""" | ||
| 33 | - response = client.get("/search", params={"q": "红色连衣裙"}) | ||
| 34 | - | ||
| 35 | - assert response.status_code == 200 | ||
| 36 | - data = response.json() | ||
| 37 | - | ||
| 38 | - # 验证响应结构 | ||
| 39 | - assert "hits" in data | ||
| 40 | - assert "total" in data | ||
| 41 | - assert "max_score" in data | ||
| 42 | - assert "took_ms" in data | ||
| 43 | - assert "query_info" in data | ||
| 44 | - assert "performance_summary" in data | ||
| 45 | - | ||
| 46 | - # 验证hits是列表 | ||
| 47 | - assert isinstance(data["hits"], list) | ||
| 48 | - assert isinstance(data["total"], int) | ||
| 49 | - assert isinstance(data["max_score"], (int, float)) | ||
| 50 | - assert isinstance(data["took_ms"], int) | ||
| 51 | - | ||
| 52 | - def test_search_api_with_parameters(self, client): | ||
| 53 | - """测试带参数的搜索API""" | ||
| 54 | - params = { | ||
| 55 | - "q": "智能手机", | ||
| 56 | - "size": 15, | ||
| 57 | - "from": 5, | ||
| 58 | - "enable_translation": False, | ||
| 59 | - "enable_embedding": False, | ||
| 60 | - "enable_rerank": True, | ||
| 61 | - "min_score": 1.0 | ||
| 62 | - } | ||
| 63 | - | ||
| 64 | - response = client.get("/search", params=params) | ||
| 65 | - | ||
| 66 | - assert response.status_code == 200 | ||
| 67 | - data = response.json() | ||
| 68 | - | ||
| 69 | - # 验证参数被正确传递 | ||
| 70 | - performance = data.get("performance_summary", {}) | ||
| 71 | - metadata = performance.get("metadata", {}) | ||
| 72 | - search_params = metadata.get("search_params", {}) | ||
| 73 | - | ||
| 74 | - assert search_params.get("size") == 15 | ||
| 75 | - assert search_params.get("from") == 5 | ||
| 76 | - assert search_params.get("min_score") == 1.0 | ||
| 77 | - | ||
| 78 | - feature_flags = metadata.get("feature_flags", {}) | ||
| 79 | - assert feature_flags.get("enable_translation") is False | ||
| 80 | - assert feature_flags.get("enable_embedding") is False | ||
| 81 | - assert feature_flags.get("enable_rerank") is True | ||
| 82 | - | ||
| 83 | - def test_search_api_complex_query(self, client): | ||
| 84 | - """测试复杂查询API""" | ||
| 85 | - response = client.get("/search", params={"q": "手机 AND (华为 OR 苹果) ANDNOT 二手"}) | ||
| 86 | - | ||
| 87 | - assert response.status_code == 200 | ||
| 88 | - data = response.json() | ||
| 89 | - | ||
| 90 | - # 验证复杂查询被处理 | ||
| 91 | - query_info = data.get("query_info", {}) | ||
| 92 | - performance = data.get("performance_summary", {}) | ||
| 93 | - query_analysis = performance.get("query_analysis", {}) | ||
| 94 | - | ||
| 95 | - # 对于复杂查询,is_simple_query应该是False | ||
| 96 | - assert query_analysis.get("is_simple_query") is False | ||
| 97 | - | ||
| 98 | - def test_search_api_missing_query(self, client): | ||
| 99 | - """测试缺少查询参数的API""" | ||
| 100 | - response = client.get("/search") | ||
| 101 | - | ||
| 102 | - assert response.status_code == 422 # Validation error | ||
| 103 | - data = response.json() | ||
| 104 | - | ||
| 105 | - # 验证错误信息 | ||
| 106 | - assert "detail" in data | ||
| 107 | - | ||
| 108 | - def test_search_api_empty_query(self, client): | ||
| 109 | - """测试空查询API""" | ||
| 110 | - response = client.get("/search", params={"q": ""}) | ||
| 111 | - | ||
| 112 | - assert response.status_code == 200 | ||
| 113 | - data = response.json() | ||
| 114 | - | ||
| 115 | - # 空查询应该返回有效结果 | ||
| 116 | - assert "hits" in data | ||
| 117 | - assert isinstance(data["hits"], list) | ||
| 118 | - | ||
| 119 | - def test_search_api_with_filters(self, client): | ||
| 120 | - """测试带过滤器的搜索API""" | ||
| 121 | - response = client.get("/search", params={ | ||
| 122 | - "q": "连衣裙", | ||
| 123 | - "filters": json.dumps({"category_id": 1, "brand": "测试品牌"}) | ||
| 124 | - }) | ||
| 125 | - | ||
| 126 | - assert response.status_code == 200 | ||
| 127 | - data = response.json() | ||
| 128 | - | ||
| 129 | - # 验证过滤器被应用 | ||
| 130 | - performance = data.get("performance_summary", {}) | ||
| 131 | - metadata = performance.get("metadata", {}) | ||
| 132 | - search_params = metadata.get("search_params", {}) | ||
| 133 | - | ||
| 134 | - filters = search_params.get("filters", {}) | ||
| 135 | - assert filters.get("category_id") == 1 | ||
| 136 | - assert filters.get("brand") == "测试品牌" | ||
| 137 | - | ||
| 138 | - def test_search_api_performance_summary(self, client): | ||
| 139 | - """测试API性能摘要""" | ||
| 140 | - response = client.get("/search", params={"q": "性能测试查询"}) | ||
| 141 | - | ||
| 142 | - assert response.status_code == 200 | ||
| 143 | - data = response.json() | ||
| 144 | - | ||
| 145 | - performance = data.get("performance_summary", {}) | ||
| 146 | - | ||
| 147 | - # 验证性能摘要结构 | ||
| 148 | - assert "request_info" in performance | ||
| 149 | - assert "query_analysis" in performance | ||
| 150 | - assert "performance" in performance | ||
| 151 | - assert "results" in performance | ||
| 152 | - assert "metadata" in performance | ||
| 153 | - | ||
| 154 | - # 验证request_info | ||
| 155 | - request_info = performance["request_info"] | ||
| 156 | - assert "reqid" in request_info | ||
| 157 | - assert "uid" in request_info | ||
| 158 | - assert len(request_info["reqid"]) == 8 # 8字符的reqid | ||
| 159 | - | ||
| 160 | - # 验证performance | ||
| 161 | - perf_data = performance["performance"] | ||
| 162 | - assert "total_duration_ms" in perf_data | ||
| 163 | - assert "stage_timings_ms" in perf_data | ||
| 164 | - assert "stage_percentages" in perf_data | ||
| 165 | - assert isinstance(perf_data["total_duration_ms"], (int, float)) | ||
| 166 | - assert perf_data["total_duration_ms"] >= 0 | ||
| 167 | - | ||
| 168 | - def test_search_api_error_handling(self, client): | ||
| 169 | - """测试API错误处理""" | ||
| 170 | - # 模拟内部错误 | ||
| 171 | - with patch('api.app._searcher') as mock_searcher: | ||
| 172 | - mock_searcher.search.side_effect = Exception("内部服务错误") | ||
| 173 | - | ||
| 174 | - response = client.get("/search", params={"q": "错误测试"}) | ||
| 175 | - | ||
| 176 | - assert response.status_code == 500 | ||
| 177 | - data = response.json() | ||
| 178 | - | ||
| 179 | - # 验证错误响应格式 | ||
| 180 | - assert "error" in data | ||
| 181 | - assert "request_id" in data | ||
| 182 | - assert len(data["request_id"]) == 8 | ||
| 183 | - | ||
| 184 | - def test_health_check_api(self, client): | ||
| 185 | - """测试健康检查API""" | ||
| 186 | - response = client.get("/health") | ||
| 187 | - | ||
| 188 | - assert response.status_code == 200 | ||
| 189 | - data = response.json() | ||
| 190 | - | ||
| 191 | - # 验证健康检查响应 | ||
| 192 | - assert "status" in data | ||
| 193 | - assert "timestamp" in data | ||
| 194 | - assert "service" in data | ||
| 195 | - assert "version" in data | ||
| 196 | - | ||
| 197 | - assert data["status"] in ["healthy", "unhealthy"] | ||
| 198 | - assert data["service"] == "search-engine-api" | ||
| 199 | - | ||
| 200 | - def test_metrics_api(self, client): | ||
| 201 | - """测试指标API""" | ||
| 202 | - response = client.get("/metrics") | ||
| 203 | - | ||
| 204 | - # 根据实现,可能是JSON格式或Prometheus格式 | ||
| 205 | - assert response.status_code in [200, 404] # 404如果未实现 | ||
| 206 | - | ||
| 207 | - def test_concurrent_search_api(self, client): | ||
| 208 | - """测试并发搜索API""" | ||
| 209 | - async def test_concurrent(): | ||
| 210 | - tasks = [] | ||
| 211 | - for i in range(10): | ||
| 212 | - task = asyncio.create_task( | ||
| 213 | - asyncio.to_thread( | ||
| 214 | - client.get, | ||
| 215 | - "/search", | ||
| 216 | - params={"q": f"并发测试查询-{i}"} | ||
| 217 | - ) | ||
| 218 | - ) | ||
| 219 | - tasks.append(task) | ||
| 220 | - | ||
| 221 | - responses = await asyncio.gather(*tasks) | ||
| 222 | - | ||
| 223 | - # 验证所有响应都成功 | ||
| 224 | - for response in responses: | ||
| 225 | - assert response.status_code == 200 | ||
| 226 | - data = response.json() | ||
| 227 | - assert "hits" in data | ||
| 228 | - assert "performance_summary" in data | ||
| 229 | - | ||
| 230 | - # 运行并发测试 | ||
| 231 | - asyncio.run(test_concurrent()) | ||
| 232 | - | ||
| 233 | - def test_search_api_response_time(self, client): | ||
| 234 | - """测试API响应时间""" | ||
| 235 | - import time | ||
| 236 | - | ||
| 237 | - start_time = time.time() | ||
| 238 | - response = client.get("/search", params={"q": "响应时间测试"}) | ||
| 239 | - end_time = time.time() | ||
| 240 | - | ||
| 241 | - response_time_ms = (end_time - start_time) * 1000 | ||
| 242 | - | ||
| 243 | - assert response.status_code == 200 | ||
| 244 | - | ||
| 245 | - # API响应时间应该合理(例如,小于5秒) | ||
| 246 | - assert response_time_ms < 5000 | ||
| 247 | - | ||
| 248 | - # 验证响应中的时间信息 | ||
| 249 | - data = response.json() | ||
| 250 | - assert data["took_ms"] >= 0 | ||
| 251 | - | ||
| 252 | - performance = data.get("performance_summary", {}) | ||
| 253 | - perf_data = performance.get("performance", {}) | ||
| 254 | - total_duration = perf_data.get("total_duration_ms", 0) | ||
| 255 | - | ||
| 256 | - # 总处理时间应该包括API开销 | ||
| 257 | - assert total_duration > 0 | ||
| 258 | - | ||
| 259 | - def test_search_api_large_query(self, client): | ||
| 260 | - """测试大查询API""" | ||
| 261 | - # 构造一个较长的查询 | ||
| 262 | - long_query = " " * 1000 + "红色连衣裙" | ||
| 263 | - | ||
| 264 | - response = client.get("/search", params={"q": long_query}) | ||
| 265 | - | ||
| 266 | - assert response.status_code == 200 | ||
| 267 | - data = response.json() | ||
| 268 | - | ||
| 269 | - # 验证长查询被正确处理 | ||
| 270 | - query_analysis = data.get("performance_summary", {}).get("query_analysis", {}) | ||
| 271 | - assert query_analysis.get("original_query") == long_query | ||
| 272 | - | ||
| 273 | - def test_search_api_unicode_support(self, client): | ||
| 274 | - """测试API Unicode支持""" | ||
| 275 | - unicode_queries = [ | ||
| 276 | - "红色连衣裙", # 中文 | ||
| 277 | - "red dress", # 英文 | ||
| 278 | - "robe rouge", # 法文 | ||
| 279 | - "赤いドレス", # 日文 | ||
| 280 | - "أحمر فستان", # 阿拉伯文 | ||
| 281 | - "👗🔴", # Emoji | ||
| 282 | - ] | ||
| 283 | - | ||
| 284 | - for query in unicode_queries: | ||
| 285 | - response = client.get("/search", params={"q": query}) | ||
| 286 | - | ||
| 287 | - assert response.status_code == 200 | ||
| 288 | - data = response.json() | ||
| 289 | - | ||
| 290 | - # 验证Unicode查询被正确处理 | ||
| 291 | - query_analysis = data.get("performance_summary", {}).get("query_analysis", {}) | ||
| 292 | - assert query_analysis.get("original_query") == query | ||
| 293 | - | ||
| 294 | - def test_search_api_request_id_tracking(self, client): | ||
| 295 | - """测试API请求ID跟踪""" | ||
| 296 | - response = client.get("/search", params={"q": "请求ID测试"}) | ||
| 297 | - | ||
| 298 | - assert response.status_code == 200 | ||
| 299 | - data = response.json() | ||
| 300 | - | ||
| 301 | - # 验证每个请求都有唯一的reqid | ||
| 302 | - performance = data.get("performance_summary", {}) | ||
| 303 | - request_info = performance.get("request_info", {}) | ||
| 304 | - reqid = request_info.get("reqid") | ||
| 305 | - | ||
| 306 | - assert reqid is not None | ||
| 307 | - assert len(reqid) == 8 | ||
| 308 | - assert reqid.isalnum() | ||
| 309 | - | ||
| 310 | - def test_search_api_rate_limiting(self, client): | ||
| 311 | - """测试API速率限制(如果实现了)""" | ||
| 312 | - # 快速发送多个请求 | ||
| 313 | - responses = [] | ||
| 314 | - for i in range(20): # 发送20个快速请求 | ||
| 315 | - response = client.get("/search", params={"q": f"速率限制测试-{i}"}) | ||
| 316 | - responses.append(response) | ||
| 317 | - | ||
| 318 | - # 检查是否有请求被限制 | ||
| 319 | - status_codes = [r.status_code for r in responses] | ||
| 320 | - rate_limited = any(code == 429 for code in status_codes) | ||
| 321 | - | ||
| 322 | - # 根据是否实现速率限制,验证结果 | ||
| 323 | - if rate_limited: | ||
| 324 | - # 如果有速率限制,应该有一些429响应 | ||
| 325 | - assert 429 in status_codes | ||
| 326 | - else: | ||
| 327 | - # 如果没有速率限制,所有请求都应该成功 | ||
| 328 | - assert all(code == 200 for code in status_codes) | ||
| 329 | - | ||
| 330 | - def test_search_api_cors_headers(self, client): | ||
| 331 | - """测试API CORS头""" | ||
| 332 | - response = client.get("/search", params={"q": "CORS测试"}) | ||
| 333 | - | ||
| 334 | - assert response.status_code == 200 | ||
| 335 | - | ||
| 336 | - # 检查CORS头(如果配置了CORS) | ||
| 337 | - # 这取决于实际的CORS配置 | ||
| 338 | - # response.headers.get("Access-Control-Allow-Origin") | ||
| 339 | \ No newline at end of file | 0 | \ No newline at end of file |
tests/integration/test_search_integration.py deleted
| @@ -1,297 +0,0 @@ | @@ -1,297 +0,0 @@ | ||
| 1 | -""" | ||
| 2 | -搜索集成测试 | ||
| 3 | - | ||
| 4 | -测试搜索流程的完整集成,包括QueryParser、BooleanParser、ESQueryBuilder等组件的协同工作 | ||
| 5 | -""" | ||
| 6 | - | ||
| 7 | -import pytest | ||
| 8 | -from unittest.mock import Mock, patch, AsyncMock | ||
| 9 | -import json | ||
| 10 | -import numpy as np | ||
| 11 | - | ||
| 12 | -from search import Searcher | ||
| 13 | -from query import QueryParser | ||
| 14 | -from search.boolean_parser import BooleanParser, QueryNode | ||
| 15 | -from search.multilang_query_builder import MultiLanguageQueryBuilder | ||
| 16 | -from context import RequestContext, create_request_context | ||
| 17 | - | ||
| 18 | - | ||
| 19 | -@pytest.mark.integration | ||
| 20 | -@pytest.mark.slow | ||
| 21 | -class TestSearchIntegration: | ||
| 22 | - """搜索集成测试""" | ||
| 23 | - | ||
| 24 | - def test_end_to_end_search_flow(self, test_searcher): | ||
| 25 | - """测试端到端搜索流程""" | ||
| 26 | - context = create_request_context("e2e-001", "e2e-user") | ||
| 27 | - | ||
| 28 | - # 执行搜索 | ||
| 29 | - result = test_searcher.search("红色连衣裙", context=context) | ||
| 30 | - | ||
| 31 | - # 验证结果结构 | ||
| 32 | - assert result.hits is not None | ||
| 33 | - assert isinstance(result.hits, list) | ||
| 34 | - assert result.total >= 0 | ||
| 35 | - assert result.took_ms >= 0 | ||
| 36 | - assert result.context == context | ||
| 37 | - | ||
| 38 | - # 验证context中有完整的数据 | ||
| 39 | - summary = context.get_summary() | ||
| 40 | - assert summary['query_analysis']['original_query'] == "红色连衣裙" | ||
| 41 | - assert 'performance' in summary | ||
| 42 | - assert summary['performance']['total_duration_ms'] > 0 | ||
| 43 | - | ||
| 44 | - # 验证各阶段都被执行 | ||
| 45 | - assert context.get_stage_duration("query_parsing") >= 0 | ||
| 46 | - assert context.get_stage_duration("query_building") >= 0 | ||
| 47 | - assert context.get_stage_duration("elasticsearch_search") >= 0 | ||
| 48 | - assert context.get_stage_duration("result_processing") >= 0 | ||
| 49 | - | ||
| 50 | - def test_complex_boolean_query_integration(self, test_searcher): | ||
| 51 | - """测试复杂布尔查询的集成""" | ||
| 52 | - context = create_request_context("boolean-001") | ||
| 53 | - | ||
| 54 | - # 复杂布尔查询 | ||
| 55 | - result = test_searcher.search("手机 AND (华为 OR 苹果) ANDNOT 二手", context=context) | ||
| 56 | - | ||
| 57 | - assert result is not None | ||
| 58 | - assert context.query_analysis.is_simple_query is False | ||
| 59 | - assert context.query_analysis.boolean_ast is not None | ||
| 60 | - | ||
| 61 | - # 验证中间结果 | ||
| 62 | - query_node = context.get_intermediate_result('query_node') | ||
| 63 | - assert query_node is not None | ||
| 64 | - assert isinstance(query_node, QueryNode) | ||
| 65 | - | ||
| 66 | - def test_multilingual_search_integration(self, test_searcher): | ||
| 67 | - """测试多语言搜索集成""" | ||
| 68 | - context = create_request_context("multilang-001") | ||
| 69 | - | ||
| 70 | - with patch('query.query_parser.Translator') as mock_translator_class, \ | ||
| 71 | - patch('query.query_parser.LanguageDetector') as mock_detector_class: | ||
| 72 | - | ||
| 73 | - # 设置mock | ||
| 74 | - mock_translator = Mock() | ||
| 75 | - mock_translator_class.return_value = mock_translator | ||
| 76 | - mock_translator.get_translation_needs.return_value = ["en"] | ||
| 77 | - mock_translator.translate_multi.return_value = {"en": "red dress"} | ||
| 78 | - | ||
| 79 | - mock_detector = Mock() | ||
| 80 | - mock_detector_class.return_value = mock_detector | ||
| 81 | - mock_detector.detect.return_value = "zh" | ||
| 82 | - | ||
| 83 | - result = test_searcher.search("红色连衣裙", enable_translation=True, context=context) | ||
| 84 | - | ||
| 85 | - # 验证翻译结果被使用 | ||
| 86 | - assert context.query_analysis.translations.get("en") == "red dress" | ||
| 87 | - assert context.query_analysis.detected_language == "zh" | ||
| 88 | - | ||
| 89 | - def test_embedding_search_integration(self, test_searcher): | ||
| 90 | - """测试向量搜索集成""" | ||
| 91 | - # 配置embedding字段 | ||
| 92 | - test_searcher.text_embedding_field = "text_embedding" | ||
| 93 | - | ||
| 94 | - context = create_request_context("embedding-001") | ||
| 95 | - | ||
| 96 | - with patch('query.query_parser.BgeEncoder') as mock_encoder_class: | ||
| 97 | - # 设置mock | ||
| 98 | - mock_encoder = Mock() | ||
| 99 | - mock_encoder_class.return_value = mock_encoder | ||
| 100 | - mock_encoder.encode.return_value = [np.array([0.1, 0.2, 0.3, 0.4])] | ||
| 101 | - | ||
| 102 | - result = test_searcher.search("智能手机", enable_embedding=True, context=context) | ||
| 103 | - | ||
| 104 | - # 验证向量被生成和使用 | ||
| 105 | - assert context.query_analysis.query_vector is not None | ||
| 106 | - assert len(context.query_analysis.query_vector) == 4 | ||
| 107 | - | ||
| 108 | - # 验证ES查询包含KNN | ||
| 109 | - es_query = context.get_intermediate_result('es_query') | ||
| 110 | - if es_query and 'knn' in es_query: | ||
| 111 | - assert 'text_embedding' in es_query['knn'] | ||
| 112 | - | ||
| 113 | - def test_spu_collapse_integration(self, test_searcher): | ||
| 114 | - """测试SPU折叠集成""" | ||
| 115 | - # 启用SPU折叠 | ||
| 116 | - test_searcher.config.spu_config.enabled = True | ||
| 117 | - test_searcher.config.spu_config.spu_field = "spu_id" | ||
| 118 | - test_searcher.config.spu_config.inner_hits_size = 3 | ||
| 119 | - | ||
| 120 | - context = create_request_context("spu-001") | ||
| 121 | - | ||
| 122 | - result = test_searcher.search("手机", context=context) | ||
| 123 | - | ||
| 124 | - # 验证SPU折叠被应用 | ||
| 125 | - es_query = context.get_intermediate_result('es_query') | ||
| 126 | - assert es_query is not None | ||
| 127 | - | ||
| 128 | - # 如果ES查询构建正确,应该包含collapse配置 | ||
| 129 | - # 注意:这取决于ESQueryBuilder的实现 | ||
| 130 | - | ||
| 131 | - def test_reranking_integration(self, test_searcher): | ||
| 132 | - """测试重排序集成""" | ||
| 133 | - context = create_request_context("rerank-001") | ||
| 134 | - | ||
| 135 | - # 启用重排序 | ||
| 136 | - result = test_searcher.search("笔记本电脑", enable_rerank=True, context=context) | ||
| 137 | - | ||
| 138 | - # 验证重排序阶段被执行 | ||
| 139 | - if result.hits: # 如果有结果 | ||
| 140 | - # 应该有自定义分数 | ||
| 141 | - assert all('_custom_score' in hit for hit in result.hits) | ||
| 142 | - assert all('_original_score' in hit for hit in result.hits) | ||
| 143 | - | ||
| 144 | - # 自定义分数应该被计算 | ||
| 145 | - custom_scores = [hit['_custom_score'] for hit in result.hits] | ||
| 146 | - original_scores = [hit['_original_score'] for hit in result.hits] | ||
| 147 | - assert len(custom_scores) == len(original_scores) | ||
| 148 | - | ||
| 149 | - def test_error_propagation_integration(self, test_searcher): | ||
| 150 | - """测试错误传播集成""" | ||
| 151 | - context = create_request_context("error-001") | ||
| 152 | - | ||
| 153 | - # 模拟ES错误 | ||
| 154 | - test_searcher.es_client.search.side_effect = Exception("ES连接失败") | ||
| 155 | - | ||
| 156 | - with pytest.raises(Exception, match="ES连接失败"): | ||
| 157 | - test_searcher.search("测试查询", context=context) | ||
| 158 | - | ||
| 159 | - # 验证错误被正确记录 | ||
| 160 | - assert context.has_error() | ||
| 161 | - assert "ES连接失败" in context.metadata['error_info']['message'] | ||
| 162 | - | ||
| 163 | - def test_performance_monitoring_integration(self, test_searcher): | ||
| 164 | - """测试性能监控集成""" | ||
| 165 | - context = create_request_context("perf-001") | ||
| 166 | - | ||
| 167 | - # 模拟耗时操作 | ||
| 168 | - with patch('query.query_parser.QueryParser') as mock_parser_class: | ||
| 169 | - mock_parser = Mock() | ||
| 170 | - mock_parser_class.return_value = mock_parser | ||
| 171 | - mock_parser.parse.side_effect = lambda q, **kwargs: Mock( | ||
| 172 | - original_query=q, | ||
| 173 | - normalized_query=q, | ||
| 174 | - rewritten_query=q, | ||
| 175 | - detected_language="zh", | ||
| 176 | - domain="default", | ||
| 177 | - translations={}, | ||
| 178 | - query_vector=None | ||
| 179 | - ) | ||
| 180 | - | ||
| 181 | - # 执行搜索 | ||
| 182 | - result = test_searcher.search("性能测试查询", context=context) | ||
| 183 | - | ||
| 184 | - # 验证性能数据被收集 | ||
| 185 | - summary = context.get_summary() | ||
| 186 | - assert summary['performance']['total_duration_ms'] > 0 | ||
| 187 | - assert 'stage_timings_ms' in summary['performance'] | ||
| 188 | - assert 'stage_percentages' in summary['performance'] | ||
| 189 | - | ||
| 190 | - # 验证主要阶段都被计时 | ||
| 191 | - stages = ['query_parsing', 'query_building', 'elasticsearch_search', 'result_processing'] | ||
| 192 | - for stage in stages: | ||
| 193 | - assert stage in summary['performance']['stage_timings_ms'] | ||
| 194 | - | ||
| 195 | - def test_context_data_persistence_integration(self, test_searcher): | ||
| 196 | - """测试context数据持久化集成""" | ||
| 197 | - context = create_request_context("persist-001") | ||
| 198 | - | ||
| 199 | - result = test_searcher.search("数据持久化测试", context=context) | ||
| 200 | - | ||
| 201 | - # 验证所有关键数据都被存储 | ||
| 202 | - assert context.query_analysis.original_query == "数据持久化测试" | ||
| 203 | - assert context.get_intermediate_result('parsed_query') is not None | ||
| 204 | - assert context.get_intermediate_result('es_query') is not None | ||
| 205 | - assert context.get_intermediate_result('es_response') is not None | ||
| 206 | - assert context.get_intermediate_result('processed_hits') is not None | ||
| 207 | - | ||
| 208 | - # 验证元数据 | ||
| 209 | - assert 'search_params' in context.metadata | ||
| 210 | - assert 'feature_flags' in context.metadata | ||
| 211 | - assert context.metadata['search_params']['query'] == "数据持久化测试" | ||
| 212 | - | ||
| 213 | - @pytest.mark.parametrize("query,expected_simple", [ | ||
| 214 | - ("红色连衣裙", True), | ||
| 215 | - ("手机 AND 电脑", False), | ||
| 216 | - ("(华为 OR 苹果) ANDNOT 二手", False), | ||
| 217 | - "laptop RANK gaming", False, | ||
| 218 | - ("简单查询", True) | ||
| 219 | - ]) | ||
| 220 | - def test_query_complexity_detection(self, test_searcher, query, expected_simple): | ||
| 221 | - """测试查询复杂度检测""" | ||
| 222 | - context = create_request_context(f"complexity-{hash(query)}") | ||
| 223 | - | ||
| 224 | - result = test_searcher.search(query, context=context) | ||
| 225 | - | ||
| 226 | - assert context.query_analysis.is_simple_query == expected_simple | ||
| 227 | - | ||
| 228 | - def test_search_with_all_features_enabled(self, test_searcher): | ||
| 229 | - """测试启用所有功能的搜索""" | ||
| 230 | - # 配置所有功能 | ||
| 231 | - test_searcher.text_embedding_field = "text_embedding" | ||
| 232 | - test_searcher.config.spu_config.enabled = True | ||
| 233 | - test_searcher.config.spu_config.spu_field = "spu_id" | ||
| 234 | - | ||
| 235 | - context = create_request_context("all-features-001") | ||
| 236 | - | ||
| 237 | - with patch('query.query_parser.BgeEncoder') as mock_encoder_class, \ | ||
| 238 | - patch('query.query_parser.Translator') as mock_translator_class, \ | ||
| 239 | - patch('query.query_parser.LanguageDetector') as mock_detector_class: | ||
| 240 | - | ||
| 241 | - # 设置所有mock | ||
| 242 | - mock_encoder = Mock() | ||
| 243 | - mock_encoder_class.return_value = mock_encoder | ||
| 244 | - mock_encoder.encode.return_value = [np.array([0.1, 0.2])] | ||
| 245 | - | ||
| 246 | - mock_translator = Mock() | ||
| 247 | - mock_translator_class.return_value = mock_translator | ||
| 248 | - mock_translator.get_translation_needs.return_value = ["en"] | ||
| 249 | - mock_translator.translate_multi.return_value = {"en": "test query"} | ||
| 250 | - | ||
| 251 | - mock_detector = Mock() | ||
| 252 | - mock_detector_class.return_value = mock_detector | ||
| 253 | - mock_detector.detect.return_value = "zh" | ||
| 254 | - | ||
| 255 | - # 执行完整搜索 | ||
| 256 | - result = test_searcher.search( | ||
| 257 | - "完整功能测试", | ||
| 258 | - enable_translation=True, | ||
| 259 | - enable_embedding=True, | ||
| 260 | - enable_rerank=True, | ||
| 261 | - context=context | ||
| 262 | - ) | ||
| 263 | - | ||
| 264 | - # 验证所有功能都被使用 | ||
| 265 | - assert context.query_analysis.detected_language == "zh" | ||
| 266 | - assert context.query_analysis.translations.get("en") == "test query" | ||
| 267 | - assert context.query_analysis.query_vector is not None | ||
| 268 | - | ||
| 269 | - # 验证所有阶段都有耗时记录 | ||
| 270 | - summary = context.get_summary() | ||
| 271 | - expected_stages = [ | ||
| 272 | - 'query_parsing', 'query_building', | ||
| 273 | - 'elasticsearch_search', 'result_processing' | ||
| 274 | - ] | ||
| 275 | - for stage in expected_stages: | ||
| 276 | - assert stage in summary['performance']['stage_timings_ms'] | ||
| 277 | - | ||
| 278 | - def test_search_result_context_integration(self, test_searcher): | ||
| 279 | - """测试搜索结果与context的集成""" | ||
| 280 | - context = create_request_context("result-context-001") | ||
| 281 | - | ||
| 282 | - result = test_searcher.search("结果上下文集成测试", context=context) | ||
| 283 | - | ||
| 284 | - # 验证结果包含context | ||
| 285 | - assert result.context == context | ||
| 286 | - | ||
| 287 | - # 验证结果to_dict方法包含性能摘要 | ||
| 288 | - result_dict = result.to_dict() | ||
| 289 | - assert 'performance_summary' in result_dict | ||
| 290 | - assert result_dict['performance_summary']['request_info']['reqid'] == context.reqid | ||
| 291 | - | ||
| 292 | - # 验证性能摘要内容 | ||
| 293 | - perf_summary = result_dict['performance_summary'] | ||
| 294 | - assert 'query_analysis' in perf_summary | ||
| 295 | - assert 'performance' in perf_summary | ||
| 296 | - assert 'results' in perf_summary | ||
| 297 | - assert 'metadata' in perf_summary | ||
| 298 | \ No newline at end of file | 0 | \ No newline at end of file |
tests/unit/test_context.py deleted
| @@ -1,228 +0,0 @@ | @@ -1,228 +0,0 @@ | ||
| 1 | -""" | ||
| 2 | -RequestContext单元测试 | ||
| 3 | -""" | ||
| 4 | - | ||
| 5 | -import pytest | ||
| 6 | -import time | ||
| 7 | -from context import RequestContext, RequestContextStage, create_request_context | ||
| 8 | - | ||
| 9 | - | ||
| 10 | -@pytest.mark.unit | ||
| 11 | -class TestRequestContext: | ||
| 12 | - """RequestContext测试用例""" | ||
| 13 | - | ||
| 14 | - def test_create_context(self): | ||
| 15 | - """测试创建context""" | ||
| 16 | - context = create_request_context("req-001", "user-123") | ||
| 17 | - | ||
| 18 | - assert context.reqid == "req-001" | ||
| 19 | - assert context.uid == "user-123" | ||
| 20 | - assert not context.has_error() | ||
| 21 | - | ||
| 22 | - def test_auto_generated_reqid(self): | ||
| 23 | - """测试自动生成reqid""" | ||
| 24 | - context = RequestContext() | ||
| 25 | - | ||
| 26 | - assert context.reqid is not None | ||
| 27 | - assert len(context.reqid) == 8 | ||
| 28 | - assert context.uid == "anonymous" | ||
| 29 | - | ||
| 30 | - def test_stage_timing(self): | ||
| 31 | - """测试阶段计时""" | ||
| 32 | - context = create_request_context() | ||
| 33 | - | ||
| 34 | - # 开始计时 | ||
| 35 | - context.start_stage(RequestContextStage.QUERY_PARSING) | ||
| 36 | - time.sleep(0.05) # 50ms | ||
| 37 | - duration = context.end_stage(RequestContextStage.QUERY_PARSING) | ||
| 38 | - | ||
| 39 | - assert duration >= 40 # 至少40ms(允许一些误差) | ||
| 40 | - assert duration < 100 # 不超过100ms | ||
| 41 | - assert context.get_stage_duration(RequestContextStage.QUERY_PARSING) == duration | ||
| 42 | - | ||
| 43 | - def test_store_query_analysis(self): | ||
| 44 | - """测试存储查询分析结果""" | ||
| 45 | - context = create_request_context() | ||
| 46 | - | ||
| 47 | - context.store_query_analysis( | ||
| 48 | - original_query="红色连衣裙", | ||
| 49 | - normalized_query="红色 连衣裙", | ||
| 50 | - rewritten_query="红色 女 连衣裙", | ||
| 51 | - detected_language="zh", | ||
| 52 | - translations={"en": "red dress"}, | ||
| 53 | - domain="default", | ||
| 54 | - is_simple_query=True | ||
| 55 | - ) | ||
| 56 | - | ||
| 57 | - assert context.query_analysis.original_query == "红色连衣裙" | ||
| 58 | - assert context.query_analysis.detected_language == "zh" | ||
| 59 | - assert context.query_analysis.translations["en"] == "red dress" | ||
| 60 | - assert context.query_analysis.is_simple_query is True | ||
| 61 | - | ||
| 62 | - def test_store_intermediate_results(self): | ||
| 63 | - """测试存储中间结果""" | ||
| 64 | - context = create_request_context() | ||
| 65 | - | ||
| 66 | - # 存储各种类型的中间结果 | ||
| 67 | - context.store_intermediate_result('parsed_query', {'query': 'test'}) | ||
| 68 | - context.store_intermediate_result('es_query', {'bool': {'must': []}}) | ||
| 69 | - context.store_intermediate_result('hits', [{'_id': '1', '_score': 1.0}]) | ||
| 70 | - | ||
| 71 | - assert context.get_intermediate_result('parsed_query') == {'query': 'test'} | ||
| 72 | - assert context.get_intermediate_result('es_query') == {'bool': {'must': []}} | ||
| 73 | - assert context.get_intermediate_result('hits') == [{'_id': '1', '_score': 1.0}] | ||
| 74 | - | ||
| 75 | - # 测试不存在的key | ||
| 76 | - assert context.get_intermediate_result('nonexistent') is None | ||
| 77 | - assert context.get_intermediate_result('nonexistent', 'default') == 'default' | ||
| 78 | - | ||
| 79 | - def test_error_handling(self): | ||
| 80 | - """测试错误处理""" | ||
| 81 | - context = create_request_context() | ||
| 82 | - | ||
| 83 | - assert not context.has_error() | ||
| 84 | - | ||
| 85 | - # 设置错误 | ||
| 86 | - try: | ||
| 87 | - raise ValueError("测试错误") | ||
| 88 | - except Exception as e: | ||
| 89 | - context.set_error(e) | ||
| 90 | - | ||
| 91 | - assert context.has_error() | ||
| 92 | - error_info = context.metadata['error_info'] | ||
| 93 | - assert error_info['type'] == 'ValueError' | ||
| 94 | - assert error_info['message'] == '测试错误' | ||
| 95 | - | ||
| 96 | - def test_warnings(self): | ||
| 97 | - """测试警告处理""" | ||
| 98 | - context = create_request_context() | ||
| 99 | - | ||
| 100 | - assert len(context.metadata['warnings']) == 0 | ||
| 101 | - | ||
| 102 | - # 添加警告 | ||
| 103 | - context.add_warning("第一个警告") | ||
| 104 | - context.add_warning("第二个警告") | ||
| 105 | - | ||
| 106 | - assert len(context.metadata['warnings']) == 2 | ||
| 107 | - assert "第一个警告" in context.metadata['warnings'] | ||
| 108 | - assert "第二个警告" in context.metadata['warnings'] | ||
| 109 | - | ||
| 110 | - def test_stage_percentages(self): | ||
| 111 | - """测试阶段耗时占比计算""" | ||
| 112 | - context = create_request_context() | ||
| 113 | - context.performance_metrics.total_duration = 100.0 | ||
| 114 | - | ||
| 115 | - # 设置各阶段耗时 | ||
| 116 | - context.performance_metrics.stage_timings = { | ||
| 117 | - 'query_parsing': 25.0, | ||
| 118 | - 'elasticsearch_search': 50.0, | ||
| 119 | - 'result_processing': 25.0 | ||
| 120 | - } | ||
| 121 | - | ||
| 122 | - percentages = context.calculate_stage_percentages() | ||
| 123 | - | ||
| 124 | - assert percentages['query_parsing'] == 25.0 | ||
| 125 | - assert percentages['elasticsearch_search'] == 50.0 | ||
| 126 | - assert percentages['result_processing'] == 25.0 | ||
| 127 | - | ||
| 128 | - def test_get_summary(self): | ||
| 129 | - """测试获取摘要""" | ||
| 130 | - context = create_request_context("test-req", "test-user") | ||
| 131 | - | ||
| 132 | - # 设置一些数据 | ||
| 133 | - context.store_query_analysis( | ||
| 134 | - original_query="测试查询", | ||
| 135 | - detected_language="zh", | ||
| 136 | - domain="default" | ||
| 137 | - ) | ||
| 138 | - context.store_intermediate_result('test_key', 'test_value') | ||
| 139 | - context.performance_metrics.total_duration = 150.0 | ||
| 140 | - context.performance_metrics.stage_timings = { | ||
| 141 | - 'query_parsing': 30.0, | ||
| 142 | - 'elasticsearch_search': 80.0 | ||
| 143 | - } | ||
| 144 | - | ||
| 145 | - summary = context.get_summary() | ||
| 146 | - | ||
| 147 | - # 验证基本结构 | ||
| 148 | - assert 'request_info' in summary | ||
| 149 | - assert 'query_analysis' in summary | ||
| 150 | - assert 'performance' in summary | ||
| 151 | - assert 'results' in summary | ||
| 152 | - assert 'metadata' in summary | ||
| 153 | - | ||
| 154 | - # 验证具体内容 | ||
| 155 | - assert summary['request_info']['reqid'] == 'test-req' | ||
| 156 | - assert summary['request_info']['uid'] == 'test-user' | ||
| 157 | - assert summary['query_analysis']['original_query'] == '测试查询' | ||
| 158 | - assert summary['query_analysis']['detected_language'] == 'zh' | ||
| 159 | - assert summary['performance']['total_duration_ms'] == 150.0 | ||
| 160 | - assert 'query_parsing' in summary['performance']['stage_timings_ms'] | ||
| 161 | - | ||
| 162 | - def test_context_manager(self): | ||
| 163 | - """测试上下文管理器功能""" | ||
| 164 | - with create_request_context("cm-test", "cm-user") as context: | ||
| 165 | - assert context.reqid == "cm-test" | ||
| 166 | - assert context.uid == "cm-user" | ||
| 167 | - | ||
| 168 | - # 在上下文中执行一些操作 | ||
| 169 | - context.start_stage(RequestContextStage.QUERY_PARSING) | ||
| 170 | - time.sleep(0.01) | ||
| 171 | - context.end_stage(RequestContextStage.QUERY_PARSING) | ||
| 172 | - | ||
| 173 | - # 上下文应该仍然活跃 | ||
| 174 | - assert context.get_stage_duration(RequestContextStage.QUERY_PARSING) > 0 | ||
| 175 | - | ||
| 176 | - # 退出上下文后,应该自动记录了总时间 | ||
| 177 | - assert context.performance_metrics.total_duration > 0 | ||
| 178 | - | ||
| 179 | - | ||
| 180 | -@pytest.mark.unit | ||
| 181 | -class TestContextFactory: | ||
| 182 | - """Context工厂函数测试""" | ||
| 183 | - | ||
| 184 | - def test_create_request_context_with_params(self): | ||
| 185 | - """测试带参数创建context""" | ||
| 186 | - context = create_request_context("custom-req", "custom-user") | ||
| 187 | - | ||
| 188 | - assert context.reqid == "custom-req" | ||
| 189 | - assert context.uid == "custom-user" | ||
| 190 | - | ||
| 191 | - def test_create_request_context_without_params(self): | ||
| 192 | - """测试不带参数创建context""" | ||
| 193 | - context = create_request_context() | ||
| 194 | - | ||
| 195 | - assert context.reqid is not None | ||
| 196 | - assert len(context.reqid) == 8 | ||
| 197 | - assert context.uid == "anonymous" | ||
| 198 | - | ||
| 199 | - def test_create_request_context_with_partial_params(self): | ||
| 200 | - """测试部分参数创建context""" | ||
| 201 | - context = create_request_context(reqid="partial-req") | ||
| 202 | - | ||
| 203 | - assert context.reqid == "partial-req" | ||
| 204 | - assert context.uid == "anonymous" | ||
| 205 | - | ||
| 206 | - context2 = create_request_context(uid="partial-user") | ||
| 207 | - assert context2.reqid is not None | ||
| 208 | - assert context2.uid == "partial-user" | ||
| 209 | - | ||
| 210 | - | ||
| 211 | -@pytest.mark.unit | ||
| 212 | -class TestContextStages: | ||
| 213 | - """Context阶段枚举测试""" | ||
| 214 | - | ||
| 215 | - def test_stage_values(self): | ||
| 216 | - """测试阶段枚举值""" | ||
| 217 | - assert RequestContextStage.TOTAL.value == "total_search" | ||
| 218 | - assert RequestContextStage.QUERY_PARSING.value == "query_parsing" | ||
| 219 | - assert RequestContextStage.BOOLEAN_PARSING.value == "boolean_parsing" | ||
| 220 | - assert RequestContextStage.QUERY_BUILDING.value == "query_building" | ||
| 221 | - assert RequestContextStage.ELASTICSEARCH_SEARCH.value == "elasticsearch_search" | ||
| 222 | - assert RequestContextStage.RESULT_PROCESSING.value == "result_processing" | ||
| 223 | - assert RequestContextStage.RERANKING.value == "reranking" | ||
| 224 | - | ||
| 225 | - def test_stage_uniqueness(self): | ||
| 226 | - """测试阶段值唯一性""" | ||
| 227 | - values = [stage.value for stage in RequestContextStage] | ||
| 228 | - assert len(values) == len(set(values)), "阶段值应该是唯一的" | ||
| 229 | \ No newline at end of file | 0 | \ No newline at end of file |
tests/unit/test_query_parser.py deleted
| @@ -1,270 +0,0 @@ | @@ -1,270 +0,0 @@ | ||
| 1 | -""" | ||
| 2 | -QueryParser单元测试 | ||
| 3 | -""" | ||
| 4 | - | ||
| 5 | -import pytest | ||
| 6 | -from unittest.mock import Mock, patch, MagicMock | ||
| 7 | -import numpy as np | ||
| 8 | - | ||
| 9 | -from query import QueryParser, ParsedQuery | ||
| 10 | -from context import RequestContext, create_request_context | ||
| 11 | - | ||
| 12 | - | ||
| 13 | -@pytest.mark.unit | ||
| 14 | -class TestQueryParser: | ||
| 15 | - """QueryParser测试用例""" | ||
| 16 | - | ||
| 17 | - def test_parser_initialization(self, sample_customer_config): | ||
| 18 | - """测试QueryParser初始化""" | ||
| 19 | - parser = QueryParser(sample_customer_config) | ||
| 20 | - | ||
| 21 | - assert parser.config == sample_customer_config | ||
| 22 | - assert parser.query_config is not None | ||
| 23 | - assert parser.normalizer is not None | ||
| 24 | - assert parser.rewriter is not None | ||
| 25 | - assert parser.language_detector is not None | ||
| 26 | - assert parser.translator is not None | ||
| 27 | - | ||
| 28 | - @patch('query.query_parser.QueryNormalizer') | ||
| 29 | - @patch('query.query_parser.LanguageDetector') | ||
| 30 | - def test_parse_without_context(self, mock_detector_class, mock_normalizer_class, test_query_parser): | ||
| 31 | - """测试不带context的解析""" | ||
| 32 | - # 设置mock | ||
| 33 | - mock_normalizer = Mock() | ||
| 34 | - mock_normalizer_class.return_value = mock_normalizer | ||
| 35 | - mock_normalizer.normalize.return_value = "红色 连衣裙" | ||
| 36 | - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") | ||
| 37 | - | ||
| 38 | - mock_detector = Mock() | ||
| 39 | - mock_detector_class.return_value = mock_detector | ||
| 40 | - mock_detector.detect.return_value = "zh" | ||
| 41 | - | ||
| 42 | - result = test_query_parser.parse("红色连衣裙") | ||
| 43 | - | ||
| 44 | - assert isinstance(result, ParsedQuery) | ||
| 45 | - assert result.original_query == "红色连衣裙" | ||
| 46 | - assert result.normalized_query == "红色 连衣裙" | ||
| 47 | - assert result.rewritten_query == "红色 连衣裙" # 没有重写 | ||
| 48 | - assert result.detected_language == "zh" | ||
| 49 | - | ||
| 50 | - def test_parse_with_context(self, test_query_parser): | ||
| 51 | - """测试带context的解析""" | ||
| 52 | - context = create_request_context("parse-001", "parse-user") | ||
| 53 | - | ||
| 54 | - # Mock各种组件 | ||
| 55 | - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ | ||
| 56 | - patch.object(test_query_parser, 'language_detector') as mock_detector, \ | ||
| 57 | - patch.object(test_query_parser, 'translator') as mock_translator, \ | ||
| 58 | - patch.object(test_query_parser, 'text_encoder') as mock_encoder: | ||
| 59 | - | ||
| 60 | - # 设置mock返回值 | ||
| 61 | - mock_normalizer.normalize.return_value = "红色 连衣裙" | ||
| 62 | - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") | ||
| 63 | - mock_detector.detect.return_value = "zh" | ||
| 64 | - mock_translator.translate_multi.return_value = {"en": "red dress"} | ||
| 65 | - mock_encoder.encode.return_value = [np.array([0.1, 0.2, 0.3])] | ||
| 66 | - | ||
| 67 | - result = test_query_parser.parse("红色连衣裙", generate_vector=True, context=context) | ||
| 68 | - | ||
| 69 | - # 验证结果 | ||
| 70 | - assert isinstance(result, ParsedQuery) | ||
| 71 | - assert result.original_query == "红色连衣裙" | ||
| 72 | - assert result.detected_language == "zh" | ||
| 73 | - assert result.translations["en"] == "red dress" | ||
| 74 | - assert result.query_vector is not None | ||
| 75 | - | ||
| 76 | - # 验证context被更新 | ||
| 77 | - assert context.query_analysis.original_query == "红色连衣裙" | ||
| 78 | - assert context.query_analysis.normalized_query == "红色 连衣裙" | ||
| 79 | - assert context.query_analysis.detected_language == "zh" | ||
| 80 | - assert context.query_analysis.translations["en"] == "red dress" | ||
| 81 | - assert context.query_analysis.domain == "default" | ||
| 82 | - | ||
| 83 | - # 验证计时 | ||
| 84 | - assert context.get_stage_duration("query_parsing") > 0 | ||
| 85 | - | ||
| 86 | - @patch('query.query_parser.QueryRewriter') | ||
| 87 | - def test_query_rewriting(self, mock_rewriter_class, test_query_parser): | ||
| 88 | - """测试查询重写""" | ||
| 89 | - # 设置mock | ||
| 90 | - mock_rewriter = Mock() | ||
| 91 | - mock_rewriter_class.return_value = mock_rewriter | ||
| 92 | - mock_rewriter.rewrite.return_value = "红色 女 连衣裙" | ||
| 93 | - | ||
| 94 | - context = create_request_context() | ||
| 95 | - | ||
| 96 | - # 启用查询重写 | ||
| 97 | - test_query_parser.query_config.enable_query_rewrite = True | ||
| 98 | - | ||
| 99 | - result = test_query_parser.parse("红色连衣裙", context=context) | ||
| 100 | - | ||
| 101 | - assert result.rewritten_query == "红色 女 连衣裙" | ||
| 102 | - assert context.query_analysis.rewritten_query == "红色 女 连衣裙" | ||
| 103 | - | ||
| 104 | - def test_language_detection(self, test_query_parser): | ||
| 105 | - """测试语言检测""" | ||
| 106 | - context = create_request_context() | ||
| 107 | - | ||
| 108 | - with patch.object(test_query_parser, 'language_detector') as mock_detector, \ | ||
| 109 | - patch.object(test_query_parser, 'normalizer') as mock_normalizer: | ||
| 110 | - | ||
| 111 | - mock_normalizer.normalize.return_value = "red dress" | ||
| 112 | - mock_normalizer.extract_domain_query.return_value = ("default", "red dress") | ||
| 113 | - mock_detector.detect.return_value = "en" | ||
| 114 | - | ||
| 115 | - result = test_query_parser.parse("red dress", context=context) | ||
| 116 | - | ||
| 117 | - assert result.detected_language == "en" | ||
| 118 | - assert context.query_analysis.detected_language == "en" | ||
| 119 | - | ||
| 120 | - @patch('query.query_parser.Translator') | ||
| 121 | - def test_query_translation(self, mock_translator_class, test_query_parser): | ||
| 122 | - """测试查询翻译""" | ||
| 123 | - # 设置mock | ||
| 124 | - mock_translator = Mock() | ||
| 125 | - mock_translator_class.return_value = mock_translator | ||
| 126 | - mock_translator.get_translation_needs.return_value = ["en"] | ||
| 127 | - mock_translator.translate_multi.return_value = {"en": "red dress"} | ||
| 128 | - | ||
| 129 | - context = create_request_context() | ||
| 130 | - | ||
| 131 | - # 启用翻译 | ||
| 132 | - test_query_parser.query_config.enable_translation = True | ||
| 133 | - test_query_parser.query_config.supported_languages = ["zh", "en"] | ||
| 134 | - | ||
| 135 | - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ | ||
| 136 | - patch.object(test_query_parser, 'language_detector') as mock_detector: | ||
| 137 | - | ||
| 138 | - mock_normalizer.normalize.return_value = "红色 连衣裙" | ||
| 139 | - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") | ||
| 140 | - mock_detector.detect.return_value = "zh" | ||
| 141 | - | ||
| 142 | - result = test_query_parser.parse("红色连衣裙", context=context) | ||
| 143 | - | ||
| 144 | - assert result.translations["en"] == "red dress" | ||
| 145 | - assert context.query_analysis.translations["en"] == "red dress" | ||
| 146 | - | ||
| 147 | - @patch('query.query_parser.BgeEncoder') | ||
| 148 | - def test_text_embedding(self, mock_encoder_class, test_query_parser): | ||
| 149 | - """测试文本向量化""" | ||
| 150 | - # 设置mock | ||
| 151 | - mock_encoder = Mock() | ||
| 152 | - mock_encoder_class.return_value = mock_encoder | ||
| 153 | - mock_encoder.encode.return_value = [np.array([0.1, 0.2, 0.3])] | ||
| 154 | - | ||
| 155 | - context = create_request_context() | ||
| 156 | - | ||
| 157 | - # 启用向量化 | ||
| 158 | - test_query_parser.query_config.enable_text_embedding = True | ||
| 159 | - | ||
| 160 | - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ | ||
| 161 | - patch.object(test_query_parser, 'language_detector') as mock_detector: | ||
| 162 | - | ||
| 163 | - mock_normalizer.normalize.return_value = "红色 连衣裙" | ||
| 164 | - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") | ||
| 165 | - mock_detector.detect.return_value = "zh" | ||
| 166 | - | ||
| 167 | - result = test_query_parser.parse("红色连衣裙", generate_vector=True, context=context) | ||
| 168 | - | ||
| 169 | - assert result.query_vector is not None | ||
| 170 | - assert isinstance(result.query_vector, np.ndarray) | ||
| 171 | - assert context.query_analysis.query_vector is not None | ||
| 172 | - | ||
| 173 | - def test_domain_extraction(self, test_query_parser): | ||
| 174 | - """测试域名提取""" | ||
| 175 | - context = create_request_context() | ||
| 176 | - | ||
| 177 | - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ | ||
| 178 | - patch.object(test_query_parser, 'language_detector') as mock_detector: | ||
| 179 | - | ||
| 180 | - # 测试带域名的查询 | ||
| 181 | - mock_normalizer.normalize.return_value = "brand:nike 鞋子" | ||
| 182 | - mock_normalizer.extract_domain_query.return_value = ("brand", "nike 鞋子") | ||
| 183 | - mock_detector.detect.return_value = "zh" | ||
| 184 | - | ||
| 185 | - result = test_query_parser.parse("brand:nike 鞋子", context=context) | ||
| 186 | - | ||
| 187 | - assert result.domain == "brand" | ||
| 188 | - assert context.query_analysis.domain == "brand" | ||
| 189 | - | ||
| 190 | - def test_parse_with_disabled_features(self, test_query_parser): | ||
| 191 | - """测试禁用功能的解析""" | ||
| 192 | - context = create_request_context() | ||
| 193 | - | ||
| 194 | - # 禁用所有功能 | ||
| 195 | - test_query_parser.query_config.enable_query_rewrite = False | ||
| 196 | - test_query_parser.query_config.enable_translation = False | ||
| 197 | - test_query_parser.query_config.enable_text_embedding = False | ||
| 198 | - | ||
| 199 | - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ | ||
| 200 | - patch.object(test_query_parser, 'language_detector') as mock_detector: | ||
| 201 | - | ||
| 202 | - mock_normalizer.normalize.return_value = "红色 连衣裙" | ||
| 203 | - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") | ||
| 204 | - mock_detector.detect.return_value = "zh" | ||
| 205 | - | ||
| 206 | - result = test_query_parser.parse("红色连衣裙", generate_vector=False, context=context) | ||
| 207 | - | ||
| 208 | - assert result.original_query == "红色连衣裙" | ||
| 209 | - assert result.rewritten_query == "红色 连衣裙" # 没有重写 | ||
| 210 | - assert result.detected_language == "zh" | ||
| 211 | - assert len(result.translations) == 0 # 没有翻译 | ||
| 212 | - assert result.query_vector is None # 没有向量 | ||
| 213 | - | ||
| 214 | - def test_get_search_queries(self, test_query_parser): | ||
| 215 | - """测试获取搜索查询列表""" | ||
| 216 | - parsed_query = ParsedQuery( | ||
| 217 | - original_query="红色连衣裙", | ||
| 218 | - normalized_query="红色 连衣裙", | ||
| 219 | - rewritten_query="红色 连衣裙", | ||
| 220 | - detected_language="zh", | ||
| 221 | - translations={"en": "red dress", "fr": "robe rouge"} | ||
| 222 | - ) | ||
| 223 | - | ||
| 224 | - queries = test_query_parser.get_search_queries(parsed_query) | ||
| 225 | - | ||
| 226 | - assert len(queries) == 3 | ||
| 227 | - assert "红色 连衣裙" in queries | ||
| 228 | - assert "red dress" in queries | ||
| 229 | - assert "robe rouge" in queries | ||
| 230 | - | ||
| 231 | - def test_empty_query_handling(self, test_query_parser): | ||
| 232 | - """测试空查询处理""" | ||
| 233 | - result = test_query_parser.parse("") | ||
| 234 | - | ||
| 235 | - assert result.original_query == "" | ||
| 236 | - assert result.normalized_query == "" | ||
| 237 | - | ||
| 238 | - def test_whitespace_query_handling(self, test_query_parser): | ||
| 239 | - """测试空白字符查询处理""" | ||
| 240 | - result = test_query_parser.parse(" ") | ||
| 241 | - | ||
| 242 | - assert result.original_query == " " | ||
| 243 | - | ||
| 244 | - def test_error_handling_in_parsing(self, test_query_parser): | ||
| 245 | - """测试解析过程中的错误处理""" | ||
| 246 | - context = create_request_context() | ||
| 247 | - | ||
| 248 | - # Mock normalizer抛出异常 | ||
| 249 | - with patch.object(test_query_parser, 'normalizer') as mock_normalizer: | ||
| 250 | - mock_normalizer.normalize.side_effect = Exception("Normalization failed") | ||
| 251 | - | ||
| 252 | - with pytest.raises(Exception, match="Normalization failed"): | ||
| 253 | - test_query_parser.parse("红色连衣裙", context=context) | ||
| 254 | - | ||
| 255 | - def test_performance_timing(self, test_query_parser): | ||
| 256 | - """测试性能计时""" | ||
| 257 | - context = create_request_context() | ||
| 258 | - | ||
| 259 | - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ | ||
| 260 | - patch.object(test_query_parser, 'language_detector') as mock_detector: | ||
| 261 | - | ||
| 262 | - mock_normalizer.normalize.return_value = "test" | ||
| 263 | - mock_normalizer.extract_domain_query.return_value = ("default", "test") | ||
| 264 | - mock_detector.detect.return_value = "zh" | ||
| 265 | - | ||
| 266 | - result = test_query_parser.parse("test", context=context) | ||
| 267 | - | ||
| 268 | - # 验证计时被记录 | ||
| 269 | - assert context.get_stage_duration("query_parsing") > 0 | ||
| 270 | - assert context.get_intermediate_result('parsed_query') == result | ||
| 271 | \ No newline at end of file | 0 | \ No newline at end of file |
tests/unit/test_searcher.py deleted
| @@ -1,242 +0,0 @@ | @@ -1,242 +0,0 @@ | ||
| 1 | -""" | ||
| 2 | -Searcher单元测试 | ||
| 3 | -""" | ||
| 4 | - | ||
| 5 | -import pytest | ||
| 6 | -from unittest.mock import Mock, patch, MagicMock | ||
| 7 | -import numpy as np | ||
| 8 | - | ||
| 9 | -from search import Searcher | ||
| 10 | -from query import ParsedQuery | ||
| 11 | -from context import RequestContext, create_request_context | ||
| 12 | - | ||
| 13 | - | ||
| 14 | -@pytest.mark.unit | ||
| 15 | -class TestSearcher: | ||
| 16 | - """Searcher测试用例""" | ||
| 17 | - | ||
| 18 | - def test_searcher_initialization(self, sample_customer_config, mock_es_client): | ||
| 19 | - """测试Searcher初始化""" | ||
| 20 | - searcher = Searcher(sample_customer_config, mock_es_client) | ||
| 21 | - | ||
| 22 | - assert searcher.config == sample_customer_config | ||
| 23 | - assert searcher.es_client == mock_es_client | ||
| 24 | - assert searcher.query_parser is not None | ||
| 25 | - assert searcher.boolean_parser is not None | ||
| 26 | - assert searcher.ranking_engine is not None | ||
| 27 | - | ||
| 28 | - def test_search_without_context(self, test_searcher): | ||
| 29 | - """测试不带context的搜索(向后兼容)""" | ||
| 30 | - result = test_searcher.search("红色连衣裙", size=5) | ||
| 31 | - | ||
| 32 | - assert result.hits is not None | ||
| 33 | - assert result.total >= 0 | ||
| 34 | - assert result.context is not None # 应该自动创建context | ||
| 35 | - assert result.took_ms >= 0 | ||
| 36 | - | ||
| 37 | - def test_search_with_context(self, test_searcher): | ||
| 38 | - """测试带context的搜索""" | ||
| 39 | - context = create_request_context("test-req", "test-user") | ||
| 40 | - | ||
| 41 | - result = test_searcher.search("红色连衣裙", context=context) | ||
| 42 | - | ||
| 43 | - assert result.hits is not None | ||
| 44 | - assert result.context == context | ||
| 45 | - assert context.reqid == "test-req" | ||
| 46 | - assert context.uid == "test-user" | ||
| 47 | - | ||
| 48 | - def test_search_with_parameters(self, test_searcher): | ||
| 49 | - """测试带各种参数的搜索""" | ||
| 50 | - context = create_request_context() | ||
| 51 | - | ||
| 52 | - result = test_searcher.search( | ||
| 53 | - query="红色连衣裙", | ||
| 54 | - size=15, | ||
| 55 | - from_=5, | ||
| 56 | - filters={"category_id": 1}, | ||
| 57 | - enable_translation=False, | ||
| 58 | - enable_embedding=False, | ||
| 59 | - enable_rerank=False, | ||
| 60 | - min_score=1.0, | ||
| 61 | - context=context | ||
| 62 | - ) | ||
| 63 | - | ||
| 64 | - assert result is not None | ||
| 65 | - assert context.metadata['search_params']['size'] == 15 | ||
| 66 | - assert context.metadata['search_params']['from'] == 5 | ||
| 67 | - assert context.metadata['search_params']['filters'] == {"category_id": 1} | ||
| 68 | - assert context.metadata['search_params']['min_score'] == 1.0 | ||
| 69 | - | ||
| 70 | - # 验证feature flags | ||
| 71 | - assert context.metadata['feature_flags']['enable_translation'] is False | ||
| 72 | - assert context.metadata['feature_flags']['enable_embedding'] is False | ||
| 73 | - assert context.metadata['feature_flags']['enable_rerank'] is False | ||
| 74 | - | ||
| 75 | - @patch('search.searcher.QueryParser') | ||
| 76 | - def test_search_query_parsing(self, mock_query_parser_class, test_searcher): | ||
| 77 | - """测试查询解析流程""" | ||
| 78 | - # 设置mock | ||
| 79 | - mock_parser = Mock() | ||
| 80 | - mock_query_parser_class.return_value = mock_parser | ||
| 81 | - | ||
| 82 | - parsed_query = ParsedQuery( | ||
| 83 | - original_query="红色连衣裙", | ||
| 84 | - normalized_query="红色 连衣裙", | ||
| 85 | - rewritten_query="红色 女 连衣裙", | ||
| 86 | - detected_language="zh", | ||
| 87 | - domain="default" | ||
| 88 | - ) | ||
| 89 | - mock_parser.parse.return_value = parsed_query | ||
| 90 | - | ||
| 91 | - context = create_request_context() | ||
| 92 | - test_searcher.search("红色连衣裙", context=context) | ||
| 93 | - | ||
| 94 | - # 验证query parser被调用 | ||
| 95 | - mock_parser.parse.assert_called_once_with("红色连衣裙", generate_vector=True, context=context) | ||
| 96 | - | ||
| 97 | - def test_search_error_handling(self, test_searcher): | ||
| 98 | - """测试搜索错误处理""" | ||
| 99 | - # 设置ES客户端抛出异常 | ||
| 100 | - test_searcher.es_client.search.side_effect = Exception("ES连接失败") | ||
| 101 | - | ||
| 102 | - context = create_request_context() | ||
| 103 | - | ||
| 104 | - with pytest.raises(Exception, match="ES连接失败"): | ||
| 105 | - test_searcher.search("红色连衣裙", context=context) | ||
| 106 | - | ||
| 107 | - # 验证错误被记录到context | ||
| 108 | - assert context.has_error() | ||
| 109 | - assert "ES连接失败" in context.metadata['error_info']['message'] | ||
| 110 | - | ||
| 111 | - def test_search_result_processing(self, test_searcher): | ||
| 112 | - """测试搜索结果处理""" | ||
| 113 | - context = create_request_context() | ||
| 114 | - | ||
| 115 | - result = test_searcher.search("红色连衣裙", enable_rerank=True, context=context) | ||
| 116 | - | ||
| 117 | - # 验证结果结构 | ||
| 118 | - assert hasattr(result, 'hits') | ||
| 119 | - assert hasattr(result, 'total') | ||
| 120 | - assert hasattr(result, 'max_score') | ||
| 121 | - assert hasattr(result, 'took_ms') | ||
| 122 | - assert hasattr(result, 'aggregations') | ||
| 123 | - assert hasattr(result, 'query_info') | ||
| 124 | - assert hasattr(result, 'context') | ||
| 125 | - | ||
| 126 | - # 验证context中有中间结果 | ||
| 127 | - assert context.get_intermediate_result('es_response') is not None | ||
| 128 | - assert context.get_intermediate_result('raw_hits') is not None | ||
| 129 | - assert context.get_intermediate_result('processed_hits') is not None | ||
| 130 | - | ||
| 131 | - def test_boolean_query_handling(self, test_searcher): | ||
| 132 | - """测试布尔查询处理""" | ||
| 133 | - context = create_request_context() | ||
| 134 | - | ||
| 135 | - # 测试复杂布尔查询 | ||
| 136 | - result = test_searcher.search("laptop AND (gaming OR professional)", context=context) | ||
| 137 | - | ||
| 138 | - assert result is not None | ||
| 139 | - # 对于复杂查询,应该调用boolean parser | ||
| 140 | - assert not context.query_analysis.is_simple_query | ||
| 141 | - | ||
| 142 | - def test_simple_query_handling(self, test_searcher): | ||
| 143 | - """测试简单查询处理""" | ||
| 144 | - context = create_request_context() | ||
| 145 | - | ||
| 146 | - # 测试简单查询 | ||
| 147 | - result = test_searcher.search("红色连衣裙", context=context) | ||
| 148 | - | ||
| 149 | - assert result is not None | ||
| 150 | - # 简单查询应该标记为simple | ||
| 151 | - assert context.query_analysis.is_simple_query | ||
| 152 | - | ||
| 153 | - @patch('search.searcher.RankingEngine') | ||
| 154 | - def test_reranking(self, mock_ranking_engine_class, test_searcher): | ||
| 155 | - """测试重排序功能""" | ||
| 156 | - # 设置mock | ||
| 157 | - mock_ranking = Mock() | ||
| 158 | - mock_ranking_engine_class.return_value = mock_ranking | ||
| 159 | - mock_ranking.calculate_score.return_value = 2.0 | ||
| 160 | - | ||
| 161 | - context = create_request_context() | ||
| 162 | - result = test_searcher.search("红色连衣裙", enable_rerank=True, context=context) | ||
| 163 | - | ||
| 164 | - # 验证重排序被调用 | ||
| 165 | - hits = result.hits | ||
| 166 | - if hits: # 如果有结果 | ||
| 167 | - # 应该有自定义分数 | ||
| 168 | - assert all('_custom_score' in hit for hit in hits) | ||
| 169 | - assert all('_original_score' in hit for hit in hits) | ||
| 170 | - | ||
| 171 | - def test_spu_collapse(self, test_searcher): | ||
| 172 | - """测试SPU折叠功能""" | ||
| 173 | - # 配置SPU | ||
| 174 | - test_searcher.config.spu_config.enabled = True | ||
| 175 | - test_searcher.config.spu_config.spu_field = "spu_id" | ||
| 176 | - test_searcher.config.spu_config.inner_hits_size = 3 | ||
| 177 | - | ||
| 178 | - context = create_request_context() | ||
| 179 | - result = test_searcher.search("红色连衣裙", context=context) | ||
| 180 | - | ||
| 181 | - assert result is not None | ||
| 182 | - # 验证SPU折叠配置被应用 | ||
| 183 | - assert context.get_intermediate_result('es_query') is not None | ||
| 184 | - | ||
| 185 | - def test_embedding_search(self, test_searcher): | ||
| 186 | - """测试向量搜索功能""" | ||
| 187 | - # 配置embedding字段 | ||
| 188 | - test_searcher.text_embedding_field = "text_embedding" | ||
| 189 | - | ||
| 190 | - context = create_request_context() | ||
| 191 | - result = test_searcher.search("红色连衣裙", enable_embedding=True, context=context) | ||
| 192 | - | ||
| 193 | - assert result is not None | ||
| 194 | - # embedding搜索应该被启用 | ||
| 195 | - | ||
| 196 | - def test_search_by_image(self, test_searcher): | ||
| 197 | - """测试图片搜索功能""" | ||
| 198 | - # 配置图片embedding字段 | ||
| 199 | - test_searcher.image_embedding_field = "image_embedding" | ||
| 200 | - | ||
| 201 | - # Mock图片编码器 | ||
| 202 | - with patch('search.searcher.CLIPImageEncoder') as mock_encoder_class: | ||
| 203 | - mock_encoder = Mock() | ||
| 204 | - mock_encoder_class.return_value = mock_encoder | ||
| 205 | - mock_encoder.encode_image_from_url.return_value = np.array([0.1, 0.2, 0.3]) | ||
| 206 | - | ||
| 207 | - result = test_searcher.search_by_image("http://example.com/image.jpg") | ||
| 208 | - | ||
| 209 | - assert result is not None | ||
| 210 | - assert result.query_info['search_type'] == 'image_similarity' | ||
| 211 | - assert result.query_info['image_url'] == "http://example.com/image.jpg" | ||
| 212 | - | ||
| 213 | - def test_performance_monitoring(self, test_searcher): | ||
| 214 | - """测试性能监控""" | ||
| 215 | - context = create_request_context() | ||
| 216 | - | ||
| 217 | - result = test_searcher.search("红色连衣裙", context=context) | ||
| 218 | - | ||
| 219 | - # 验证各阶段都被计时 | ||
| 220 | - assert context.get_stage_duration(RequestContextStage.QUERY_PARSING) >= 0 | ||
| 221 | - assert context.get_stage_duration(RequestContextStage.QUERY_BUILDING) >= 0 | ||
| 222 | - assert context.get_stage_duration(RequestContextStage.ELASTICSEARCH_SEARCH) >= 0 | ||
| 223 | - assert context.get_stage_duration(RequestContextStage.RESULT_PROCESSING) >= 0 | ||
| 224 | - | ||
| 225 | - # 验证总耗时 | ||
| 226 | - assert context.performance_metrics.total_duration > 0 | ||
| 227 | - | ||
| 228 | - def test_context_storage(self, test_searcher): | ||
| 229 | - """测试context存储功能""" | ||
| 230 | - context = create_request_context() | ||
| 231 | - | ||
| 232 | - result = test_searcher.search("红色连衣裙", context=context) | ||
| 233 | - | ||
| 234 | - # 验证查询分析结果被存储 | ||
| 235 | - assert context.query_analysis.original_query == "红色连衣裙" | ||
| 236 | - assert context.query_analysis.domain is not None | ||
| 237 | - | ||
| 238 | - # 验证中间结果被存储 | ||
| 239 | - assert context.get_intermediate_result('parsed_query') is not None | ||
| 240 | - assert context.get_intermediate_result('es_query') is not None | ||
| 241 | - assert context.get_intermediate_result('es_response') is not None | ||
| 242 | - assert context.get_intermediate_result('processed_hits') is not None | ||
| 243 | \ No newline at end of file | 0 | \ No newline at end of file |
verification_report.py deleted
| @@ -1,142 +0,0 @@ | @@ -1,142 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | -""" | ||
| 3 | -验证报告 - 确认请求上下文和日志系统修复完成 | ||
| 4 | -""" | ||
| 5 | - | ||
| 6 | -import sys | ||
| 7 | -import os | ||
| 8 | -import traceback | ||
| 9 | - | ||
| 10 | -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | ||
| 11 | - | ||
| 12 | -def run_verification(): | ||
| 13 | - """运行完整的验证测试""" | ||
| 14 | - print("🔍 开始系统验证...") | ||
| 15 | - print("=" * 60) | ||
| 16 | - | ||
| 17 | - tests_passed = 0 | ||
| 18 | - tests_total = 0 | ||
| 19 | - | ||
| 20 | - def run_test(test_name, test_func): | ||
| 21 | - nonlocal tests_passed, tests_total | ||
| 22 | - tests_total += 1 | ||
| 23 | - try: | ||
| 24 | - test_func() | ||
| 25 | - print(f"✅ {test_name}") | ||
| 26 | - tests_passed += 1 | ||
| 27 | - except Exception as e: | ||
| 28 | - print(f"❌ {test_name} - 失败: {e}") | ||
| 29 | - traceback.print_exc() | ||
| 30 | - | ||
| 31 | - # 测试1: 基础模块导入 | ||
| 32 | - def test_imports(): | ||
| 33 | - from utils.logger import get_logger, setup_logging | ||
| 34 | - from context.request_context import create_request_context, RequestContextStage | ||
| 35 | - from query.query_parser import QueryParser | ||
| 36 | - assert get_logger is not None | ||
| 37 | - assert create_request_context is not None | ||
| 38 | - | ||
| 39 | - # 测试2: 日志系统 | ||
| 40 | - def test_logging(): | ||
| 41 | - from utils.logger import get_logger, setup_logging | ||
| 42 | - setup_logging(log_level="INFO", log_dir="verification_logs") | ||
| 43 | - logger = get_logger("verification") | ||
| 44 | - logger.info("测试消息", extra={'reqid': 'test', 'uid': 'user'}) | ||
| 45 | - | ||
| 46 | - # 测试3: 请求上下文创建 | ||
| 47 | - def test_context_creation(): | ||
| 48 | - from context.request_context import create_request_context | ||
| 49 | - context = create_request_context("req123", "user123") | ||
| 50 | - assert context.reqid == "req123" | ||
| 51 | - assert context.uid == "user123" | ||
| 52 | - | ||
| 53 | - # 测试4: 查询解析(这是之前出错的地方) | ||
| 54 | - def test_query_parsing(): | ||
| 55 | - from context.request_context import create_request_context | ||
| 56 | - from query.query_parser import QueryParser | ||
| 57 | - | ||
| 58 | - class TestConfig: | ||
| 59 | - class QueryConfig: | ||
| 60 | - enable_query_rewrite = False | ||
| 61 | - rewrite_dictionary = {} | ||
| 62 | - enable_translation = False | ||
| 63 | - supported_languages = ['en', 'zh'] | ||
| 64 | - enable_text_embedding = False | ||
| 65 | - query_config = QueryConfig() | ||
| 66 | - indexes = [] | ||
| 67 | - | ||
| 68 | - config = TestConfig() | ||
| 69 | - parser = QueryParser(config) | ||
| 70 | - context = create_request_context("req456", "user456") | ||
| 71 | - | ||
| 72 | - # 这之前会抛出 "Logger._log() got an unexpected keyword argument 'reqid'" 错误 | ||
| 73 | - result = parser.parse("test query", context=context, generate_vector=False) | ||
| 74 | - assert result.original_query == "test query" | ||
| 75 | - | ||
| 76 | - # 测试5: 完整的中文查询处理 | ||
| 77 | - def test_chinese_query(): | ||
| 78 | - from context.request_context import create_request_context | ||
| 79 | - from query.query_parser import QueryParser | ||
| 80 | - | ||
| 81 | - class TestConfig: | ||
| 82 | - class QueryConfig: | ||
| 83 | - enable_query_rewrite = True | ||
| 84 | - rewrite_dictionary = {'芭比娃娃': 'brand:芭比'} | ||
| 85 | - enable_translation = False | ||
| 86 | - supported_languages = ['en', 'zh'] | ||
| 87 | - enable_text_embedding = False | ||
| 88 | - query_config = QueryConfig() | ||
| 89 | - indexes = [] | ||
| 90 | - | ||
| 91 | - config = TestConfig() | ||
| 92 | - parser = QueryParser(config) | ||
| 93 | - context = create_request_context("req789", "user789") | ||
| 94 | - | ||
| 95 | - result = parser.parse("芭比娃娃", context=context, generate_vector=False) | ||
| 96 | - # 语言检测可能不准确,但查询应该正常处理 | ||
| 97 | - assert result.original_query == "芭比娃娃" | ||
| 98 | - assert "brand:芭比" in result.rewritten_query | ||
| 99 | - | ||
| 100 | - # 测试6: 性能摘要 | ||
| 101 | - def test_performance_summary(): | ||
| 102 | - from context.request_context import create_request_context, RequestContextStage | ||
| 103 | - | ||
| 104 | - context = create_request_context("req_perf", "user_perf") | ||
| 105 | - context.start_stage(RequestContextStage.TOTAL) | ||
| 106 | - context.start_stage(RequestContextStage.QUERY_PARSING) | ||
| 107 | - context.end_stage(RequestContextStage.QUERY_PARSING) | ||
| 108 | - context.end_stage(RequestContextStage.TOTAL) | ||
| 109 | - | ||
| 110 | - summary = context.get_summary() | ||
| 111 | - assert 'performance' in summary | ||
| 112 | - assert 'stage_timings_ms' in summary['performance'] | ||
| 113 | - | ||
| 114 | - # 运行所有测试 | ||
| 115 | - run_test("基础模块导入", test_imports) | ||
| 116 | - run_test("日志系统", test_logging) | ||
| 117 | - run_test("请求上下文创建", test_context_creation) | ||
| 118 | - run_test("查询解析(修复验证)", test_query_parsing) | ||
| 119 | - run_test("中文查询处理", test_chinese_query) | ||
| 120 | - run_test("性能摘要", test_performance_summary) | ||
| 121 | - | ||
| 122 | - # 输出结果 | ||
| 123 | - print("\n" + "=" * 60) | ||
| 124 | - print(f"📊 验证结果: {tests_passed}/{tests_total} 测试通过") | ||
| 125 | - | ||
| 126 | - if tests_passed == tests_total: | ||
| 127 | - print("🎉 所有验证通过!系统修复完成。") | ||
| 128 | - print("\n🔧 修复内容:") | ||
| 129 | - print(" - 修复了 utils/logger.py 中的日志参数处理") | ||
| 130 | - print(" - 修复了 context/request_context.py 中的日志调用格式") | ||
| 131 | - print(" - 修复了 query/query_parser.py 中的日志调用格式") | ||
| 132 | - print(" - 修复了 search/searcher.py 中的日志调用格式") | ||
| 133 | - print(" - 修复了 api/routes/search.py 中的日志调用格式") | ||
| 134 | - print("\n✅ 现在可以正常处理搜索请求,不会再出现 Logger._log() 错误。") | ||
| 135 | - return True | ||
| 136 | - else: | ||
| 137 | - print("💥 还有测试失败,需要进一步修复。") | ||
| 138 | - return False | ||
| 139 | - | ||
| 140 | -if __name__ == "__main__": | ||
| 141 | - success = run_verification() | ||
| 142 | - sys.exit(0 if success else 1) | ||
| 143 | \ No newline at end of file | 0 | \ No newline at end of file |
verify_refactoring.py deleted
| @@ -1,307 +0,0 @@ | @@ -1,307 +0,0 @@ | ||
| 1 | -#!/usr/bin/env python3 | ||
| 2 | -""" | ||
| 3 | -验证 API v3.0 重构是否完整 | ||
| 4 | -检查代码中是否还有旧的逻辑残留 | ||
| 5 | -""" | ||
| 6 | - | ||
| 7 | -import os | ||
| 8 | -import re | ||
| 9 | -from pathlib import Path | ||
| 10 | - | ||
| 11 | -def print_header(title): | ||
| 12 | - print(f"\n{'='*60}") | ||
| 13 | - print(f" {title}") | ||
| 14 | - print('='*60) | ||
| 15 | - | ||
| 16 | -def search_in_file(filepath, pattern, description): | ||
| 17 | - """在文件中搜索模式""" | ||
| 18 | - try: | ||
| 19 | - with open(filepath, 'r', encoding='utf-8') as f: | ||
| 20 | - content = f.read() | ||
| 21 | - matches = re.findall(pattern, content, re.MULTILINE) | ||
| 22 | - return matches | ||
| 23 | - except Exception as e: | ||
| 24 | - return None | ||
| 25 | - | ||
| 26 | -def check_removed_code(): | ||
| 27 | - """检查已移除的代码""" | ||
| 28 | - print_header("检查已移除的代码") | ||
| 29 | - | ||
| 30 | - checks = [ | ||
| 31 | - { | ||
| 32 | - "file": "search/es_query_builder.py", | ||
| 33 | - "pattern": r"if field == ['\"]price_ranges['\"]", | ||
| 34 | - "description": "硬编码的 price_ranges 逻辑", | ||
| 35 | - "should_exist": False | ||
| 36 | - }, | ||
| 37 | - { | ||
| 38 | - "file": "search/es_query_builder.py", | ||
| 39 | - "pattern": r"def add_dynamic_aggregations", | ||
| 40 | - "description": "add_dynamic_aggregations 方法", | ||
| 41 | - "should_exist": False | ||
| 42 | - }, | ||
| 43 | - { | ||
| 44 | - "file": "api/models.py", | ||
| 45 | - "pattern": r"aggregations.*Optional\[Dict", | ||
| 46 | - "description": "aggregations 参数(在 SearchRequest 中)", | ||
| 47 | - "should_exist": False | ||
| 48 | - }, | ||
| 49 | - { | ||
| 50 | - "file": "frontend/static/js/app.js", | ||
| 51 | - "pattern": r"price_ranges", | ||
| 52 | - "description": "前端硬编码的 price_ranges", | ||
| 53 | - "should_exist": False | ||
| 54 | - }, | ||
| 55 | - { | ||
| 56 | - "file": "frontend/static/js/app.js", | ||
| 57 | - "pattern": r"displayAggregations", | ||
| 58 | - "description": "旧的 displayAggregations 函数", | ||
| 59 | - "should_exist": False | ||
| 60 | - } | ||
| 61 | - ] | ||
| 62 | - | ||
| 63 | - all_passed = True | ||
| 64 | - for check in checks: | ||
| 65 | - filepath = os.path.join("/home/tw/SearchEngine", check["file"]) | ||
| 66 | - matches = search_in_file(filepath, check["pattern"], check["description"]) | ||
| 67 | - | ||
| 68 | - if matches is None: | ||
| 69 | - print(f" ⚠️ 无法读取:{check['file']}") | ||
| 70 | - continue | ||
| 71 | - | ||
| 72 | - if check["should_exist"]: | ||
| 73 | - if matches: | ||
| 74 | - print(f" ✓ 存在:{check['description']}") | ||
| 75 | - else: | ||
| 76 | - print(f" ✗ 缺失:{check['description']}") | ||
| 77 | - all_passed = False | ||
| 78 | - else: | ||
| 79 | - if matches: | ||
| 80 | - print(f" ✗ 仍存在:{check['description']}") | ||
| 81 | - print(f" 匹配:{matches[:2]}") | ||
| 82 | - all_passed = False | ||
| 83 | - else: | ||
| 84 | - print(f" ✓ 已移除:{check['description']}") | ||
| 85 | - | ||
| 86 | - return all_passed | ||
| 87 | - | ||
| 88 | -def check_new_code(): | ||
| 89 | - """检查新增的代码""" | ||
| 90 | - print_header("检查新增的代码") | ||
| 91 | - | ||
| 92 | - checks = [ | ||
| 93 | - { | ||
| 94 | - "file": "api/models.py", | ||
| 95 | - "pattern": r"class RangeFilter", | ||
| 96 | - "description": "RangeFilter 模型", | ||
| 97 | - "should_exist": True | ||
| 98 | - }, | ||
| 99 | - { | ||
| 100 | - "file": "api/models.py", | ||
| 101 | - "pattern": r"class FacetConfig", | ||
| 102 | - "description": "FacetConfig 模型", | ||
| 103 | - "should_exist": True | ||
| 104 | - }, | ||
| 105 | - { | ||
| 106 | - "file": "api/models.py", | ||
| 107 | - "pattern": r"class FacetValue", | ||
| 108 | - "description": "FacetValue 模型", | ||
| 109 | - "should_exist": True | ||
| 110 | - }, | ||
| 111 | - { | ||
| 112 | - "file": "api/models.py", | ||
| 113 | - "pattern": r"class FacetResult", | ||
| 114 | - "description": "FacetResult 模型", | ||
| 115 | - "should_exist": True | ||
| 116 | - }, | ||
| 117 | - { | ||
| 118 | - "file": "api/models.py", | ||
| 119 | - "pattern": r"range_filters.*RangeFilter", | ||
| 120 | - "description": "range_filters 参数", | ||
| 121 | - "should_exist": True | ||
| 122 | - }, | ||
| 123 | - { | ||
| 124 | - "file": "api/models.py", | ||
| 125 | - "pattern": r"facets.*FacetConfig", | ||
| 126 | - "description": "facets 参数", | ||
| 127 | - "should_exist": True | ||
| 128 | - }, | ||
| 129 | - { | ||
| 130 | - "file": "search/es_query_builder.py", | ||
| 131 | - "pattern": r"def build_facets", | ||
| 132 | - "description": "build_facets 方法", | ||
| 133 | - "should_exist": True | ||
| 134 | - }, | ||
| 135 | - { | ||
| 136 | - "file": "search/searcher.py", | ||
| 137 | - "pattern": r"def _standardize_facets", | ||
| 138 | - "description": "_standardize_facets 方法", | ||
| 139 | - "should_exist": True | ||
| 140 | - }, | ||
| 141 | - { | ||
| 142 | - "file": "api/routes/search.py", | ||
| 143 | - "pattern": r"@router.get\(['\"]\/suggestions", | ||
| 144 | - "description": "/search/suggestions 端点", | ||
| 145 | - "should_exist": True | ||
| 146 | - }, | ||
| 147 | - { | ||
| 148 | - "file": "api/routes/search.py", | ||
| 149 | - "pattern": r"@router.get\(['\"]\/instant", | ||
| 150 | - "description": "/search/instant 端点", | ||
| 151 | - "should_exist": True | ||
| 152 | - }, | ||
| 153 | - { | ||
| 154 | - "file": "frontend/static/js/app.js", | ||
| 155 | - "pattern": r"function displayFacets", | ||
| 156 | - "description": "displayFacets 函数", | ||
| 157 | - "should_exist": True | ||
| 158 | - }, | ||
| 159 | - { | ||
| 160 | - "file": "frontend/static/js/app.js", | ||
| 161 | - "pattern": r"rangeFilters", | ||
| 162 | - "description": "rangeFilters 状态", | ||
| 163 | - "should_exist": True | ||
| 164 | - } | ||
| 165 | - ] | ||
| 166 | - | ||
| 167 | - all_passed = True | ||
| 168 | - for check in checks: | ||
| 169 | - filepath = os.path.join("/home/tw/SearchEngine", check["file"]) | ||
| 170 | - matches = search_in_file(filepath, check["pattern"], check["description"]) | ||
| 171 | - | ||
| 172 | - if matches is None: | ||
| 173 | - print(f" ⚠️ 无法读取:{check['file']}") | ||
| 174 | - continue | ||
| 175 | - | ||
| 176 | - if check["should_exist"]: | ||
| 177 | - if matches: | ||
| 178 | - print(f" ✓ 存在:{check['description']}") | ||
| 179 | - else: | ||
| 180 | - print(f" ✗ 缺失:{check['description']}") | ||
| 181 | - all_passed = False | ||
| 182 | - else: | ||
| 183 | - if matches: | ||
| 184 | - print(f" ✗ 仍存在:{check['description']}") | ||
| 185 | - all_passed = False | ||
| 186 | - else: | ||
| 187 | - print(f" ✓ 已移除:{check['description']}") | ||
| 188 | - | ||
| 189 | - return all_passed | ||
| 190 | - | ||
| 191 | -def check_documentation(): | ||
| 192 | - """检查文档""" | ||
| 193 | - print_header("检查文档") | ||
| 194 | - | ||
| 195 | - docs = [ | ||
| 196 | - "API_DOCUMENTATION.md", | ||
| 197 | - "API_EXAMPLES.md", | ||
| 198 | - "MIGRATION_GUIDE_V3.md", | ||
| 199 | - "CHANGES.md" | ||
| 200 | - ] | ||
| 201 | - | ||
| 202 | - all_exist = True | ||
| 203 | - for doc in docs: | ||
| 204 | - filepath = os.path.join("/home/tw/SearchEngine", doc) | ||
| 205 | - if os.path.exists(filepath): | ||
| 206 | - size_kb = os.path.getsize(filepath) / 1024 | ||
| 207 | - print(f" ✓ 存在:{doc} ({size_kb:.1f} KB)") | ||
| 208 | - else: | ||
| 209 | - print(f" ✗ 缺失:{doc}") | ||
| 210 | - all_exist = False | ||
| 211 | - | ||
| 212 | - return all_exist | ||
| 213 | - | ||
| 214 | -def check_imports(): | ||
| 215 | - """检查模块导入""" | ||
| 216 | - print_header("检查模块导入") | ||
| 217 | - | ||
| 218 | - import sys | ||
| 219 | - sys.path.insert(0, '/home/tw/SearchEngine') | ||
| 220 | - | ||
| 221 | - try: | ||
| 222 | - from api.models import ( | ||
| 223 | - RangeFilter, FacetConfig, FacetValue, FacetResult, | ||
| 224 | - SearchRequest, SearchResponse, ImageSearchRequest, | ||
| 225 | - SearchSuggestRequest, SearchSuggestResponse | ||
| 226 | - ) | ||
| 227 | - print(" ✓ API 模型导入成功") | ||
| 228 | - | ||
| 229 | - from search.es_query_builder import ESQueryBuilder | ||
| 230 | - print(" ✓ ESQueryBuilder 导入成功") | ||
| 231 | - | ||
| 232 | - from search.searcher import Searcher, SearchResult | ||
| 233 | - print(" ✓ Searcher 导入成功") | ||
| 234 | - | ||
| 235 | - # 检查方法 | ||
| 236 | - qb = ESQueryBuilder('test', ['field1']) | ||
| 237 | - if hasattr(qb, 'build_facets'): | ||
| 238 | - print(" ✓ build_facets 方法存在") | ||
| 239 | - else: | ||
| 240 | - print(" ✗ build_facets 方法不存在") | ||
| 241 | - return False | ||
| 242 | - | ||
| 243 | - if hasattr(qb, 'add_dynamic_aggregations'): | ||
| 244 | - print(" ✗ add_dynamic_aggregations 方法仍存在(应该已删除)") | ||
| 245 | - return False | ||
| 246 | - else: | ||
| 247 | - print(" ✓ add_dynamic_aggregations 方法已删除") | ||
| 248 | - | ||
| 249 | - # 检查 SearchResult | ||
| 250 | - sr = SearchResult(hits=[], total=0, max_score=0, took_ms=10, facets=[]) | ||
| 251 | - if hasattr(sr, 'facets'): | ||
| 252 | - print(" ✓ SearchResult.facets 属性存在") | ||
| 253 | - else: | ||
| 254 | - print(" ✗ SearchResult.facets 属性不存在") | ||
| 255 | - return False | ||
| 256 | - | ||
| 257 | - if hasattr(sr, 'aggregations'): | ||
| 258 | - print(" ✗ SearchResult.aggregations 属性仍存在(应该已删除)") | ||
| 259 | - return False | ||
| 260 | - else: | ||
| 261 | - print(" ✓ SearchResult.aggregations 属性已删除") | ||
| 262 | - | ||
| 263 | - return True | ||
| 264 | - | ||
| 265 | - except Exception as e: | ||
| 266 | - print(f" ✗ 导入失败:{e}") | ||
| 267 | - return False | ||
| 268 | - | ||
| 269 | -def main(): | ||
| 270 | - """主函数""" | ||
| 271 | - print("\n" + "🔍 开始验证 API v3.0 重构") | ||
| 272 | - print(f"项目路径:/home/tw/SearchEngine\n") | ||
| 273 | - | ||
| 274 | - # 运行检查 | ||
| 275 | - check1 = check_removed_code() | ||
| 276 | - check2 = check_new_code() | ||
| 277 | - check3 = check_documentation() | ||
| 278 | - check4 = check_imports() | ||
| 279 | - | ||
| 280 | - # 总结 | ||
| 281 | - print_header("验证总结") | ||
| 282 | - | ||
| 283 | - results = { | ||
| 284 | - "已移除的代码": check1, | ||
| 285 | - "新增的代码": check2, | ||
| 286 | - "文档完整性": check3, | ||
| 287 | - "模块导入": check4 | ||
| 288 | - } | ||
| 289 | - | ||
| 290 | - all_passed = all(results.values()) | ||
| 291 | - | ||
| 292 | - for name, passed in results.items(): | ||
| 293 | - status = "✓ 通过" if passed else "✗ 失败" | ||
| 294 | - print(f" {status}: {name}") | ||
| 295 | - | ||
| 296 | - if all_passed: | ||
| 297 | - print(f"\n 🎉 所有检查通过!API v3.0 重构完成。") | ||
| 298 | - else: | ||
| 299 | - print(f"\n ⚠️ 部分检查失败,请检查上述详情。") | ||
| 300 | - | ||
| 301 | - print("\n" + "="*60 + "\n") | ||
| 302 | - | ||
| 303 | - return 0 if all_passed else 1 | ||
| 304 | - | ||
| 305 | -if __name__ == "__main__": | ||
| 306 | - exit(main()) | ||
| 307 | - |