c5d0f4c63e
- 移除 conftest 中对 app.api.deps 的全局依赖覆盖:该覆盖会绕过真实 会话鉴权,导致 /auth/*、文档变更、用户管理等鉴权测试(期望 1005/1006/1001) 误判为通过。鉴权测试现走真实 UserStore/SessionStore。 - 检索与知识类查询端点(POST /search、GET /knowledge/categories、GET /knowledge/stats)改为免登录:与测试套件明确声明的「查询类端点免登录」 设计意图一致,同时保留文档变更、/auth/*、Settings 变更端点的登录要求。 变更文件:tests/conftest.py、app/api/v1/search.py、app/api/v1/knowledge.py
59 lines
1.8 KiB
Python
59 lines
1.8 KiB
Python
"""检索 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
|