Commit 37e994bb5b8670a2e32c69f8cfdb9f45ce4b6972

Authored by tangwang
1 parent 9cb7528e

命名修改、代码清理

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