Commit 37e994bb5b8670a2e32c69f8cfdb9f45ce4b6972

Authored by tangwang
1 parent 9cb7528e

命名修改、代码清理

api/routes/admin.py
... ... @@ -41,7 +41,7 @@ async def health_check():
41 41 @router.get("/config")
42 42 async def get_configuration():
43 43 """
44   - Get current customer configuration (sanitized).
  44 + Get current search configuration (sanitized).
45 45 """
46 46 try:
47 47 from ..app import get_config
... ...
indexer/bulk_indexer.py
... ... @@ -211,7 +211,7 @@ class IndexingPipeline:
211 211 Initialize indexing pipeline.
212 212  
213 213 Args:
214   - config: Customer configuration
  214 + config: Search configuration
215 215 es_client: Elasticsearch client
216 216 data_transformer: Data transformer instance
217 217 recreate_index: Whether to recreate index if exists
... ...
search/multilang_query_builder.py
... ... @@ -36,7 +36,7 @@ class MultiLanguageQueryBuilder(ESQueryBuilder):
36 36 Initialize multi-language query builder.
37 37  
38 38 Args:
39   - config: Customer configuration
  39 + config: Search configuration
40 40 index_name: ES index name
41 41 text_embedding_field: Field name for text embeddings
42 42 image_embedding_field: Field name for image embeddings
... ...
test_all.sh deleted
... ... @@ -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 0 \ No newline at end of file
test_data_base.sql deleted
... ... @@ -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   -#!/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   -#!/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 0 \ No newline at end of file
test_source_fields.py deleted
... ... @@ -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 0 \ No newline at end of file
tests/integration/test_aggregation_api.py deleted
... ... @@ -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 0 \ No newline at end of file
tests/integration/test_api_integration.py deleted
... ... @@ -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 0 \ No newline at end of file
tests/integration/test_search_integration.py deleted
... ... @@ -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 0 \ No newline at end of file
tests/unit/test_context.py deleted
... ... @@ -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 0 \ No newline at end of file
tests/unit/test_query_parser.py deleted
... ... @@ -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 0 \ No newline at end of file
tests/unit/test_searcher.py deleted
... ... @@ -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 0 \ No newline at end of file
verification_report.py deleted
... ... @@ -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 0 \ No newline at end of file
verify_refactoring.py deleted
... ... @@ -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   -