""" Search API routes. """ from fastapi import APIRouter, HTTPException, Query, Request from typing import Optional import uuid from ..models import ( SearchRequest, ImageSearchRequest, SearchResponse, DocumentResponse, ErrorResponse ) from context.request_context import create_request_context, set_current_request_context, clear_current_request_context router = APIRouter(prefix="/search", tags=["search"]) def extract_request_info(request: Request) -> tuple[str, str]: """Extract request ID and user ID from HTTP request""" # Try to get request ID from headers reqid = request.headers.get('X-Request-ID') or str(uuid.uuid4())[:8] # Try to get user ID from headers or default to anonymous uid = request.headers.get('X-User-ID') or request.headers.get('User-ID') or 'anonymous' return reqid, uid @router.post("/", response_model=SearchResponse) async def search(request: SearchRequest, http_request: Request): """ Execute text search query. Supports: - Multi-language query processing - Boolean operators (AND, OR, RANK, ANDNOT) - Semantic search with embeddings - Custom ranking functions - Filters and aggregations """ reqid, uid = extract_request_info(http_request) # Create request context context = create_request_context(reqid=reqid, uid=uid) # Set context in thread-local storage set_current_request_context(context) try: # Log request start context.logger.info( f"收到搜索请求 | IP: {http_request.client.host if http_request.client else 'unknown'} | " f"用户代理: {http_request.headers.get('User-Agent', 'unknown')[:100]}", extra={'reqid': context.reqid, 'uid': context.uid} ) # Get searcher from app state from api.app import get_searcher searcher = get_searcher() # Execute search with context (using backend defaults from config) result = searcher.search( query=request.query, size=request.size, from_=request.from_, filters=request.filters, min_score=request.min_score, context=context, aggregations=request.aggregations, sort_by=request.sort_by, sort_order=request.sort_order ) # Include performance summary in response performance_summary = context.get_summary() if context else None # Convert to response model return SearchResponse( hits=result.hits, total=result.total, max_score=result.max_score, took_ms=result.took_ms, aggregations=result.aggregations, query_info=result.query_info, performance_info=performance_summary ) except Exception as e: # Log error in context if context: context.set_error(e) context.logger.error( f"搜索请求失败 | 错误: {str(e)}", extra={'reqid': context.reqid, 'uid': context.uid} ) raise HTTPException(status_code=500, detail=str(e)) finally: # Clear thread-local context clear_current_request_context() @router.post("/image", response_model=SearchResponse) async def search_by_image(request: ImageSearchRequest, http_request: Request): """ Search by image similarity. Uses image embeddings to find visually similar products. """ reqid, uid = extract_request_info(http_request) # Create request context context = create_request_context(reqid=reqid, uid=uid) # Set context in thread-local storage set_current_request_context(context) try: # Log request start context.logger.info( f"收到图片搜索请求 | 图片URL: {request.image_url} | " f"IP: {http_request.client.host if http_request.client else 'unknown'}", extra={'reqid': context.reqid, 'uid': context.uid} ) from api.app import get_searcher searcher = get_searcher() # Execute image search result = searcher.search_by_image( image_url=request.image_url, size=request.size, filters=request.filters ) # Include performance summary in response performance_summary = context.get_summary() if context else None return SearchResponse( hits=result.hits, total=result.total, max_score=result.max_score, took_ms=result.took_ms, aggregations=result.aggregations, query_info=result.query_info, performance_info=performance_summary ) except ValueError as e: if context: context.set_error(e) context.logger.error( f"图片搜索请求参数错误 | 错误: {str(e)}", extra={'reqid': context.reqid, 'uid': context.uid} ) raise HTTPException(status_code=400, detail=str(e)) except Exception as e: if context: context.set_error(e) context.logger.error( f"图片搜索请求失败 | 错误: {str(e)}", extra={'reqid': context.reqid, 'uid': context.uid} ) raise HTTPException(status_code=500, detail=str(e)) finally: # Clear thread-local context clear_current_request_context() @router.get("/{doc_id}", response_model=DocumentResponse) async def get_document(doc_id: str): """ Get a single document by ID. """ try: from api.app import get_searcher searcher = get_searcher() doc = searcher.get_document(doc_id) if doc is None: raise HTTPException(status_code=404, detail=f"Document {doc_id} not found") return DocumentResponse(id=doc_id, source=doc) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e))