From 37e994bb5b8670a2e32c69f8cfdb9f45ce4b6972 Mon Sep 17 00:00:00 2001 From: tangwang Date: Thu, 13 Nov 2025 15:18:35 +0800 Subject: [PATCH] 命名修改、代码清理 --- api/routes/admin.py | 2 +- indexer/bulk_indexer.py | 2 +- search/multilang_query_builder.py | 2 +- test_all.sh | 155 ----------------------------------------------------------------------------------------------------------------------------------------------------------- test_data_base.sql | 69 --------------------------------------------------------------------- test_new_api.py | 420 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ test_search_with_source_fields.py | 147 --------------------------------------------------------------------------------------------------------------------------------------------------- test_source_fields.py | 132 ------------------------------------------------------------------------------------------------------------------------------------ tests/integration/test_aggregation_api.py | 256 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- tests/integration/test_api_integration.py | 338 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- tests/integration/test_search_integration.py | 297 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- tests/unit/test_context.py | 228 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ tests/unit/test_query_parser.py | 270 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ tests/unit/test_searcher.py | 242 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- verification_report.py | 142 ---------------------------------------------------------------------------------------------------------------------------------------------- verify_refactoring.py | 307 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 16 files changed, 3 insertions(+), 3006 deletions(-) delete mode 100755 test_all.sh delete mode 100644 test_data_base.sql delete mode 100755 test_new_api.py delete mode 100644 test_search_with_source_fields.py delete mode 100644 test_source_fields.py delete mode 100644 tests/integration/test_aggregation_api.py delete mode 100644 tests/integration/test_api_integration.py delete mode 100644 tests/integration/test_search_integration.py delete mode 100644 tests/unit/test_context.py delete mode 100644 tests/unit/test_query_parser.py delete mode 100644 tests/unit/test_searcher.py delete mode 100644 verification_report.py delete mode 100755 verify_refactoring.py diff --git a/api/routes/admin.py b/api/routes/admin.py index 1b889ac..9a80127 100644 --- a/api/routes/admin.py +++ b/api/routes/admin.py @@ -41,7 +41,7 @@ async def health_check(): @router.get("/config") async def get_configuration(): """ - Get current customer configuration (sanitized). + Get current search configuration (sanitized). """ try: from ..app import get_config diff --git a/indexer/bulk_indexer.py b/indexer/bulk_indexer.py index 4095710..291e996 100644 --- a/indexer/bulk_indexer.py +++ b/indexer/bulk_indexer.py @@ -211,7 +211,7 @@ class IndexingPipeline: Initialize indexing pipeline. Args: - config: Customer configuration + config: Search configuration es_client: Elasticsearch client data_transformer: Data transformer instance recreate_index: Whether to recreate index if exists diff --git a/search/multilang_query_builder.py b/search/multilang_query_builder.py index 2338f19..8ef0cbb 100644 --- a/search/multilang_query_builder.py +++ b/search/multilang_query_builder.py @@ -36,7 +36,7 @@ class MultiLanguageQueryBuilder(ESQueryBuilder): Initialize multi-language query builder. Args: - config: Customer configuration + config: Search configuration index_name: ES index name text_embedding_field: Field name for text embeddings image_embedding_field: Field name for image embeddings diff --git a/test_all.sh b/test_all.sh deleted file mode 100755 index 8d7200d..0000000 --- a/test_all.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/bash - -# Complete test script for SearchEngine -# This script performs full testing including data ingestion and service restart - -cd "$(dirname "$0")" - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN}SearchEngine完整测试脚本${NC}" -echo -e "${GREEN}========================================${NC}" - -# Step 1: Setup environment -echo -e "\n${YELLOW}Step 1/4: 设置环境${NC}" -if [ -f "./setup.sh" ]; then - ./setup.sh - if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ 环境设置完成${NC}" - else - echo -e "${RED}✗ 环境设置失败${NC}" - exit 1 - fi -else - echo -e "${YELLOW}⚠ setup脚本不存在,跳过环境设置${NC}" -fi - -# Step 2: Check and ingest data if needed -echo -e "\n${YELLOW}Step 2/4: 检查并准备数据${NC}" -source /home/tw/miniconda3/etc/profile.d/conda.sh -conda activate searchengine - -# Check if index exists -INDEX_EXISTS=$(python -c " -from config.env_config import get_es_config -from utils.es_client import ESClient -from config import ConfigLoader - -try: - es_config = get_es_config() - es_client = ESClient(hosts=[es_config['host']], username=es_config.get('username'), password=es_config.get('password')) - - config_loader = ConfigLoader('config/config.yaml') - config = config_loader.load_config() - - if es_client.index_exists(config.es_index_name): - doc_count = es_client.count(config.es_index_name) - print(f'{doc_count}') - else: - print('0') -except Exception as e: - print(f'0') -" 2>/dev/null || echo "0") - -if [ "$INDEX_EXISTS" = "0" ]; then - echo -e "${YELLOW}索引不存在,开始导入数据...${NC}" - echo -e "${YELLOW}注意: 首次导入会下载模型文件,可能需要10-30分钟${NC}" - echo -e "${YELLOW}导入1000条数据进行快速测试(跳过embedding以加快速度)${NC}" - - if [ -f "./scripts/ingest.sh" ]; then - ./scripts/ingest.sh 1000 true - if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ 数据导入完成${NC}" - else - echo -e "${RED}✗ 数据导入失败${NC}" - exit 1 - fi - else - echo -e "${RED}✗ 数据导入脚本不存在${NC}" - exit 1 - fi -else - echo -e "${GREEN}✓ 数据已存在,包含 $INDEX_EXISTS 条文档${NC}" -fi - -# Step 3: Restart services (stop first, then start) -echo -e "\n${YELLOW}Step 3/4: 重启服务${NC}" -if [ -f "./restart.sh" ]; then - ./restart.sh - if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ 服务重启完成${NC}" - else - echo -e "${RED}✗ 服务重启失败${NC}" - exit 1 - fi -else - echo -e "${RED}✗ 重启脚本不存在${NC}" - exit 1 -fi - -# Step 4: Test the services -echo -e "\n${YELLOW}Step 4/4: 测试服务${NC}" -sleep 3 - -# Test backend health -echo -e "${YELLOW}测试后端服务健康状态...${NC}" -if curl -s http://localhost:6002/admin/health > /dev/null 2>&1; then - echo -e "${GREEN}✓ 后端服务健康检查通过${NC}" -else - echo -e "${YELLOW}⚠ 后端服务健康检查失败,但服务可能仍在启动${NC}" -fi - -# Test frontend -echo -e "${YELLOW}测试前端服务...${NC}" -if curl -s http://localhost:6003/ > /dev/null 2>&1; then - echo -e "${GREEN}✓ 前端服务可访问${NC}" -else - echo -e "${YELLOW}⚠ 前端服务可能还在启动中${NC}" -fi - -# Test API endpoint -echo -e "${YELLOW}测试API端点...${NC}" -API_TEST_RESULT=$(curl -s -X GET "http://localhost:6002/search?q=test&size=5" 2>/dev/null | python -c " -import json -import sys -try: - data = json.load(sys.stdin) - if 'results' in data and len(data['results']) > 0: - print('API_TEST_OK') - else: - print('API_TEST_EMPTY') -except: - print('API_TEST_ERROR') -" 2>/dev/null || echo "API_TEST_ERROR") - -case $API_TEST_RESULT in - "API_TEST_OK") - echo -e "${GREEN}✓ API测试通过,返回搜索结果${NC}" - ;; - "API_TEST_EMPTY") - echo -e "${YELLOW}⚠ API测试通过,但返回空结果${NC}" - ;; - *) - echo -e "${YELLOW}⚠ API测试失败,服务可能还在启动中${NC}" - ;; -esac - -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN}完整测试流程结束!${NC}" -echo -e "${GREEN}========================================${NC}" -echo "" -echo -e "服务状态:" -echo -e " ${GREEN}前端界面: http://localhost:6003${NC}" -echo -e " ${GREEN}后端API: http://localhost:6002${NC}" -echo -e " ${GREEN}API文档: http://localhost:6002/docs${NC}" -echo "" -echo -e "可用脚本:" -echo -e " ${YELLOW}./run.sh${NC} - 仅启动服务(不导入数据)" -echo -e " ${YELLOW}./stop.sh${NC} - 停止所有服务" -echo -e " ${YELLOW}./restart.sh${NC} - 重启所有服务" -echo -e " ${YELLOW}./test_all.sh${NC}- 完整测试(包含数据导入)" -echo "" \ No newline at end of file diff --git a/test_data_base.sql b/test_data_base.sql deleted file mode 100644 index c97a400..0000000 --- a/test_data_base.sql +++ /dev/null @@ -1,69 +0,0 @@ --- SPU Test Data -INSERT INTO shoplazza_product_spu ( - id, shop_id, shoplazza_id, handle, title, brief, description, spu, - vendor, vendor_url, seo_title, seo_description, seo_keywords, - image_src, image_width, image_height, image_path, image_alt, - inventory_policy, inventory_quantity, inventory_tracking, - published, published_at, requires_shipping, taxable, - fake_sales, display_fake_sales, mixed_wholesale, need_variant_image, - has_only_default_variant, tags, note, category, - shoplazza_created_at, shoplazza_updated_at, tenant_id, - creator, create_time, updater, update_time, deleted -) VALUES -(1, 1, 'spu-1', 'product-1', '音响 海尔', '蓝牙无线音响', '

蓝牙无线音响,来自海尔品牌。Bluetooth wireless speaker

', '', '海尔', '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), -(2, 1, 'spu-2', 'product-2', '无线鼠标 华为', '人体工学无线鼠标', '

人体工学无线鼠标,来自华为品牌。Ergonomic wireless mouse

', '', '华为', '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), -(3, 1, 'spu-3', 'product-3', '显示器 Sony', '4K高清显示器', '

4K高清显示器,来自Sony品牌。4K high-definition monitor

', '', '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), -(4, 1, 'spu-4', 'product-4', '蓝牙耳机 Apple', '高品质无线蓝牙耳机', '

高品质无线蓝牙耳机,来自Apple品牌。High-quality wireless Bluetooth headphone

', '', '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), -(5, 1, 'spu-5', 'product-5', '智能手表 海尔', '多功能智能手表', '

多功能智能手表,来自海尔品牌。Multi-function smart watch

', '', '海尔', '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), -(6, 1, 'spu-6', 'product-6', '无线鼠标 Samsung', '人体工学无线鼠标', '

人体工学无线鼠标,来自Samsung品牌。Ergonomic wireless mouse

', '', '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), -(7, 1, 'spu-7', 'product-7', '显示器 美的', '4K高清显示器', '

4K高清显示器,来自美的品牌。4K high-definition monitor

', '', '美的', '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), -(8, 1, 'spu-8', 'product-8', '智能手机 华为', '高性能智能手机', '

高性能智能手机,来自华为品牌。High-performance smartphone

', '', '华为', '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), -(9, 1, 'spu-9', 'product-9', '运动鞋 Apple', '舒适透气的运动鞋', '

舒适透气的运动鞋,来自Apple品牌。Comfortable and breathable running shoes

', '', '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), -(10, 1, 'spu-10', 'product-10', '机械键盘 Sony', 'RGB背光机械键盘', '

RGB背光机械键盘,来自Sony品牌。RGB backlit mechanical keyboard

', '', '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); - --- SKU Test Data -INSERT INTO shoplazza_product_sku ( - id, spu_id, shop_id, shoplazza_id, shoplazza_product_id, shoplazza_image_id, - title, sku, barcode, position, price, compare_at_price, cost_price, - option1, option2, option3, inventory_quantity, weight, weight_unit, - image_src, wholesale_price, note, extend, - shoplazza_created_at, shoplazza_updated_at, tenant_id, - creator, create_time, updater, update_time, deleted -) VALUES -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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), -(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); diff --git a/test_new_api.py b/test_new_api.py deleted file mode 100755 index 1ac1682..0000000 --- a/test_new_api.py +++ /dev/null @@ -1,420 +0,0 @@ -#!/usr/bin/env python3 -""" -测试新的 API 接口(v3.0) -验证重构后的过滤器、分面搜索等功能 -""" - -import requests -import json - -API_BASE_URL = 'http://120.76.41.98:6002' - -def print_section(title): - """打印章节标题""" - print("\n" + "="*60) - print(f" {title}") - print("="*60) - -def test_simple_search(): - """测试1:简单搜索""" - print_section("测试1:简单搜索") - - payload = { - "query": "玩具", - "size": 5 - } - - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload) - - if response.ok: - data = response.json() - print(f"✓ 成功:找到 {data['total']} 个结果,耗时 {data['took_ms']}ms") - print(f" 响应键:{list(data.keys())}") - print(f" 是否有 facets 字段:{'facets' in data}") - print(f" 是否有 aggregations 字段(应该没有):{'aggregations' in data}") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_range_filters(): - """测试2:范围过滤器""" - print_section("测试2:范围过滤器") - - payload = { - "query": "玩具", - "size": 5, - "range_filters": { - "price": { - "gte": 50, - "lte": 200 - } - } - } - - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload) - - if response.ok: - data = response.json() - print(f"✓ 成功:找到 {data['total']} 个结果") - - # 检查价格范围 - print(f"\n 前3个结果的价格:") - for i, hit in enumerate(data['hits'][:3]): - price = hit['_source'].get('price', 'N/A') - print(f" {i+1}. {hit['_source'].get('name', 'N/A')}: ¥{price}") - if isinstance(price, (int, float)) and (price < 50 or price > 200): - print(f" ⚠️ 警告:价格 {price} 不在范围内") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_combined_filters(): - """测试3:组合过滤器""" - print_section("测试3:组合过滤器(精确+范围)") - - payload = { - "query": "玩具", - "size": 5, - "filters": { - "categoryName_keyword": ["玩具"] - }, - "range_filters": { - "price": { - "gte": 50, - "lte": 100 - } - } - } - - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload) - - if response.ok: - data = response.json() - print(f"✓ 成功:找到 {data['total']} 个结果") - - print(f"\n 前3个结果:") - for i, hit in enumerate(data['hits'][:3]): - source = hit['_source'] - print(f" {i+1}. {source.get('name', 'N/A')}") - print(f" 类目:{source.get('categoryName', 'N/A')}") - print(f" 价格:¥{source.get('price', 'N/A')}") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_facets_simple(): - """测试4:分面搜索(简单模式)""" - print_section("测试4:分面搜索(简单模式)") - - payload = { - "query": "玩具", - "size": 10, - "facets": ["categoryName_keyword", "brandName_keyword"] - } - - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload) - - if response.ok: - data = response.json() - print(f"✓ 成功:找到 {data['total']} 个结果") - - if data.get('facets'): - print(f"\n ✓ 分面结果(标准化格式):") - for facet in data['facets']: - print(f"\n {facet['label']} ({facet['field']}):") - print(f" 类型:{facet['type']}") - print(f" 分面值数量:{len(facet['values'])}") - for value in facet['values'][:3]: - selected_mark = "✓" if value['selected'] else " " - print(f" [{selected_mark}] {value['label']}: {value['count']}") - else: - print(f" ⚠️ 警告:没有返回分面结果") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_facets_advanced(): - """测试5:分面搜索(高级模式)""" - print_section("测试5:分面搜索(高级模式)") - - payload = { - "query": "玩具", - "size": 10, - "facets": [ - { - "field": "categoryName_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "brandName_keyword", - "size": 15, - "type": "terms" - }, - { - "field": "price", - "type": "range", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - ] - } - - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload) - - if response.ok: - data = response.json() - print(f"✓ 成功:找到 {data['total']} 个结果") - - if data.get('facets'): - print(f"\n ✓ 分面结果:") - for facet in data['facets']: - print(f"\n {facet['label']} ({facet['type']}):") - for value in facet['values']: - print(f" {value['value']}: {value['count']}") - else: - print(f" ⚠️ 警告:没有返回分面结果") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_complete_scenario(): - """测试6:完整场景(过滤+分面+排序)""" - print_section("测试6:完整场景") - - payload = { - "query": "玩具", - "size": 10, - "filters": { - "categoryName_keyword": ["玩具"] - }, - "range_filters": { - "price": { - "gte": 50, - "lte": 200 - } - }, - "facets": [ - {"field": "brandName_keyword", "size": 10}, - {"field": "supplierName_keyword", "size": 10} - ], - "sort_by": "price", - "sort_order": "asc" - } - - print(f"请求:{json.dumps(payload, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload) - - if response.ok: - data = response.json() - print(f"✓ 成功:找到 {data['total']} 个结果") - - print(f"\n 前5个结果(按价格升序):") - for i, hit in enumerate(data['hits'][:5]): - source = hit['_source'] - print(f" {i+1}. {source.get('name', 'N/A')}: ¥{source.get('price', 'N/A')}") - - if data.get('facets'): - print(f"\n 分面统计:") - for facet in data['facets']: - print(f" {facet['label']}: {len(facet['values'])} 个值") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_search_suggestions(): - """测试7:搜索建议(框架)""" - print_section("测试7:搜索建议(框架)") - - url = f"{API_BASE_URL}/search/suggestions?q=芭&size=5" - print(f"请求:GET {url}") - - try: - response = requests.get(url) - - if response.ok: - data = response.json() - print(f"✓ 成功:返回 {len(data['suggestions'])} 个建议") - print(f" 响应:{json.dumps(data, indent=2, ensure_ascii=False)}") - print(f" ℹ️ 注意:此功能暂未实现,仅返回框架响应") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_instant_search(): - """测试8:即时搜索(框架)""" - print_section("测试8:即时搜索(框架)") - - url = f"{API_BASE_URL}/search/instant?q=玩具&size=5" - print(f"请求:GET {url}") - - try: - response = requests.get(url) - - if response.ok: - data = response.json() - print(f"✓ 成功:找到 {data['total']} 个结果") - print(f" 响应键:{list(data.keys())}") - print(f" ℹ️ 注意:此功能暂未实现,调用标准搜索") - else: - print(f"✗ 失败:{response.status_code}") - print(f" 错误:{response.text}") - except Exception as e: - print(f"✗ 异常:{e}") - - -def test_backward_compatibility(): - """测试9:确认旧接口已移除""" - print_section("测试9:确认旧接口已移除") - - # 测试旧的 price_ranges 参数 - payload_old = { - "query": "玩具", - "filters": { - "price_ranges": ["0-50", "50-100"] # 旧格式 - } - } - - print(f"测试旧的 price_ranges 格式:") - print(f"请求:{json.dumps(payload_old, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload_old) - data = response.json() - - # 应该被当作普通过滤器处理(无效果)或报错 - print(f" 状态:{response.status_code}") - print(f" 结果数:{data.get('total', 'N/A')}") - print(f" ℹ️ 旧的 price_ranges 已不再特殊处理") - except Exception as e: - print(f" 异常:{e}") - - # 测试旧的 aggregations 参数 - payload_old_agg = { - "query": "玩具", - "aggregations": { - "category_stats": { - "terms": { - "field": "categoryName_keyword", - "size": 10 - } - } - } - } - - print(f"\n测试旧的 aggregations 格式:") - print(f"请求:{json.dumps(payload_old_agg, indent=2, ensure_ascii=False)}") - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload_old_agg) - - if response.ok: - print(f" ⚠️ 警告:请求成功,但 aggregations 参数应该已被移除") - else: - print(f" ✓ 正确:旧参数已不被接受({response.status_code})") - except Exception as e: - print(f" 异常:{e}") - - -def test_validation(): - """测试10:参数验证""" - print_section("测试10:参数验证") - - # 测试空的 range_filter - print("测试空的 range_filter(应该报错):") - payload_invalid = { - "query": "玩具", - "range_filters": { - "price": {} # 空对象 - } - } - - try: - response = requests.post(f"{API_BASE_URL}/search/", json=payload_invalid) - if response.ok: - print(f" ⚠️ 警告:应该验证失败但成功了") - else: - print(f" ✓ 正确:验证失败({response.status_code})") - print(f" 错误信息:{response.json().get('detail', 'N/A')}") - except Exception as e: - print(f" 异常:{e}") - - -def test_summary(): - """测试总结""" - print_section("测试总结") - - print("重构验证清单:") - print(" ✓ 新的 range_filters 参数工作正常") - print(" ✓ 新的 facets 参数工作正常") - print(" ✓ 标准化的 facets 响应格式") - print(" ✓ 旧的 price_ranges 硬编码已移除") - print(" ✓ 旧的 aggregations 参数已移除") - print(" ✓ 新的 /search/suggestions 端点已添加") - print(" ✓ 新的 /search/instant 端点已添加") - print("\n 🎉 API v3.0 重构完成!") - - -if __name__ == "__main__": - print("\n" + "🚀 开始测试新 API(v3.0)") - print(f"API 地址:{API_BASE_URL}\n") - - # 运行所有测试 - test_simple_search() - test_range_filters() - test_combined_filters() - test_facets_simple() - test_facets_advanced() - test_complete_scenario() - test_search_suggestions() - test_instant_search() - test_backward_compatibility() - test_validation() - test_summary() - - print("\n" + "="*60) - print(" 测试完成!") - print("="*60 + "\n") - diff --git a/test_search_with_source_fields.py b/test_search_with_source_fields.py deleted file mode 100644 index b5c7897..0000000 --- a/test_search_with_source_fields.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -""" -测试实际搜索功能中的source_fields应用 -""" - -import sys -import os -import json -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from config import ConfigLoader - -def test_search_query_structure(): - """测试搜索查询是否正确应用了source_fields""" - print("测试搜索查询中的source_fields应用...") - - try: - from search.searcher import Searcher - from utils.es_client import ESClient - - # 加载配置 - config_loader = ConfigLoader("config/schema") - config = config_loader.load_customer_config("customer1") - - print(f"✓ 配置加载成功: {config.customer_id}") - print(f" source_fields配置数量: {len(config.query_config.source_fields)}") - - # 创建ES客户端(使用模拟客户端避免实际连接) - class MockESClient: - def search(self, index_name, body, size=10, from_=0): - print(f"模拟ES搜索 - 索引: {index_name}") - print(f"查询body结构:") - print(json.dumps(body, indent=2, ensure_ascii=False)) - - # 检查_source配置 - if "_source" in body: - print("✓ 查询包含_source配置") - source_config = body["_source"] - if "includes" in source_config: - print(f"✓ source includes字段: {source_config['includes']}") - return { - 'took': 5, - 'hits': { - 'total': {'value': 0}, - 'max_score': 0.0, - 'hits': [] - } - } - else: - print("✗ _source配置中缺少includes") - return None - else: - print("✗ 查询中缺少_source配置") - return None - - def client(self): - return self - - # 创建Searcher实例 - es_client = MockESClient() - searcher = Searcher(config, es_client) - - print("\n测试文本搜索...") - result = searcher.search("test query", size=5) - - if result: - print("✓ 文本搜索测试成功") - else: - print("✗ 文本搜索测试失败") - - print("\n测试图像搜索...") - try: - result = searcher.search_by_image("http://example.com/image.jpg", size=3) - if result: - print("✓ 图像搜索测试成功") - else: - print("✗ 图像搜索测试失败") - except Exception as e: - print(f"✗ 图像搜索测试失败: {e}") - - return True - - except Exception as e: - print(f"✗ 搜索测试失败: {e}") - import traceback - traceback.print_exc() - return False - -def test_es_query_builder_integration(): - """测试ES查询构建器的集成""" - print("\n测试ES查询构建器集成...") - - try: - from search.es_query_builder import ESQueryBuilder - - # 创建构建器,传入空的source_fields列表 - builder = ESQueryBuilder( - index_name="test_index", - match_fields=["title", "content"], - source_fields=None # 测试空配置的情况 - ) - - query = builder.build_query("test query") - - if "_source" not in query: - print("✓ 空source_fields配置下,查询不包含_source过滤") - else: - print("⚠ 空source_fields配置下,查询仍然包含_source过滤") - - # 测试非空配置 - builder2 = ESQueryBuilder( - index_name="test_index", - match_fields=["title", "content"], - source_fields=["id", "title"] - ) - - query2 = builder2.build_query("test query") - - if "_source" in query2 and "includes" in query2["_source"]: - print("✓ 非空source_fields配置下,查询正确包含_source过滤") - else: - print("✗ 非空source_fields配置下,查询缺少_source过滤") - - return True - - except Exception as e: - print(f"✗ 查询构建器集成测试失败: {e}") - return False - -if __name__ == "__main__": - print("=" * 60) - print("搜索功能source_fields应用测试") - print("=" * 60) - - success = True - - # 运行所有测试 - success &= test_es_query_builder_integration() - success &= test_search_query_structure() - - print("\n" + "=" * 60) - if success: - print("✓ 所有测试通过!source_fields在搜索功能中正确应用。") - print("✓ ES现在只返回配置中指定的字段,减少了网络传输和响应大小。") - else: - print("✗ 部分测试失败,请检查实现。") - print("=" * 60) \ No newline at end of file diff --git a/test_source_fields.py b/test_source_fields.py deleted file mode 100644 index e4897ce..0000000 --- a/test_source_fields.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 -""" -测试ES source_fields配置的脚本 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from config import ConfigLoader, SearchConfig - -def test_source_fields_config(): - """测试source_fields配置是否正确加载""" - print("测试ES source_fields配置...") - - # 加载配置 - config_loader = ConfigLoader("config/schema") - - try: - # 加载customer1配置 - config = config_loader.load_customer_config("customer1") - print(f"✓ 成功加载配置: {config.customer_id}") - - # 检查source_fields配置 - source_fields = config.query_config.source_fields - print(f"✓ source_fields配置 ({len(source_fields)}个字段):") - for i, field in enumerate(source_fields, 1): - print(f" {i:2d}. {field}") - - # 检查默认字段列表是否包含预期字段 - expected_fields = ["id", "title", "brandName", "price", "image"] - for field in expected_fields: - if field in source_fields: - print(f"✓ 包含预期字段: {field}") - else: - print(f"⚠ 缺少预期字段: {field}") - - return True - - except Exception as e: - print(f"✗ 配置加载失败: {e}") - return False - -def test_es_query_builder(): - """测试ES查询构建器是否正确应用source_fields""" - print("\n测试ES查询构建器...") - - try: - from search.es_query_builder import ESQueryBuilder - - # 测试基础查询构建器 - builder = ESQueryBuilder( - index_name="test_index", - match_fields=["title", "content"], - source_fields=["id", "title", "price"] - ) - - # 构建查询 - query = builder.build_query("test query") - - print("✓ ES查询构建成功") - print(f"查询结构:") - print(f" size: {query.get('size')}") - print(f" _source: {query.get('_source')}") - - # 检查_source配置 - if "_source" in query: - source_config = query["_source"] - if "includes" in source_config: - print(f"✓ _source includes配置正确: {source_config['includes']}") - else: - print("✗ _source配置中缺少includes字段") - else: - print("✗ 查询中缺少_source配置") - - return True - - except Exception as e: - print(f"✗ ES查询构建器测试失败: {e}") - import traceback - traceback.print_exc() - return False - -def test_multilang_query_builder(): - """测试多语言查询构建器""" - print("\n测试多语言查询构建器...") - - try: - from search.multilang_query_builder import MultiLanguageQueryBuilder - - # 加载配置 - config_loader = ConfigLoader("config/schema") - config = config_loader.load_customer_config("customer1") - - # 创建多语言查询构建器 - builder = MultiLanguageQueryBuilder( - config=config, - index_name=config.es_index_name, - text_embedding_field="text_embedding", - image_embedding_field="image_embedding", - source_fields=config.query_config.source_fields - ) - - print("✓ 多语言查询构建器创建成功") - print(f" source_fields配置: {builder.source_fields}") - - return True - - except Exception as e: - print(f"✗ 多语言查询构建器测试失败: {e}") - import traceback - traceback.print_exc() - return False - -if __name__ == "__main__": - print("=" * 60) - print("ES Source Fields 配置测试") - print("=" * 60) - - success = True - - # 运行所有测试 - success &= test_source_fields_config() - success &= test_es_query_builder() - success &= test_multilang_query_builder() - - print("\n" + "=" * 60) - if success: - print("✓ 所有测试通过!source_fields配置已正确实现。") - else: - print("✗ 部分测试失败,请检查配置和代码。") - print("=" * 60) \ No newline at end of file diff --git a/tests/integration/test_aggregation_api.py b/tests/integration/test_aggregation_api.py deleted file mode 100644 index 8540533..0000000 --- a/tests/integration/test_aggregation_api.py +++ /dev/null @@ -1,256 +0,0 @@ -""" -Tests for aggregation API functionality. -""" - -import pytest -from fastapi.testclient import TestClient -from api.app import app - -client = TestClient(app) - - -@pytest.mark.integration -@pytest.mark.api -def test_search_with_aggregations(): - """Test search with dynamic aggregations.""" - request_data = { - "query": "芭比娃娃", - "size": 10, - "aggregations": { - "category_name": { - "type": "terms", - "field": "categoryName_keyword", - "size": 10 - }, - "brand_name": { - "type": "terms", - "field": "brandName_keyword", - "size": 10 - }, - "price_ranges": { - "type": "range", - "field": "price", - "ranges": [ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100}, - {"key": "100-200", "from": 100, "to": 200}, - {"key": "200+", "from": 200} - ] - } - } - } - - response = client.post("/search/", json=request_data) - - assert response.status_code == 200 - data = response.json() - - # Check basic search response structure - assert "hits" in data - assert "total" in data - assert "aggregations" in data - assert "query_info" in data - - # Check aggregations structure - aggregations = data["aggregations"] - - # Should have category aggregations - if "category_name" in aggregations: - assert "buckets" in aggregations["category_name"] - assert isinstance(aggregations["category_name"]["buckets"], list) - - # Should have brand aggregations - if "brand_name" in aggregations: - assert "buckets" in aggregations["brand_name"] - assert isinstance(aggregations["brand_name"]["buckets"], list) - - # Should have price range aggregations - if "price_ranges" in aggregations: - assert "buckets" in aggregations["price_ranges"] - assert isinstance(aggregations["price_ranges"]["buckets"], list) - - -@pytest.mark.integration -@pytest.mark.api -def test_search_with_sorting(): - """Test search with different sorting options.""" - - # Test price ascending - request_data = { - "query": "玩具", - "size": 5, - "sort_by": "price_asc" - } - - response = client.post("/search/", json=request_data) - assert response.status_code == 200 - data = response.json() - - if data["hits"] and len(data["hits"]) > 1: - # Check if results are sorted by price (ascending) - prices = [] - for hit in data["hits"]: - if "_source" in hit and "price" in hit["_source"]: - prices.append(hit["_source"]["price"]) - - if len(prices) > 1: - assert prices == sorted(prices), "Results should be sorted by price ascending" - - # Test price descending - request_data["sort_by"] = "price_desc" - response = client.post("/search/", json=request_data) - assert response.status_code == 200 - data = response.json() - - if data["hits"] and len(data["hits"]) > 1: - prices = [] - for hit in data["hits"]: - if "_source" in hit and "price" in hit["_source"]: - prices.append(hit["_source"]["price"]) - - if len(prices) > 1: - assert prices == sorted(prices, reverse=True), "Results should be sorted by price descending" - - # Test time descending - request_data["sort_by"] = "time_desc" - response = client.post("/search/", json=request_data) - assert response.status_code == 200 - data = response.json() - - if data["hits"] and len(data["hits"]) > 1: - times = [] - for hit in data["hits"]: - if "_source" in hit and "create_time" in hit["_source"]: - times.append(hit["_source"]["create_time"]) - - if len(times) > 1: - # Newer items should come first - assert times == sorted(times, reverse=True), "Results should be sorted by time descending" - - -@pytest.mark.integration -@pytest.mark.api -def test_search_with_filters_and_aggregations(): - """Test search with filters and aggregations together.""" - request_data = { - "query": "玩具", - "size": 10, - "filters": { - "category_name": ["芭比"] - }, - "aggregations": { - "brand_name": { - "type": "terms", - "field": "brandName_keyword", - "size": 10 - } - } - } - - response = client.post("/search/", json=request_data) - assert response.status_code == 200 - data = response.json() - - # Check that results are filtered - assert "hits" in data - for hit in data["hits"]: - if "_source" in hit and "categoryName" in hit["_source"]: - assert "芭比" in hit["_source"]["categoryName"] - - # Check that aggregations are still present - assert "aggregations" in data - - -@pytest.mark.integration -@pytest.mark.api -def test_search_without_aggregations(): - """Test search without aggregations (default behavior).""" - request_data = { - "query": "玩具", - "size": 10 - } - - response = client.post("/search/", json=request_data) - assert response.status_code == 200 - data = response.json() - - # Should still have basic response structure - assert "hits" in data - assert "total" in data - assert "query_info" in data - - # Aggregations might be empty or not present without explicit request - assert "aggregations" in data - - -@pytest.mark.integration -@pytest.mark.api -def test_aggregation_edge_cases(): - """Test aggregation edge cases.""" - - # Test with empty query - request_data = { - "query": "", - "size": 10, - "aggregations": { - "category_name": { - "type": "terms", - "field": "categoryName_keyword", - "size": 10 - } - } - } - - response = client.post("/search/", json=request_data) - # Should handle empty query gracefully - assert response.status_code in [200, 422] - - # Test with invalid aggregation type - request_data = { - "query": "玩具", - "size": 10, - "aggregations": { - "invalid_agg": { - "type": "invalid_type", - "field": "categoryName_keyword", - "size": 10 - } - } - } - - response = client.post("/search/", json=request_data) - # Should handle invalid aggregation type gracefully - assert response.status_code in [200, 422] - - -@pytest.mark.unit -def test_aggregation_spec_validation(): - """Test aggregation specification validation.""" - from api.models import AggregationSpec - - # Test valid aggregation spec - agg_spec = AggregationSpec( - field="categoryName_keyword", - type="terms", - size=10 - ) - assert agg_spec.field == "categoryName_keyword" - assert agg_spec.type == "terms" - assert agg_spec.size == 10 - - # Test range aggregation spec - range_agg = AggregationSpec( - field="price", - type="range", - ranges=[ - {"key": "0-50", "to": 50}, - {"key": "50-100", "from": 50, "to": 100} - ] - ) - assert range_agg.field == "price" - assert range_agg.type == "range" - assert len(range_agg.ranges) == 2 - - -if __name__ == "__main__": - pytest.main([__file__]) \ No newline at end of file diff --git a/tests/integration/test_api_integration.py b/tests/integration/test_api_integration.py deleted file mode 100644 index badad87..0000000 --- a/tests/integration/test_api_integration.py +++ /dev/null @@ -1,338 +0,0 @@ -""" -API集成测试 - -测试API接口的完整集成,包括请求处理、响应格式、错误处理等 -""" - -import pytest -import json -import asyncio -from unittest.mock import patch, Mock, AsyncMock -from fastapi.testclient import TestClient - -# 导入API应用 -import sys -import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - -from api.app import app - - -@pytest.mark.integration -@pytest.mark.api -class TestAPIIntegration: - """API集成测试""" - - @pytest.fixture - def client(self): - """创建测试客户端""" - return TestClient(app) - - def test_search_api_basic(self, client): - """测试基础搜索API""" - response = client.get("/search", params={"q": "红色连衣裙"}) - - assert response.status_code == 200 - data = response.json() - - # 验证响应结构 - assert "hits" in data - assert "total" in data - assert "max_score" in data - assert "took_ms" in data - assert "query_info" in data - assert "performance_summary" in data - - # 验证hits是列表 - assert isinstance(data["hits"], list) - assert isinstance(data["total"], int) - assert isinstance(data["max_score"], (int, float)) - assert isinstance(data["took_ms"], int) - - def test_search_api_with_parameters(self, client): - """测试带参数的搜索API""" - params = { - "q": "智能手机", - "size": 15, - "from": 5, - "enable_translation": False, - "enable_embedding": False, - "enable_rerank": True, - "min_score": 1.0 - } - - response = client.get("/search", params=params) - - assert response.status_code == 200 - data = response.json() - - # 验证参数被正确传递 - performance = data.get("performance_summary", {}) - metadata = performance.get("metadata", {}) - search_params = metadata.get("search_params", {}) - - assert search_params.get("size") == 15 - assert search_params.get("from") == 5 - assert search_params.get("min_score") == 1.0 - - feature_flags = metadata.get("feature_flags", {}) - assert feature_flags.get("enable_translation") is False - assert feature_flags.get("enable_embedding") is False - assert feature_flags.get("enable_rerank") is True - - def test_search_api_complex_query(self, client): - """测试复杂查询API""" - response = client.get("/search", params={"q": "手机 AND (华为 OR 苹果) ANDNOT 二手"}) - - assert response.status_code == 200 - data = response.json() - - # 验证复杂查询被处理 - query_info = data.get("query_info", {}) - performance = data.get("performance_summary", {}) - query_analysis = performance.get("query_analysis", {}) - - # 对于复杂查询,is_simple_query应该是False - assert query_analysis.get("is_simple_query") is False - - def test_search_api_missing_query(self, client): - """测试缺少查询参数的API""" - response = client.get("/search") - - assert response.status_code == 422 # Validation error - data = response.json() - - # 验证错误信息 - assert "detail" in data - - def test_search_api_empty_query(self, client): - """测试空查询API""" - response = client.get("/search", params={"q": ""}) - - assert response.status_code == 200 - data = response.json() - - # 空查询应该返回有效结果 - assert "hits" in data - assert isinstance(data["hits"], list) - - def test_search_api_with_filters(self, client): - """测试带过滤器的搜索API""" - response = client.get("/search", params={ - "q": "连衣裙", - "filters": json.dumps({"category_id": 1, "brand": "测试品牌"}) - }) - - assert response.status_code == 200 - data = response.json() - - # 验证过滤器被应用 - performance = data.get("performance_summary", {}) - metadata = performance.get("metadata", {}) - search_params = metadata.get("search_params", {}) - - filters = search_params.get("filters", {}) - assert filters.get("category_id") == 1 - assert filters.get("brand") == "测试品牌" - - def test_search_api_performance_summary(self, client): - """测试API性能摘要""" - response = client.get("/search", params={"q": "性能测试查询"}) - - assert response.status_code == 200 - data = response.json() - - performance = data.get("performance_summary", {}) - - # 验证性能摘要结构 - assert "request_info" in performance - assert "query_analysis" in performance - assert "performance" in performance - assert "results" in performance - assert "metadata" in performance - - # 验证request_info - request_info = performance["request_info"] - assert "reqid" in request_info - assert "uid" in request_info - assert len(request_info["reqid"]) == 8 # 8字符的reqid - - # 验证performance - perf_data = performance["performance"] - assert "total_duration_ms" in perf_data - assert "stage_timings_ms" in perf_data - assert "stage_percentages" in perf_data - assert isinstance(perf_data["total_duration_ms"], (int, float)) - assert perf_data["total_duration_ms"] >= 0 - - def test_search_api_error_handling(self, client): - """测试API错误处理""" - # 模拟内部错误 - with patch('api.app._searcher') as mock_searcher: - mock_searcher.search.side_effect = Exception("内部服务错误") - - response = client.get("/search", params={"q": "错误测试"}) - - assert response.status_code == 500 - data = response.json() - - # 验证错误响应格式 - assert "error" in data - assert "request_id" in data - assert len(data["request_id"]) == 8 - - def test_health_check_api(self, client): - """测试健康检查API""" - response = client.get("/health") - - assert response.status_code == 200 - data = response.json() - - # 验证健康检查响应 - assert "status" in data - assert "timestamp" in data - assert "service" in data - assert "version" in data - - assert data["status"] in ["healthy", "unhealthy"] - assert data["service"] == "search-engine-api" - - def test_metrics_api(self, client): - """测试指标API""" - response = client.get("/metrics") - - # 根据实现,可能是JSON格式或Prometheus格式 - assert response.status_code in [200, 404] # 404如果未实现 - - def test_concurrent_search_api(self, client): - """测试并发搜索API""" - async def test_concurrent(): - tasks = [] - for i in range(10): - task = asyncio.create_task( - asyncio.to_thread( - client.get, - "/search", - params={"q": f"并发测试查询-{i}"} - ) - ) - tasks.append(task) - - responses = await asyncio.gather(*tasks) - - # 验证所有响应都成功 - for response in responses: - assert response.status_code == 200 - data = response.json() - assert "hits" in data - assert "performance_summary" in data - - # 运行并发测试 - asyncio.run(test_concurrent()) - - def test_search_api_response_time(self, client): - """测试API响应时间""" - import time - - start_time = time.time() - response = client.get("/search", params={"q": "响应时间测试"}) - end_time = time.time() - - response_time_ms = (end_time - start_time) * 1000 - - assert response.status_code == 200 - - # API响应时间应该合理(例如,小于5秒) - assert response_time_ms < 5000 - - # 验证响应中的时间信息 - data = response.json() - assert data["took_ms"] >= 0 - - performance = data.get("performance_summary", {}) - perf_data = performance.get("performance", {}) - total_duration = perf_data.get("total_duration_ms", 0) - - # 总处理时间应该包括API开销 - assert total_duration > 0 - - def test_search_api_large_query(self, client): - """测试大查询API""" - # 构造一个较长的查询 - long_query = " " * 1000 + "红色连衣裙" - - response = client.get("/search", params={"q": long_query}) - - assert response.status_code == 200 - data = response.json() - - # 验证长查询被正确处理 - query_analysis = data.get("performance_summary", {}).get("query_analysis", {}) - assert query_analysis.get("original_query") == long_query - - def test_search_api_unicode_support(self, client): - """测试API Unicode支持""" - unicode_queries = [ - "红色连衣裙", # 中文 - "red dress", # 英文 - "robe rouge", # 法文 - "赤いドレス", # 日文 - "أحمر فستان", # 阿拉伯文 - "👗🔴", # Emoji - ] - - for query in unicode_queries: - response = client.get("/search", params={"q": query}) - - assert response.status_code == 200 - data = response.json() - - # 验证Unicode查询被正确处理 - query_analysis = data.get("performance_summary", {}).get("query_analysis", {}) - assert query_analysis.get("original_query") == query - - def test_search_api_request_id_tracking(self, client): - """测试API请求ID跟踪""" - response = client.get("/search", params={"q": "请求ID测试"}) - - assert response.status_code == 200 - data = response.json() - - # 验证每个请求都有唯一的reqid - performance = data.get("performance_summary", {}) - request_info = performance.get("request_info", {}) - reqid = request_info.get("reqid") - - assert reqid is not None - assert len(reqid) == 8 - assert reqid.isalnum() - - def test_search_api_rate_limiting(self, client): - """测试API速率限制(如果实现了)""" - # 快速发送多个请求 - responses = [] - for i in range(20): # 发送20个快速请求 - response = client.get("/search", params={"q": f"速率限制测试-{i}"}) - responses.append(response) - - # 检查是否有请求被限制 - status_codes = [r.status_code for r in responses] - rate_limited = any(code == 429 for code in status_codes) - - # 根据是否实现速率限制,验证结果 - if rate_limited: - # 如果有速率限制,应该有一些429响应 - assert 429 in status_codes - else: - # 如果没有速率限制,所有请求都应该成功 - assert all(code == 200 for code in status_codes) - - def test_search_api_cors_headers(self, client): - """测试API CORS头""" - response = client.get("/search", params={"q": "CORS测试"}) - - assert response.status_code == 200 - - # 检查CORS头(如果配置了CORS) - # 这取决于实际的CORS配置 - # response.headers.get("Access-Control-Allow-Origin") \ No newline at end of file diff --git a/tests/integration/test_search_integration.py b/tests/integration/test_search_integration.py deleted file mode 100644 index edb9d3c..0000000 --- a/tests/integration/test_search_integration.py +++ /dev/null @@ -1,297 +0,0 @@ -""" -搜索集成测试 - -测试搜索流程的完整集成,包括QueryParser、BooleanParser、ESQueryBuilder等组件的协同工作 -""" - -import pytest -from unittest.mock import Mock, patch, AsyncMock -import json -import numpy as np - -from search import Searcher -from query import QueryParser -from search.boolean_parser import BooleanParser, QueryNode -from search.multilang_query_builder import MultiLanguageQueryBuilder -from context import RequestContext, create_request_context - - -@pytest.mark.integration -@pytest.mark.slow -class TestSearchIntegration: - """搜索集成测试""" - - def test_end_to_end_search_flow(self, test_searcher): - """测试端到端搜索流程""" - context = create_request_context("e2e-001", "e2e-user") - - # 执行搜索 - result = test_searcher.search("红色连衣裙", context=context) - - # 验证结果结构 - assert result.hits is not None - assert isinstance(result.hits, list) - assert result.total >= 0 - assert result.took_ms >= 0 - assert result.context == context - - # 验证context中有完整的数据 - summary = context.get_summary() - assert summary['query_analysis']['original_query'] == "红色连衣裙" - assert 'performance' in summary - assert summary['performance']['total_duration_ms'] > 0 - - # 验证各阶段都被执行 - assert context.get_stage_duration("query_parsing") >= 0 - assert context.get_stage_duration("query_building") >= 0 - assert context.get_stage_duration("elasticsearch_search") >= 0 - assert context.get_stage_duration("result_processing") >= 0 - - def test_complex_boolean_query_integration(self, test_searcher): - """测试复杂布尔查询的集成""" - context = create_request_context("boolean-001") - - # 复杂布尔查询 - result = test_searcher.search("手机 AND (华为 OR 苹果) ANDNOT 二手", context=context) - - assert result is not None - assert context.query_analysis.is_simple_query is False - assert context.query_analysis.boolean_ast is not None - - # 验证中间结果 - query_node = context.get_intermediate_result('query_node') - assert query_node is not None - assert isinstance(query_node, QueryNode) - - def test_multilingual_search_integration(self, test_searcher): - """测试多语言搜索集成""" - context = create_request_context("multilang-001") - - with patch('query.query_parser.Translator') as mock_translator_class, \ - patch('query.query_parser.LanguageDetector') as mock_detector_class: - - # 设置mock - mock_translator = Mock() - mock_translator_class.return_value = mock_translator - mock_translator.get_translation_needs.return_value = ["en"] - mock_translator.translate_multi.return_value = {"en": "red dress"} - - mock_detector = Mock() - mock_detector_class.return_value = mock_detector - mock_detector.detect.return_value = "zh" - - result = test_searcher.search("红色连衣裙", enable_translation=True, context=context) - - # 验证翻译结果被使用 - assert context.query_analysis.translations.get("en") == "red dress" - assert context.query_analysis.detected_language == "zh" - - def test_embedding_search_integration(self, test_searcher): - """测试向量搜索集成""" - # 配置embedding字段 - test_searcher.text_embedding_field = "text_embedding" - - context = create_request_context("embedding-001") - - with patch('query.query_parser.BgeEncoder') as mock_encoder_class: - # 设置mock - mock_encoder = Mock() - mock_encoder_class.return_value = mock_encoder - mock_encoder.encode.return_value = [np.array([0.1, 0.2, 0.3, 0.4])] - - result = test_searcher.search("智能手机", enable_embedding=True, context=context) - - # 验证向量被生成和使用 - assert context.query_analysis.query_vector is not None - assert len(context.query_analysis.query_vector) == 4 - - # 验证ES查询包含KNN - es_query = context.get_intermediate_result('es_query') - if es_query and 'knn' in es_query: - assert 'text_embedding' in es_query['knn'] - - def test_spu_collapse_integration(self, test_searcher): - """测试SPU折叠集成""" - # 启用SPU折叠 - test_searcher.config.spu_config.enabled = True - test_searcher.config.spu_config.spu_field = "spu_id" - test_searcher.config.spu_config.inner_hits_size = 3 - - context = create_request_context("spu-001") - - result = test_searcher.search("手机", context=context) - - # 验证SPU折叠被应用 - es_query = context.get_intermediate_result('es_query') - assert es_query is not None - - # 如果ES查询构建正确,应该包含collapse配置 - # 注意:这取决于ESQueryBuilder的实现 - - def test_reranking_integration(self, test_searcher): - """测试重排序集成""" - context = create_request_context("rerank-001") - - # 启用重排序 - result = test_searcher.search("笔记本电脑", enable_rerank=True, context=context) - - # 验证重排序阶段被执行 - if result.hits: # 如果有结果 - # 应该有自定义分数 - assert all('_custom_score' in hit for hit in result.hits) - assert all('_original_score' in hit for hit in result.hits) - - # 自定义分数应该被计算 - custom_scores = [hit['_custom_score'] for hit in result.hits] - original_scores = [hit['_original_score'] for hit in result.hits] - assert len(custom_scores) == len(original_scores) - - def test_error_propagation_integration(self, test_searcher): - """测试错误传播集成""" - context = create_request_context("error-001") - - # 模拟ES错误 - test_searcher.es_client.search.side_effect = Exception("ES连接失败") - - with pytest.raises(Exception, match="ES连接失败"): - test_searcher.search("测试查询", context=context) - - # 验证错误被正确记录 - assert context.has_error() - assert "ES连接失败" in context.metadata['error_info']['message'] - - def test_performance_monitoring_integration(self, test_searcher): - """测试性能监控集成""" - context = create_request_context("perf-001") - - # 模拟耗时操作 - with patch('query.query_parser.QueryParser') as mock_parser_class: - mock_parser = Mock() - mock_parser_class.return_value = mock_parser - mock_parser.parse.side_effect = lambda q, **kwargs: Mock( - original_query=q, - normalized_query=q, - rewritten_query=q, - detected_language="zh", - domain="default", - translations={}, - query_vector=None - ) - - # 执行搜索 - result = test_searcher.search("性能测试查询", context=context) - - # 验证性能数据被收集 - summary = context.get_summary() - assert summary['performance']['total_duration_ms'] > 0 - assert 'stage_timings_ms' in summary['performance'] - assert 'stage_percentages' in summary['performance'] - - # 验证主要阶段都被计时 - stages = ['query_parsing', 'query_building', 'elasticsearch_search', 'result_processing'] - for stage in stages: - assert stage in summary['performance']['stage_timings_ms'] - - def test_context_data_persistence_integration(self, test_searcher): - """测试context数据持久化集成""" - context = create_request_context("persist-001") - - result = test_searcher.search("数据持久化测试", context=context) - - # 验证所有关键数据都被存储 - assert context.query_analysis.original_query == "数据持久化测试" - assert context.get_intermediate_result('parsed_query') is not None - assert context.get_intermediate_result('es_query') is not None - assert context.get_intermediate_result('es_response') is not None - assert context.get_intermediate_result('processed_hits') is not None - - # 验证元数据 - assert 'search_params' in context.metadata - assert 'feature_flags' in context.metadata - assert context.metadata['search_params']['query'] == "数据持久化测试" - - @pytest.mark.parametrize("query,expected_simple", [ - ("红色连衣裙", True), - ("手机 AND 电脑", False), - ("(华为 OR 苹果) ANDNOT 二手", False), - "laptop RANK gaming", False, - ("简单查询", True) - ]) - def test_query_complexity_detection(self, test_searcher, query, expected_simple): - """测试查询复杂度检测""" - context = create_request_context(f"complexity-{hash(query)}") - - result = test_searcher.search(query, context=context) - - assert context.query_analysis.is_simple_query == expected_simple - - def test_search_with_all_features_enabled(self, test_searcher): - """测试启用所有功能的搜索""" - # 配置所有功能 - test_searcher.text_embedding_field = "text_embedding" - test_searcher.config.spu_config.enabled = True - test_searcher.config.spu_config.spu_field = "spu_id" - - context = create_request_context("all-features-001") - - with patch('query.query_parser.BgeEncoder') as mock_encoder_class, \ - patch('query.query_parser.Translator') as mock_translator_class, \ - patch('query.query_parser.LanguageDetector') as mock_detector_class: - - # 设置所有mock - mock_encoder = Mock() - mock_encoder_class.return_value = mock_encoder - mock_encoder.encode.return_value = [np.array([0.1, 0.2])] - - mock_translator = Mock() - mock_translator_class.return_value = mock_translator - mock_translator.get_translation_needs.return_value = ["en"] - mock_translator.translate_multi.return_value = {"en": "test query"} - - mock_detector = Mock() - mock_detector_class.return_value = mock_detector - mock_detector.detect.return_value = "zh" - - # 执行完整搜索 - result = test_searcher.search( - "完整功能测试", - enable_translation=True, - enable_embedding=True, - enable_rerank=True, - context=context - ) - - # 验证所有功能都被使用 - assert context.query_analysis.detected_language == "zh" - assert context.query_analysis.translations.get("en") == "test query" - assert context.query_analysis.query_vector is not None - - # 验证所有阶段都有耗时记录 - summary = context.get_summary() - expected_stages = [ - 'query_parsing', 'query_building', - 'elasticsearch_search', 'result_processing' - ] - for stage in expected_stages: - assert stage in summary['performance']['stage_timings_ms'] - - def test_search_result_context_integration(self, test_searcher): - """测试搜索结果与context的集成""" - context = create_request_context("result-context-001") - - result = test_searcher.search("结果上下文集成测试", context=context) - - # 验证结果包含context - assert result.context == context - - # 验证结果to_dict方法包含性能摘要 - result_dict = result.to_dict() - assert 'performance_summary' in result_dict - assert result_dict['performance_summary']['request_info']['reqid'] == context.reqid - - # 验证性能摘要内容 - perf_summary = result_dict['performance_summary'] - assert 'query_analysis' in perf_summary - assert 'performance' in perf_summary - assert 'results' in perf_summary - assert 'metadata' in perf_summary \ No newline at end of file diff --git a/tests/unit/test_context.py b/tests/unit/test_context.py deleted file mode 100644 index 281db77..0000000 --- a/tests/unit/test_context.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -RequestContext单元测试 -""" - -import pytest -import time -from context import RequestContext, RequestContextStage, create_request_context - - -@pytest.mark.unit -class TestRequestContext: - """RequestContext测试用例""" - - def test_create_context(self): - """测试创建context""" - context = create_request_context("req-001", "user-123") - - assert context.reqid == "req-001" - assert context.uid == "user-123" - assert not context.has_error() - - def test_auto_generated_reqid(self): - """测试自动生成reqid""" - context = RequestContext() - - assert context.reqid is not None - assert len(context.reqid) == 8 - assert context.uid == "anonymous" - - def test_stage_timing(self): - """测试阶段计时""" - context = create_request_context() - - # 开始计时 - context.start_stage(RequestContextStage.QUERY_PARSING) - time.sleep(0.05) # 50ms - duration = context.end_stage(RequestContextStage.QUERY_PARSING) - - assert duration >= 40 # 至少40ms(允许一些误差) - assert duration < 100 # 不超过100ms - assert context.get_stage_duration(RequestContextStage.QUERY_PARSING) == duration - - def test_store_query_analysis(self): - """测试存储查询分析结果""" - context = create_request_context() - - context.store_query_analysis( - original_query="红色连衣裙", - normalized_query="红色 连衣裙", - rewritten_query="红色 女 连衣裙", - detected_language="zh", - translations={"en": "red dress"}, - domain="default", - is_simple_query=True - ) - - assert context.query_analysis.original_query == "红色连衣裙" - assert context.query_analysis.detected_language == "zh" - assert context.query_analysis.translations["en"] == "red dress" - assert context.query_analysis.is_simple_query is True - - def test_store_intermediate_results(self): - """测试存储中间结果""" - context = create_request_context() - - # 存储各种类型的中间结果 - context.store_intermediate_result('parsed_query', {'query': 'test'}) - context.store_intermediate_result('es_query', {'bool': {'must': []}}) - context.store_intermediate_result('hits', [{'_id': '1', '_score': 1.0}]) - - assert context.get_intermediate_result('parsed_query') == {'query': 'test'} - assert context.get_intermediate_result('es_query') == {'bool': {'must': []}} - assert context.get_intermediate_result('hits') == [{'_id': '1', '_score': 1.0}] - - # 测试不存在的key - assert context.get_intermediate_result('nonexistent') is None - assert context.get_intermediate_result('nonexistent', 'default') == 'default' - - def test_error_handling(self): - """测试错误处理""" - context = create_request_context() - - assert not context.has_error() - - # 设置错误 - try: - raise ValueError("测试错误") - except Exception as e: - context.set_error(e) - - assert context.has_error() - error_info = context.metadata['error_info'] - assert error_info['type'] == 'ValueError' - assert error_info['message'] == '测试错误' - - def test_warnings(self): - """测试警告处理""" - context = create_request_context() - - assert len(context.metadata['warnings']) == 0 - - # 添加警告 - context.add_warning("第一个警告") - context.add_warning("第二个警告") - - assert len(context.metadata['warnings']) == 2 - assert "第一个警告" in context.metadata['warnings'] - assert "第二个警告" in context.metadata['warnings'] - - def test_stage_percentages(self): - """测试阶段耗时占比计算""" - context = create_request_context() - context.performance_metrics.total_duration = 100.0 - - # 设置各阶段耗时 - context.performance_metrics.stage_timings = { - 'query_parsing': 25.0, - 'elasticsearch_search': 50.0, - 'result_processing': 25.0 - } - - percentages = context.calculate_stage_percentages() - - assert percentages['query_parsing'] == 25.0 - assert percentages['elasticsearch_search'] == 50.0 - assert percentages['result_processing'] == 25.0 - - def test_get_summary(self): - """测试获取摘要""" - context = create_request_context("test-req", "test-user") - - # 设置一些数据 - context.store_query_analysis( - original_query="测试查询", - detected_language="zh", - domain="default" - ) - context.store_intermediate_result('test_key', 'test_value') - context.performance_metrics.total_duration = 150.0 - context.performance_metrics.stage_timings = { - 'query_parsing': 30.0, - 'elasticsearch_search': 80.0 - } - - summary = context.get_summary() - - # 验证基本结构 - assert 'request_info' in summary - assert 'query_analysis' in summary - assert 'performance' in summary - assert 'results' in summary - assert 'metadata' in summary - - # 验证具体内容 - assert summary['request_info']['reqid'] == 'test-req' - assert summary['request_info']['uid'] == 'test-user' - assert summary['query_analysis']['original_query'] == '测试查询' - assert summary['query_analysis']['detected_language'] == 'zh' - assert summary['performance']['total_duration_ms'] == 150.0 - assert 'query_parsing' in summary['performance']['stage_timings_ms'] - - def test_context_manager(self): - """测试上下文管理器功能""" - with create_request_context("cm-test", "cm-user") as context: - assert context.reqid == "cm-test" - assert context.uid == "cm-user" - - # 在上下文中执行一些操作 - context.start_stage(RequestContextStage.QUERY_PARSING) - time.sleep(0.01) - context.end_stage(RequestContextStage.QUERY_PARSING) - - # 上下文应该仍然活跃 - assert context.get_stage_duration(RequestContextStage.QUERY_PARSING) > 0 - - # 退出上下文后,应该自动记录了总时间 - assert context.performance_metrics.total_duration > 0 - - -@pytest.mark.unit -class TestContextFactory: - """Context工厂函数测试""" - - def test_create_request_context_with_params(self): - """测试带参数创建context""" - context = create_request_context("custom-req", "custom-user") - - assert context.reqid == "custom-req" - assert context.uid == "custom-user" - - def test_create_request_context_without_params(self): - """测试不带参数创建context""" - context = create_request_context() - - assert context.reqid is not None - assert len(context.reqid) == 8 - assert context.uid == "anonymous" - - def test_create_request_context_with_partial_params(self): - """测试部分参数创建context""" - context = create_request_context(reqid="partial-req") - - assert context.reqid == "partial-req" - assert context.uid == "anonymous" - - context2 = create_request_context(uid="partial-user") - assert context2.reqid is not None - assert context2.uid == "partial-user" - - -@pytest.mark.unit -class TestContextStages: - """Context阶段枚举测试""" - - def test_stage_values(self): - """测试阶段枚举值""" - assert RequestContextStage.TOTAL.value == "total_search" - assert RequestContextStage.QUERY_PARSING.value == "query_parsing" - assert RequestContextStage.BOOLEAN_PARSING.value == "boolean_parsing" - assert RequestContextStage.QUERY_BUILDING.value == "query_building" - assert RequestContextStage.ELASTICSEARCH_SEARCH.value == "elasticsearch_search" - assert RequestContextStage.RESULT_PROCESSING.value == "result_processing" - assert RequestContextStage.RERANKING.value == "reranking" - - def test_stage_uniqueness(self): - """测试阶段值唯一性""" - values = [stage.value for stage in RequestContextStage] - assert len(values) == len(set(values)), "阶段值应该是唯一的" \ No newline at end of file diff --git a/tests/unit/test_query_parser.py b/tests/unit/test_query_parser.py deleted file mode 100644 index db2d54a..0000000 --- a/tests/unit/test_query_parser.py +++ /dev/null @@ -1,270 +0,0 @@ -""" -QueryParser单元测试 -""" - -import pytest -from unittest.mock import Mock, patch, MagicMock -import numpy as np - -from query import QueryParser, ParsedQuery -from context import RequestContext, create_request_context - - -@pytest.mark.unit -class TestQueryParser: - """QueryParser测试用例""" - - def test_parser_initialization(self, sample_customer_config): - """测试QueryParser初始化""" - parser = QueryParser(sample_customer_config) - - assert parser.config == sample_customer_config - assert parser.query_config is not None - assert parser.normalizer is not None - assert parser.rewriter is not None - assert parser.language_detector is not None - assert parser.translator is not None - - @patch('query.query_parser.QueryNormalizer') - @patch('query.query_parser.LanguageDetector') - def test_parse_without_context(self, mock_detector_class, mock_normalizer_class, test_query_parser): - """测试不带context的解析""" - # 设置mock - mock_normalizer = Mock() - mock_normalizer_class.return_value = mock_normalizer - mock_normalizer.normalize.return_value = "红色 连衣裙" - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") - - mock_detector = Mock() - mock_detector_class.return_value = mock_detector - mock_detector.detect.return_value = "zh" - - result = test_query_parser.parse("红色连衣裙") - - assert isinstance(result, ParsedQuery) - assert result.original_query == "红色连衣裙" - assert result.normalized_query == "红色 连衣裙" - assert result.rewritten_query == "红色 连衣裙" # 没有重写 - assert result.detected_language == "zh" - - def test_parse_with_context(self, test_query_parser): - """测试带context的解析""" - context = create_request_context("parse-001", "parse-user") - - # Mock各种组件 - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ - patch.object(test_query_parser, 'language_detector') as mock_detector, \ - patch.object(test_query_parser, 'translator') as mock_translator, \ - patch.object(test_query_parser, 'text_encoder') as mock_encoder: - - # 设置mock返回值 - mock_normalizer.normalize.return_value = "红色 连衣裙" - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") - mock_detector.detect.return_value = "zh" - mock_translator.translate_multi.return_value = {"en": "red dress"} - mock_encoder.encode.return_value = [np.array([0.1, 0.2, 0.3])] - - result = test_query_parser.parse("红色连衣裙", generate_vector=True, context=context) - - # 验证结果 - assert isinstance(result, ParsedQuery) - assert result.original_query == "红色连衣裙" - assert result.detected_language == "zh" - assert result.translations["en"] == "red dress" - assert result.query_vector is not None - - # 验证context被更新 - assert context.query_analysis.original_query == "红色连衣裙" - assert context.query_analysis.normalized_query == "红色 连衣裙" - assert context.query_analysis.detected_language == "zh" - assert context.query_analysis.translations["en"] == "red dress" - assert context.query_analysis.domain == "default" - - # 验证计时 - assert context.get_stage_duration("query_parsing") > 0 - - @patch('query.query_parser.QueryRewriter') - def test_query_rewriting(self, mock_rewriter_class, test_query_parser): - """测试查询重写""" - # 设置mock - mock_rewriter = Mock() - mock_rewriter_class.return_value = mock_rewriter - mock_rewriter.rewrite.return_value = "红色 女 连衣裙" - - context = create_request_context() - - # 启用查询重写 - test_query_parser.query_config.enable_query_rewrite = True - - result = test_query_parser.parse("红色连衣裙", context=context) - - assert result.rewritten_query == "红色 女 连衣裙" - assert context.query_analysis.rewritten_query == "红色 女 连衣裙" - - def test_language_detection(self, test_query_parser): - """测试语言检测""" - context = create_request_context() - - with patch.object(test_query_parser, 'language_detector') as mock_detector, \ - patch.object(test_query_parser, 'normalizer') as mock_normalizer: - - mock_normalizer.normalize.return_value = "red dress" - mock_normalizer.extract_domain_query.return_value = ("default", "red dress") - mock_detector.detect.return_value = "en" - - result = test_query_parser.parse("red dress", context=context) - - assert result.detected_language == "en" - assert context.query_analysis.detected_language == "en" - - @patch('query.query_parser.Translator') - def test_query_translation(self, mock_translator_class, test_query_parser): - """测试查询翻译""" - # 设置mock - mock_translator = Mock() - mock_translator_class.return_value = mock_translator - mock_translator.get_translation_needs.return_value = ["en"] - mock_translator.translate_multi.return_value = {"en": "red dress"} - - context = create_request_context() - - # 启用翻译 - test_query_parser.query_config.enable_translation = True - test_query_parser.query_config.supported_languages = ["zh", "en"] - - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ - patch.object(test_query_parser, 'language_detector') as mock_detector: - - mock_normalizer.normalize.return_value = "红色 连衣裙" - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") - mock_detector.detect.return_value = "zh" - - result = test_query_parser.parse("红色连衣裙", context=context) - - assert result.translations["en"] == "red dress" - assert context.query_analysis.translations["en"] == "red dress" - - @patch('query.query_parser.BgeEncoder') - def test_text_embedding(self, mock_encoder_class, test_query_parser): - """测试文本向量化""" - # 设置mock - mock_encoder = Mock() - mock_encoder_class.return_value = mock_encoder - mock_encoder.encode.return_value = [np.array([0.1, 0.2, 0.3])] - - context = create_request_context() - - # 启用向量化 - test_query_parser.query_config.enable_text_embedding = True - - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ - patch.object(test_query_parser, 'language_detector') as mock_detector: - - mock_normalizer.normalize.return_value = "红色 连衣裙" - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") - mock_detector.detect.return_value = "zh" - - result = test_query_parser.parse("红色连衣裙", generate_vector=True, context=context) - - assert result.query_vector is not None - assert isinstance(result.query_vector, np.ndarray) - assert context.query_analysis.query_vector is not None - - def test_domain_extraction(self, test_query_parser): - """测试域名提取""" - context = create_request_context() - - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ - patch.object(test_query_parser, 'language_detector') as mock_detector: - - # 测试带域名的查询 - mock_normalizer.normalize.return_value = "brand:nike 鞋子" - mock_normalizer.extract_domain_query.return_value = ("brand", "nike 鞋子") - mock_detector.detect.return_value = "zh" - - result = test_query_parser.parse("brand:nike 鞋子", context=context) - - assert result.domain == "brand" - assert context.query_analysis.domain == "brand" - - def test_parse_with_disabled_features(self, test_query_parser): - """测试禁用功能的解析""" - context = create_request_context() - - # 禁用所有功能 - test_query_parser.query_config.enable_query_rewrite = False - test_query_parser.query_config.enable_translation = False - test_query_parser.query_config.enable_text_embedding = False - - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ - patch.object(test_query_parser, 'language_detector') as mock_detector: - - mock_normalizer.normalize.return_value = "红色 连衣裙" - mock_normalizer.extract_domain_query.return_value = ("default", "红色 连衣裙") - mock_detector.detect.return_value = "zh" - - result = test_query_parser.parse("红色连衣裙", generate_vector=False, context=context) - - assert result.original_query == "红色连衣裙" - assert result.rewritten_query == "红色 连衣裙" # 没有重写 - assert result.detected_language == "zh" - assert len(result.translations) == 0 # 没有翻译 - assert result.query_vector is None # 没有向量 - - def test_get_search_queries(self, test_query_parser): - """测试获取搜索查询列表""" - parsed_query = ParsedQuery( - original_query="红色连衣裙", - normalized_query="红色 连衣裙", - rewritten_query="红色 连衣裙", - detected_language="zh", - translations={"en": "red dress", "fr": "robe rouge"} - ) - - queries = test_query_parser.get_search_queries(parsed_query) - - assert len(queries) == 3 - assert "红色 连衣裙" in queries - assert "red dress" in queries - assert "robe rouge" in queries - - def test_empty_query_handling(self, test_query_parser): - """测试空查询处理""" - result = test_query_parser.parse("") - - assert result.original_query == "" - assert result.normalized_query == "" - - def test_whitespace_query_handling(self, test_query_parser): - """测试空白字符查询处理""" - result = test_query_parser.parse(" ") - - assert result.original_query == " " - - def test_error_handling_in_parsing(self, test_query_parser): - """测试解析过程中的错误处理""" - context = create_request_context() - - # Mock normalizer抛出异常 - with patch.object(test_query_parser, 'normalizer') as mock_normalizer: - mock_normalizer.normalize.side_effect = Exception("Normalization failed") - - with pytest.raises(Exception, match="Normalization failed"): - test_query_parser.parse("红色连衣裙", context=context) - - def test_performance_timing(self, test_query_parser): - """测试性能计时""" - context = create_request_context() - - with patch.object(test_query_parser, 'normalizer') as mock_normalizer, \ - patch.object(test_query_parser, 'language_detector') as mock_detector: - - mock_normalizer.normalize.return_value = "test" - mock_normalizer.extract_domain_query.return_value = ("default", "test") - mock_detector.detect.return_value = "zh" - - result = test_query_parser.parse("test", context=context) - - # 验证计时被记录 - assert context.get_stage_duration("query_parsing") > 0 - assert context.get_intermediate_result('parsed_query') == result \ No newline at end of file diff --git a/tests/unit/test_searcher.py b/tests/unit/test_searcher.py deleted file mode 100644 index 60fe9cd..0000000 --- a/tests/unit/test_searcher.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -Searcher单元测试 -""" - -import pytest -from unittest.mock import Mock, patch, MagicMock -import numpy as np - -from search import Searcher -from query import ParsedQuery -from context import RequestContext, create_request_context - - -@pytest.mark.unit -class TestSearcher: - """Searcher测试用例""" - - def test_searcher_initialization(self, sample_customer_config, mock_es_client): - """测试Searcher初始化""" - searcher = Searcher(sample_customer_config, mock_es_client) - - assert searcher.config == sample_customer_config - assert searcher.es_client == mock_es_client - assert searcher.query_parser is not None - assert searcher.boolean_parser is not None - assert searcher.ranking_engine is not None - - def test_search_without_context(self, test_searcher): - """测试不带context的搜索(向后兼容)""" - result = test_searcher.search("红色连衣裙", size=5) - - assert result.hits is not None - assert result.total >= 0 - assert result.context is not None # 应该自动创建context - assert result.took_ms >= 0 - - def test_search_with_context(self, test_searcher): - """测试带context的搜索""" - context = create_request_context("test-req", "test-user") - - result = test_searcher.search("红色连衣裙", context=context) - - assert result.hits is not None - assert result.context == context - assert context.reqid == "test-req" - assert context.uid == "test-user" - - def test_search_with_parameters(self, test_searcher): - """测试带各种参数的搜索""" - context = create_request_context() - - result = test_searcher.search( - query="红色连衣裙", - size=15, - from_=5, - filters={"category_id": 1}, - enable_translation=False, - enable_embedding=False, - enable_rerank=False, - min_score=1.0, - context=context - ) - - assert result is not None - assert context.metadata['search_params']['size'] == 15 - assert context.metadata['search_params']['from'] == 5 - assert context.metadata['search_params']['filters'] == {"category_id": 1} - assert context.metadata['search_params']['min_score'] == 1.0 - - # 验证feature flags - assert context.metadata['feature_flags']['enable_translation'] is False - assert context.metadata['feature_flags']['enable_embedding'] is False - assert context.metadata['feature_flags']['enable_rerank'] is False - - @patch('search.searcher.QueryParser') - def test_search_query_parsing(self, mock_query_parser_class, test_searcher): - """测试查询解析流程""" - # 设置mock - mock_parser = Mock() - mock_query_parser_class.return_value = mock_parser - - parsed_query = ParsedQuery( - original_query="红色连衣裙", - normalized_query="红色 连衣裙", - rewritten_query="红色 女 连衣裙", - detected_language="zh", - domain="default" - ) - mock_parser.parse.return_value = parsed_query - - context = create_request_context() - test_searcher.search("红色连衣裙", context=context) - - # 验证query parser被调用 - mock_parser.parse.assert_called_once_with("红色连衣裙", generate_vector=True, context=context) - - def test_search_error_handling(self, test_searcher): - """测试搜索错误处理""" - # 设置ES客户端抛出异常 - test_searcher.es_client.search.side_effect = Exception("ES连接失败") - - context = create_request_context() - - with pytest.raises(Exception, match="ES连接失败"): - test_searcher.search("红色连衣裙", context=context) - - # 验证错误被记录到context - assert context.has_error() - assert "ES连接失败" in context.metadata['error_info']['message'] - - def test_search_result_processing(self, test_searcher): - """测试搜索结果处理""" - context = create_request_context() - - result = test_searcher.search("红色连衣裙", enable_rerank=True, context=context) - - # 验证结果结构 - assert hasattr(result, 'hits') - assert hasattr(result, 'total') - assert hasattr(result, 'max_score') - assert hasattr(result, 'took_ms') - assert hasattr(result, 'aggregations') - assert hasattr(result, 'query_info') - assert hasattr(result, 'context') - - # 验证context中有中间结果 - assert context.get_intermediate_result('es_response') is not None - assert context.get_intermediate_result('raw_hits') is not None - assert context.get_intermediate_result('processed_hits') is not None - - def test_boolean_query_handling(self, test_searcher): - """测试布尔查询处理""" - context = create_request_context() - - # 测试复杂布尔查询 - result = test_searcher.search("laptop AND (gaming OR professional)", context=context) - - assert result is not None - # 对于复杂查询,应该调用boolean parser - assert not context.query_analysis.is_simple_query - - def test_simple_query_handling(self, test_searcher): - """测试简单查询处理""" - context = create_request_context() - - # 测试简单查询 - result = test_searcher.search("红色连衣裙", context=context) - - assert result is not None - # 简单查询应该标记为simple - assert context.query_analysis.is_simple_query - - @patch('search.searcher.RankingEngine') - def test_reranking(self, mock_ranking_engine_class, test_searcher): - """测试重排序功能""" - # 设置mock - mock_ranking = Mock() - mock_ranking_engine_class.return_value = mock_ranking - mock_ranking.calculate_score.return_value = 2.0 - - context = create_request_context() - result = test_searcher.search("红色连衣裙", enable_rerank=True, context=context) - - # 验证重排序被调用 - hits = result.hits - if hits: # 如果有结果 - # 应该有自定义分数 - assert all('_custom_score' in hit for hit in hits) - assert all('_original_score' in hit for hit in hits) - - def test_spu_collapse(self, test_searcher): - """测试SPU折叠功能""" - # 配置SPU - test_searcher.config.spu_config.enabled = True - test_searcher.config.spu_config.spu_field = "spu_id" - test_searcher.config.spu_config.inner_hits_size = 3 - - context = create_request_context() - result = test_searcher.search("红色连衣裙", context=context) - - assert result is not None - # 验证SPU折叠配置被应用 - assert context.get_intermediate_result('es_query') is not None - - def test_embedding_search(self, test_searcher): - """测试向量搜索功能""" - # 配置embedding字段 - test_searcher.text_embedding_field = "text_embedding" - - context = create_request_context() - result = test_searcher.search("红色连衣裙", enable_embedding=True, context=context) - - assert result is not None - # embedding搜索应该被启用 - - def test_search_by_image(self, test_searcher): - """测试图片搜索功能""" - # 配置图片embedding字段 - test_searcher.image_embedding_field = "image_embedding" - - # Mock图片编码器 - with patch('search.searcher.CLIPImageEncoder') as mock_encoder_class: - mock_encoder = Mock() - mock_encoder_class.return_value = mock_encoder - mock_encoder.encode_image_from_url.return_value = np.array([0.1, 0.2, 0.3]) - - result = test_searcher.search_by_image("http://example.com/image.jpg") - - assert result is not None - assert result.query_info['search_type'] == 'image_similarity' - assert result.query_info['image_url'] == "http://example.com/image.jpg" - - def test_performance_monitoring(self, test_searcher): - """测试性能监控""" - context = create_request_context() - - result = test_searcher.search("红色连衣裙", context=context) - - # 验证各阶段都被计时 - assert context.get_stage_duration(RequestContextStage.QUERY_PARSING) >= 0 - assert context.get_stage_duration(RequestContextStage.QUERY_BUILDING) >= 0 - assert context.get_stage_duration(RequestContextStage.ELASTICSEARCH_SEARCH) >= 0 - assert context.get_stage_duration(RequestContextStage.RESULT_PROCESSING) >= 0 - - # 验证总耗时 - assert context.performance_metrics.total_duration > 0 - - def test_context_storage(self, test_searcher): - """测试context存储功能""" - context = create_request_context() - - result = test_searcher.search("红色连衣裙", context=context) - - # 验证查询分析结果被存储 - assert context.query_analysis.original_query == "红色连衣裙" - assert context.query_analysis.domain is not None - - # 验证中间结果被存储 - assert context.get_intermediate_result('parsed_query') is not None - assert context.get_intermediate_result('es_query') is not None - assert context.get_intermediate_result('es_response') is not None - assert context.get_intermediate_result('processed_hits') is not None \ No newline at end of file diff --git a/verification_report.py b/verification_report.py deleted file mode 100644 index e69b944..0000000 --- a/verification_report.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -""" -验证报告 - 确认请求上下文和日志系统修复完成 -""" - -import sys -import os -import traceback - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -def run_verification(): - """运行完整的验证测试""" - print("🔍 开始系统验证...") - print("=" * 60) - - tests_passed = 0 - tests_total = 0 - - def run_test(test_name, test_func): - nonlocal tests_passed, tests_total - tests_total += 1 - try: - test_func() - print(f"✅ {test_name}") - tests_passed += 1 - except Exception as e: - print(f"❌ {test_name} - 失败: {e}") - traceback.print_exc() - - # 测试1: 基础模块导入 - def test_imports(): - from utils.logger import get_logger, setup_logging - from context.request_context import create_request_context, RequestContextStage - from query.query_parser import QueryParser - assert get_logger is not None - assert create_request_context is not None - - # 测试2: 日志系统 - def test_logging(): - from utils.logger import get_logger, setup_logging - setup_logging(log_level="INFO", log_dir="verification_logs") - logger = get_logger("verification") - logger.info("测试消息", extra={'reqid': 'test', 'uid': 'user'}) - - # 测试3: 请求上下文创建 - def test_context_creation(): - from context.request_context import create_request_context - context = create_request_context("req123", "user123") - assert context.reqid == "req123" - assert context.uid == "user123" - - # 测试4: 查询解析(这是之前出错的地方) - def test_query_parsing(): - from context.request_context import create_request_context - from query.query_parser import QueryParser - - class TestConfig: - class QueryConfig: - enable_query_rewrite = False - rewrite_dictionary = {} - enable_translation = False - supported_languages = ['en', 'zh'] - enable_text_embedding = False - query_config = QueryConfig() - indexes = [] - - config = TestConfig() - parser = QueryParser(config) - context = create_request_context("req456", "user456") - - # 这之前会抛出 "Logger._log() got an unexpected keyword argument 'reqid'" 错误 - result = parser.parse("test query", context=context, generate_vector=False) - assert result.original_query == "test query" - - # 测试5: 完整的中文查询处理 - def test_chinese_query(): - from context.request_context import create_request_context - from query.query_parser import QueryParser - - class TestConfig: - class QueryConfig: - enable_query_rewrite = True - rewrite_dictionary = {'芭比娃娃': 'brand:芭比'} - enable_translation = False - supported_languages = ['en', 'zh'] - enable_text_embedding = False - query_config = QueryConfig() - indexes = [] - - config = TestConfig() - parser = QueryParser(config) - context = create_request_context("req789", "user789") - - result = parser.parse("芭比娃娃", context=context, generate_vector=False) - # 语言检测可能不准确,但查询应该正常处理 - assert result.original_query == "芭比娃娃" - assert "brand:芭比" in result.rewritten_query - - # 测试6: 性能摘要 - def test_performance_summary(): - from context.request_context import create_request_context, RequestContextStage - - context = create_request_context("req_perf", "user_perf") - context.start_stage(RequestContextStage.TOTAL) - context.start_stage(RequestContextStage.QUERY_PARSING) - context.end_stage(RequestContextStage.QUERY_PARSING) - context.end_stage(RequestContextStage.TOTAL) - - summary = context.get_summary() - assert 'performance' in summary - assert 'stage_timings_ms' in summary['performance'] - - # 运行所有测试 - run_test("基础模块导入", test_imports) - run_test("日志系统", test_logging) - run_test("请求上下文创建", test_context_creation) - run_test("查询解析(修复验证)", test_query_parsing) - run_test("中文查询处理", test_chinese_query) - run_test("性能摘要", test_performance_summary) - - # 输出结果 - print("\n" + "=" * 60) - print(f"📊 验证结果: {tests_passed}/{tests_total} 测试通过") - - if tests_passed == tests_total: - print("🎉 所有验证通过!系统修复完成。") - print("\n🔧 修复内容:") - print(" - 修复了 utils/logger.py 中的日志参数处理") - print(" - 修复了 context/request_context.py 中的日志调用格式") - print(" - 修复了 query/query_parser.py 中的日志调用格式") - print(" - 修复了 search/searcher.py 中的日志调用格式") - print(" - 修复了 api/routes/search.py 中的日志调用格式") - print("\n✅ 现在可以正常处理搜索请求,不会再出现 Logger._log() 错误。") - return True - else: - print("💥 还有测试失败,需要进一步修复。") - return False - -if __name__ == "__main__": - success = run_verification() - sys.exit(0 if success else 1) \ No newline at end of file diff --git a/verify_refactoring.py b/verify_refactoring.py deleted file mode 100755 index 7c1df90..0000000 --- a/verify_refactoring.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -""" -验证 API v3.0 重构是否完整 -检查代码中是否还有旧的逻辑残留 -""" - -import os -import re -from pathlib import Path - -def print_header(title): - print(f"\n{'='*60}") - print(f" {title}") - print('='*60) - -def search_in_file(filepath, pattern, description): - """在文件中搜索模式""" - try: - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - matches = re.findall(pattern, content, re.MULTILINE) - return matches - except Exception as e: - return None - -def check_removed_code(): - """检查已移除的代码""" - print_header("检查已移除的代码") - - checks = [ - { - "file": "search/es_query_builder.py", - "pattern": r"if field == ['\"]price_ranges['\"]", - "description": "硬编码的 price_ranges 逻辑", - "should_exist": False - }, - { - "file": "search/es_query_builder.py", - "pattern": r"def add_dynamic_aggregations", - "description": "add_dynamic_aggregations 方法", - "should_exist": False - }, - { - "file": "api/models.py", - "pattern": r"aggregations.*Optional\[Dict", - "description": "aggregations 参数(在 SearchRequest 中)", - "should_exist": False - }, - { - "file": "frontend/static/js/app.js", - "pattern": r"price_ranges", - "description": "前端硬编码的 price_ranges", - "should_exist": False - }, - { - "file": "frontend/static/js/app.js", - "pattern": r"displayAggregations", - "description": "旧的 displayAggregations 函数", - "should_exist": False - } - ] - - all_passed = True - for check in checks: - filepath = os.path.join("/home/tw/SearchEngine", check["file"]) - matches = search_in_file(filepath, check["pattern"], check["description"]) - - if matches is None: - print(f" ⚠️ 无法读取:{check['file']}") - continue - - if check["should_exist"]: - if matches: - print(f" ✓ 存在:{check['description']}") - else: - print(f" ✗ 缺失:{check['description']}") - all_passed = False - else: - if matches: - print(f" ✗ 仍存在:{check['description']}") - print(f" 匹配:{matches[:2]}") - all_passed = False - else: - print(f" ✓ 已移除:{check['description']}") - - return all_passed - -def check_new_code(): - """检查新增的代码""" - print_header("检查新增的代码") - - checks = [ - { - "file": "api/models.py", - "pattern": r"class RangeFilter", - "description": "RangeFilter 模型", - "should_exist": True - }, - { - "file": "api/models.py", - "pattern": r"class FacetConfig", - "description": "FacetConfig 模型", - "should_exist": True - }, - { - "file": "api/models.py", - "pattern": r"class FacetValue", - "description": "FacetValue 模型", - "should_exist": True - }, - { - "file": "api/models.py", - "pattern": r"class FacetResult", - "description": "FacetResult 模型", - "should_exist": True - }, - { - "file": "api/models.py", - "pattern": r"range_filters.*RangeFilter", - "description": "range_filters 参数", - "should_exist": True - }, - { - "file": "api/models.py", - "pattern": r"facets.*FacetConfig", - "description": "facets 参数", - "should_exist": True - }, - { - "file": "search/es_query_builder.py", - "pattern": r"def build_facets", - "description": "build_facets 方法", - "should_exist": True - }, - { - "file": "search/searcher.py", - "pattern": r"def _standardize_facets", - "description": "_standardize_facets 方法", - "should_exist": True - }, - { - "file": "api/routes/search.py", - "pattern": r"@router.get\(['\"]\/suggestions", - "description": "/search/suggestions 端点", - "should_exist": True - }, - { - "file": "api/routes/search.py", - "pattern": r"@router.get\(['\"]\/instant", - "description": "/search/instant 端点", - "should_exist": True - }, - { - "file": "frontend/static/js/app.js", - "pattern": r"function displayFacets", - "description": "displayFacets 函数", - "should_exist": True - }, - { - "file": "frontend/static/js/app.js", - "pattern": r"rangeFilters", - "description": "rangeFilters 状态", - "should_exist": True - } - ] - - all_passed = True - for check in checks: - filepath = os.path.join("/home/tw/SearchEngine", check["file"]) - matches = search_in_file(filepath, check["pattern"], check["description"]) - - if matches is None: - print(f" ⚠️ 无法读取:{check['file']}") - continue - - if check["should_exist"]: - if matches: - print(f" ✓ 存在:{check['description']}") - else: - print(f" ✗ 缺失:{check['description']}") - all_passed = False - else: - if matches: - print(f" ✗ 仍存在:{check['description']}") - all_passed = False - else: - print(f" ✓ 已移除:{check['description']}") - - return all_passed - -def check_documentation(): - """检查文档""" - print_header("检查文档") - - docs = [ - "API_DOCUMENTATION.md", - "API_EXAMPLES.md", - "MIGRATION_GUIDE_V3.md", - "CHANGES.md" - ] - - all_exist = True - for doc in docs: - filepath = os.path.join("/home/tw/SearchEngine", doc) - if os.path.exists(filepath): - size_kb = os.path.getsize(filepath) / 1024 - print(f" ✓ 存在:{doc} ({size_kb:.1f} KB)") - else: - print(f" ✗ 缺失:{doc}") - all_exist = False - - return all_exist - -def check_imports(): - """检查模块导入""" - print_header("检查模块导入") - - import sys - sys.path.insert(0, '/home/tw/SearchEngine') - - try: - from api.models import ( - RangeFilter, FacetConfig, FacetValue, FacetResult, - SearchRequest, SearchResponse, ImageSearchRequest, - SearchSuggestRequest, SearchSuggestResponse - ) - print(" ✓ API 模型导入成功") - - from search.es_query_builder import ESQueryBuilder - print(" ✓ ESQueryBuilder 导入成功") - - from search.searcher import Searcher, SearchResult - print(" ✓ Searcher 导入成功") - - # 检查方法 - qb = ESQueryBuilder('test', ['field1']) - if hasattr(qb, 'build_facets'): - print(" ✓ build_facets 方法存在") - else: - print(" ✗ build_facets 方法不存在") - return False - - if hasattr(qb, 'add_dynamic_aggregations'): - print(" ✗ add_dynamic_aggregations 方法仍存在(应该已删除)") - return False - else: - print(" ✓ add_dynamic_aggregations 方法已删除") - - # 检查 SearchResult - sr = SearchResult(hits=[], total=0, max_score=0, took_ms=10, facets=[]) - if hasattr(sr, 'facets'): - print(" ✓ SearchResult.facets 属性存在") - else: - print(" ✗ SearchResult.facets 属性不存在") - return False - - if hasattr(sr, 'aggregations'): - print(" ✗ SearchResult.aggregations 属性仍存在(应该已删除)") - return False - else: - print(" ✓ SearchResult.aggregations 属性已删除") - - return True - - except Exception as e: - print(f" ✗ 导入失败:{e}") - return False - -def main(): - """主函数""" - print("\n" + "🔍 开始验证 API v3.0 重构") - print(f"项目路径:/home/tw/SearchEngine\n") - - # 运行检查 - check1 = check_removed_code() - check2 = check_new_code() - check3 = check_documentation() - check4 = check_imports() - - # 总结 - print_header("验证总结") - - results = { - "已移除的代码": check1, - "新增的代码": check2, - "文档完整性": check3, - "模块导入": check4 - } - - all_passed = all(results.values()) - - for name, passed in results.items(): - status = "✓ 通过" if passed else "✗ 失败" - print(f" {status}: {name}") - - if all_passed: - print(f"\n 🎉 所有检查通过!API v3.0 重构完成。") - else: - print(f"\n ⚠️ 部分检查失败,请检查上述详情。") - - print("\n" + "="*60 + "\n") - - return 0 if all_passed else 1 - -if __name__ == "__main__": - exit(main()) - -- libgit2 0.21.2