Initial commit: QMDSearch 分层信息检索服务
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""知识分类 API:GET /api/v1/knowledge/categories、GET /api/v1/knowledge/stats"""
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.response import ApiError, ok
|
||||
from app.config import settings
|
||||
from app.models.knowledge import UNCATEGORIZED, TaxonomyCategory, load_taxonomy
|
||||
from app.services.qdrant import ALL_COLLECTIONS, COLLECTION_L1, QdrantService
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter(prefix="/api/v1", tags=["knowledge"])
|
||||
|
||||
# 类目分布统计的分页大小
|
||||
_STATS_SCROLL_PAGE_SIZE = 100
|
||||
|
||||
# 模块级懒加载单例,避免每请求重建 QdrantService
|
||||
_qdrant: QdrantService | None = None
|
||||
|
||||
|
||||
def _get_qdrant() -> QdrantService:
|
||||
global _qdrant
|
||||
if _qdrant is None:
|
||||
_qdrant = QdrantService()
|
||||
return _qdrant
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_taxonomy() -> list[TaxonomyCategory]:
|
||||
"""加载并缓存 taxonomy 类目集(进程内只加载一次)"""
|
||||
return load_taxonomy(settings.taxonomy_path)
|
||||
|
||||
|
||||
@router.get("/knowledge/categories")
|
||||
async def list_categories() -> dict[str, Any]:
|
||||
"""返回完整知识分类类目集"""
|
||||
categories = _get_taxonomy()
|
||||
return ok({"categories": [c.model_dump() for c in categories], "count": len(categories)})
|
||||
|
||||
|
||||
@router.get("/knowledge/stats")
|
||||
async def knowledge_stats() -> dict[str, Any]:
|
||||
"""返回四层集合规模与 L1 类目分布统计"""
|
||||
service = _get_qdrant()
|
||||
try:
|
||||
collections = {collection: await service.count(collection) for collection in ALL_COLLECTIONS}
|
||||
categories = await _aggregate_l1_categories(service)
|
||||
except Exception as exc:
|
||||
logger.error("获取知识库统计失败", error=str(exc))
|
||||
raise ApiError(2000, f"获取知识库统计失败: {exc}") from exc
|
||||
return ok(
|
||||
{
|
||||
"collections": collections,
|
||||
"categories": categories,
|
||||
"uncategorized_count": categories.get(UNCATEGORIZED, 0),
|
||||
"documents_total": collections[COLLECTION_L1],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _aggregate_l1_categories(service: QdrantService) -> dict[str, int]:
|
||||
"""分页遍历 L1 文档,按 category 聚合文档数(空类目归入 uncategorized)"""
|
||||
categories: dict[str, int] = {}
|
||||
offset: str | None = None
|
||||
while True:
|
||||
items, offset = await service.scroll_l1(limit=_STATS_SCROLL_PAGE_SIZE, offset=offset)
|
||||
for item in items:
|
||||
category = item.get("category") or UNCATEGORIZED
|
||||
categories[category] = categories.get(category, 0) + 1
|
||||
if offset is None:
|
||||
return categories
|
||||
Reference in New Issue
Block a user