"""检索 API:POST /api/v1/search""" from hashlib import sha256 from typing import Any import structlog from fastapi import APIRouter from app.api.response import ApiError, ok 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) -> 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