"""检索 API:POST /api/v1/search""" from hashlib import sha256 from typing import Any import structlog from fastapi import APIRouter, Depends from app.api.response import ApiError, ok from app.core.auth import AuthUser, get_current_user from app.core.retriever import Retriever from app.models.search import SearchRequest from app.services.redis import get_cache logger = structlog.get_logger() router = APIRouter(prefix="/api/v1", tags=["search"]) # 模块级懒加载单例,避免每请求重建 Qdrant/Embedding client _retriever: Retriever | None = None def _get_retriever() -> Retriever: global _retriever if _retriever is None: _retriever = Retriever() return _retriever def _cache_key(request: SearchRequest) -> str: """检索缓存键:query + top_k + summarize 的短哈希(三者均影响结果集,需参与键计算)""" digest = sha256((request.query + "|" + str(request.top_k) + "|" + str(request.summarize)).encode()).hexdigest()[:16] return f"search:{digest}" @router.post("/search") async def search(request: SearchRequest, user: AuthUser = Depends(get_current_user)) -> dict[str, Any]: """分层检索入口,返回统一包装的 SearchResponse 先查 Redis 缓存:命中直接返回缓存的响应;未命中走检索流程并回写缓存。 缓存读写失败均降级为无缓存行为,不影响检索。 """ cache_key = _cache_key(request) cached = await get_cache().get_json(cache_key) if cached is not None: logger.info("检索缓存命中", query=request.query, cache_key=cache_key) return cached try: response = await _get_retriever().search(request) except ApiError: raise except Exception as exc: logger.exception("检索失败", query=request.query) raise ApiError(2000, f"检索失败: {exc}") from exc result = ok(response.model_dump()) await get_cache().set_json(cache_key, result) return result