Initial commit: QMDSearch 分层信息检索服务

- FastAPI + Qdrant + Redis + Ollama 技术栈
- L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合)
- 文档三级总结与 2.5 级回退
- query 解析路由与分类
- /admin 管理页面
This commit is contained in:
2026-07-29 21:24:40 +08:00
commit 51dc8dc4f6
83 changed files with 10794 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
"""检索 APIPOST /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 的短哈希(top_k 影响结果集,需参与键计算)"""
digest = sha256((request.query + "|" + str(request.top_k)).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