Files
AI/ontology_platform/ont_platform/api/phase5_app.py
lasta 4bef188a19 Phase 5.1: 의미 기반 부분그래프 검색 + API 엔드포인트 완성
구현 사항:
1. SubgraphRetriever.retrieve_by_semantic_query() 추가
   - 쿼리 임베딩 기반 의미 유사도 검색
   - 코사인 유사도로 관련 엔티티 자동 발견
   - 의미 임계값(min_similarity) 기반 필터링
   - N-hop 확장으로 컨텍스트 그래프 추출

2. Phase 5 GraphRAG API 엔드포인트 완성 (phase5_app.py)
   - POST /api/v1/graph/resolve: 엔티티 중복 감지/병합
   - POST /api/v1/graph/subgraph: N-hop 부분그래프 추출
   - POST /api/v1/graph/subgraph/semantic: 의미 기반 부분그래프 추출
   - POST /api/v1/graph/patterns/paths: 경로 검색
   - POST /api/v1/graph/patterns/cycles: 순환 감지
   - POST /api/v1/graph/analytics/centrality: 중심성 분석
   - POST /api/v1/graph/analytics/communities: 커뮤니티 감지

3. 종합 테스트 스위트 작성
   - test_entity_resolver.py: 24개 테스트 
   - test_subgraph_retriever.py: 15개 테스트 
   - test_phase5_app.py: 25개 테스트 
   - test_rdf_converter.py: 2개 테스트 
   - 총 66개 테스트, 모두 통과

성능 목표:
- 벡터 임베딩: 10K 엔티티 5초 내
- 의미 검색: 상위 K개 매칭 < 200ms
- 부분그래프 추출: 2-hop 쿼리 < 200ms

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-14 13:25:06 +09:00

385 lines
11 KiB
Python

"""Phase 5 GraphRAG API: 엔티티 중복 제거, 부분그래프 추출, 패턴 매칭, 그래프 분석.
기능:
- 엔티티 중복 감지 및 병합 (벡터 + 텍스트 유사도)
- 의미 기반 부분그래프 추출 (쿼리 임베딩)
- 패턴 매칭 (경로, 순환, SCC)
- 그래프 분석 (중심성, 커뮤니티)
"""
import logging
from typing import Dict, Any, Optional, List
from fastapi import FastAPI, APIRouter, HTTPException, Depends, Query
from fastapi.responses import JSONResponse
from ont_platform.core.graph import (
EntityResolver,
RDFToPropertyGraphConverter,
SubgraphRetriever,
PatternMatcher,
GraphAnalytics,
Neo4jAdapter,
)
logger = logging.getLogger(__name__)
# FastAPI 앱
app = FastAPI(
title="Ontology Platform - Phase 5 GraphRAG",
description="엔티티 중복 제거, 부분그래프 추출, 패턴 매칭, 그래프 분석",
version="0.5.0",
)
# 라우터
graph_router = APIRouter(prefix="/api/v1/graph", tags=["graph"])
# 전역 인스턴스 (싱글톤)
_neo4j_adapter: Optional[Neo4jAdapter] = None
def get_neo4j_adapter() -> Neo4jAdapter:
"""Get or initialize Neo4j adapter."""
global _neo4j_adapter
if _neo4j_adapter is None:
_neo4j_adapter = Neo4jAdapter()
return _neo4j_adapter
adapter = get_neo4j_adapter()
entity_resolver = EntityResolver()
rdf_converter = RDFToPropertyGraphConverter()
subgraph_retriever = SubgraphRetriever(adapter=adapter)
pattern_matcher = PatternMatcher(adapter=adapter)
graph_analytics = GraphAnalytics(adapter=adapter)
# ============================================================================
# 엔티티 중복 제거 엔드포인트
# ============================================================================
@graph_router.post("/resolve")
async def resolve_duplicates(
request: Dict[str, Any],
) -> Dict[str, Any]:
"""엔티티 중복 감지 및 병합.
Args:
request: JSON body with "entities" field containing list of entities (id, label, type)
Returns:
{
"clusters": [중복 클러스터],
"report": 리포트,
"total_duplicates": 감지된 중복 수
}
"""
entities = request.get("entities", [])
try:
# 임베더 초기화 (필요시)
if not entity_resolver.embedder:
await entity_resolver.initialize_embedder()
# 중복 감지
clusters = await entity_resolver.detect_duplicates(entities)
# 리포트 생성
report = entity_resolver.get_resolution_report(clusters)
return {
"status": "success",
"clusters": [
{
"cluster_id": c.cluster_id,
"canonical_id": c.canonical_id,
"duplicates": c.duplicates,
"confidence": c.confidence,
"reason": c.reason,
}
for c in clusters
],
"report": report,
"total_duplicates": report["total_duplicates"],
}
except Exception as e:
logger.error(f"Failed to resolve duplicates: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# 부분그래프 추출 엔드포인트
# ============================================================================
@graph_router.post("/subgraph/semantic")
async def retrieve_semantic_context(
query: str = Query(...),
top_k: int = Query(10, ge=1, le=100),
min_similarity: float = Query(0.6, ge=0.0, le=1.0),
hops: int = Query(1, ge=0, le=3),
) -> Dict[str, Any]:
"""의미 기반 부분그래프 추출 (쿼리 임베딩 유사도).
Args:
query: 검색 쿼리 텍스트
top_k: 상위 K개 매칭 엔티티
min_similarity: 최소 유사도 임계값 (0-1)
hops: 확장할 홉 수 (0-3)
Returns:
의미 기반 부분그래프
"""
try:
# 임베더 초기화 (필요시)
if not subgraph_retriever.embedder:
from sentence_transformers import SentenceTransformer
subgraph_retriever.embedder = SentenceTransformer("all-MiniLM-L6-v2")
result = await subgraph_retriever.retrieve_by_semantic_query(
query=query,
top_k=top_k,
min_similarity=min_similarity,
hops=hops,
)
return {
"status": "success",
"query": query,
"query_dimension": result.get("query_embedding_dimension", 0),
"matched_count": result.get("matched_count", 0),
"matched_entities": result.get("matched_entities", []),
"nodes": result.get("nodes", []),
"edges": result.get("edges", []),
}
except Exception as e:
logger.error(f"Failed to retrieve semantic context: {e}")
raise HTTPException(status_code=500, detail=str(e))
@graph_router.post("/subgraph")
async def extract_subgraph(
entity_id: int = Query(...),
hops: int = Query(2, ge=1, le=3),
min_confidence: float = Query(0.0, ge=0.0, le=1.0),
) -> Dict[str, Any]:
"""N-hop 부분그래프 추출.
Args:
entity_id: 중심 엔티티 ID
hops: 홉 수 (1-3)
min_confidence: 최소 신뢰도
Returns:
부분그래프 정보
"""
try:
result = await subgraph_retriever.retrieve_neighborhood(
entity_id=entity_id,
hops=hops,
min_confidence=min_confidence,
)
return {
"status": "success",
"entity_id": entity_id,
"hops": hops,
"subgraph": result,
}
except Exception as e:
logger.error(f"Failed to extract subgraph: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# 패턴 매칭 엔드포인트
# ============================================================================
@graph_router.post("/patterns/paths")
async def find_paths(
start_id: int = Query(...),
end_id: int = Query(...),
max_length: int = Query(5, ge=2, le=10),
) -> Dict[str, Any]:
"""두 엔티티 사이의 경로 찾기.
Args:
start_id: 시작 엔티티 ID
end_id: 종료 엔티티 ID
max_length: 최대 경로 길이
Returns:
경로 리스트
"""
try:
paths = await pattern_matcher.find_paths(
start_entity_id=start_id,
end_entity_id=end_id,
max_length=max_length,
)
return {
"status": "success",
"start_id": start_id,
"end_id": end_id,
"paths_found": len(paths),
"paths": [
{
"path": p.path if hasattr(p, "path") else [],
"length": p.length if hasattr(p, "length") else 0,
"confidence": p.confidence if hasattr(p, "confidence") else 0,
}
for p in paths
] if paths else [],
}
except Exception as e:
logger.error(f"Failed to find paths: {e}")
raise HTTPException(status_code=500, detail=str(e))
@graph_router.post("/patterns/cycles")
async def find_cycles() -> Dict[str, Any]:
"""순환 경로 감지.
Returns:
순환 리스트
"""
try:
cycles = await pattern_matcher.find_cycles()
return {
"status": "success",
"cycles_found": len(cycles),
"cycles": cycles,
}
except Exception as e:
logger.error(f"Failed to find cycles: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# 그래프 분석 엔드포인트
# ============================================================================
@graph_router.post("/analytics/centrality")
async def analyze_centrality(
centrality_type: str = Query("pagerank", pattern="^(degree|betweenness|closeness|pagerank)$"),
top_n: int = Query(10, ge=1, le=100),
) -> Dict[str, Any]:
"""엔티티 중심성 분석.
Args:
centrality_type: 중심성 타입
top_n: 상위 N개 반환
Returns:
중심성 결과
"""
try:
results = await graph_analytics.calculate_centrality(
centrality_type=centrality_type,
)
# 상위 N개만 반환
top_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)[
:top_n
]
return {
"status": "success",
"centrality_type": centrality_type,
"total_entities": len(results),
"top_n": top_n,
"results": top_results,
}
except Exception as e:
logger.error(f"Failed to analyze centrality: {e}")
raise HTTPException(status_code=500, detail=str(e))
@graph_router.post("/analytics/communities")
async def detect_communities(
algorithm: str = Query("louvain", pattern="^(louvain|leiden)$"),
) -> Dict[str, Any]:
"""커뮤니티 감지.
Args:
algorithm: 알고리즘 (louvain, leiden)
Returns:
커뮤니티 결과
"""
try:
communities = await graph_analytics.detect_communities(algorithm=algorithm)
return {
"status": "success",
"algorithm": algorithm,
"communities_found": len(communities),
"communities": [
{
"community_id": c.community_id if hasattr(c, "community_id") else "",
"size": c.size if hasattr(c, "size") else 0,
"density": c.density if hasattr(c, "density") else 0,
}
for c in communities
],
}
except Exception as e:
logger.error(f"Failed to detect communities: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# 헬스 체크
# ============================================================================
@app.get("/health")
async def health_check() -> Dict[str, Any]:
"""헬스 체크."""
return {
"status": "healthy",
"version": "0.5.0",
"phase": "5 (GraphRAG)",
"components": {
"entity_resolver": "ok",
"rdf_converter": "ok",
"subgraph_retriever": "ok",
"pattern_matcher": "ok",
"graph_analytics": "ok",
},
}
@app.get("/info")
async def get_platform_info() -> Dict[str, Any]:
"""플랫폼 정보."""
return {
"platform": "Ontology System Construction Platform",
"phase": "5 (GraphRAG)",
"version": "0.5.0",
"features": {
"entity_resolution": True,
"subgraph_retrieval": True,
"pattern_matching": True,
"graph_analytics": True,
},
}
# ============================================================================
# 라우터 등록
# ============================================================================
app.include_router(graph_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8003)