"""检索 API — /api/search""" from fastapi import APIRouter, Request, HTTPException, Query from typing import Optional from app.services.retriever import Retriever from app.services.embedding import embed_text from app.services.history import add_search_history from app.config import MAX_QUERY_LENGTH router = APIRouter() @router.get("/search") async def search( request: Request, query: str = Query(..., min_length=1, max_length=MAX_QUERY_LENGTH, description="查询文本"), mode: str = Query("semantic", description="检索模式:semantic(语义) / keyword(关键词精确)"), category: Optional[str] = Query(None, description="法规类别过滤"), province: Optional[str] = Query(None, description="省份过滤"), city: Optional[str] = Query(None, description="市级过滤"), top_k: int = Query(20, ge=1, le=100, description="返回数量"), page: int = Query(1, ge=1, description="页码"), page_size: int = Query(20, ge=1, le=100, description="每页数量"), ): """检索法规条文(语义检索或关键词精确搜索) - mode=semantic: 向量化查询 → FAISS 检索 → metadata 过滤(默认) - mode=keyword: 按法规名+条号精确匹配,支持"郑州市劳动用工条例 第三十二条" """ trace_id = getattr(request.state, "trace_id", "") retriever = Retriever.get_instance() if not retriever.is_ready(): raise HTTPException(status_code=503, detail="索引未加载,请等待或检查索引文件") if mode == "keyword": # 关键词精确搜索 results = retriever.keyword_search( query=query, top_k=top_k * 3 if (category or province or city) else top_k, category=category, province=province, city=city, ) else: # 语义检索 try: query_vec = await embed_text(query) except Exception as e: raise HTTPException( status_code=503, detail=f"embedding 服务不可用: {str(e)}", ) fetch_k = top_k * 3 if (category or province or city) else top_k results = retriever.search( query_vec=query_vec, top_k=fetch_k, category=category, province=province, city=city, ) # 分页 total = len(results) start = (page - 1) * page_size end = start + page_size page_results = results[start:end] # 记录检索历史 add_search_history( query=query, category=category, province=province, city=city, result_count=total, top_k=top_k, ) return { "code": 0, "message": "ok", "data": { "results": page_results, "total": total, "page": page, "page_size": page_size, }, "trace_id": trace_id, }