51dc8dc4f6
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""知识分类 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
|