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>
This commit is contained in:
384
ontology_platform/ont_platform/api/phase5_app.py
Normal file
384
ontology_platform/ont_platform/api/phase5_app.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""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)
|
||||
@@ -4,25 +4,252 @@ Provides:
|
||||
- retrieve_neighborhood(): Extract N-hop neighbors of an entity
|
||||
- retrieve_context(): Find common paths between multiple entities
|
||||
- retrieve_induced_subgraph(): Extract subgraph induced by entity set
|
||||
- retrieve_by_semantic_query(): Semantic similarity-based entity search
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SubgraphRetriever:
|
||||
"""Extract meaningful subgraphs for RAG context."""
|
||||
|
||||
def __init__(self, adapter):
|
||||
def __init__(self, adapter, embedder=None):
|
||||
"""
|
||||
Initialize subgraph retriever.
|
||||
|
||||
Args:
|
||||
adapter: Neo4jAdapter instance for query execution
|
||||
embedder: Optional SentenceTransformer embedder for semantic queries
|
||||
"""
|
||||
self.adapter = adapter
|
||||
self.embedder = embedder
|
||||
|
||||
async def retrieve_by_semantic_query(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int = 10,
|
||||
min_similarity: float = 0.6,
|
||||
hops: int = 1,
|
||||
limit: int = 500,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve entities semantically similar to a query.
|
||||
|
||||
Uses embedding-based similarity search to find relevant entities
|
||||
and their N-hop neighborhoods.
|
||||
|
||||
Args:
|
||||
query: Query string to embed and search
|
||||
top_k: Number of top matching entities to return
|
||||
min_similarity: Minimum cosine similarity threshold (0-1)
|
||||
hops: Hops to expand around matched entities
|
||||
limit: Maximum total nodes to return
|
||||
|
||||
Returns:
|
||||
{
|
||||
"query": str,
|
||||
"query_embedding_dimension": int,
|
||||
"matched_entities": [
|
||||
{id, label, similarity, hops_distance}
|
||||
],
|
||||
"nodes": [...],
|
||||
"edges": [...],
|
||||
"node_count": int,
|
||||
"edge_count": int,
|
||||
"matched_count": int,
|
||||
}
|
||||
"""
|
||||
if not self.embedder:
|
||||
return {
|
||||
"error": "Embedder not initialized",
|
||||
"query": query,
|
||||
"matched_entities": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"node_count": 0,
|
||||
"edge_count": 0,
|
||||
"matched_count": 0,
|
||||
}
|
||||
|
||||
if not query or not query.strip():
|
||||
return {
|
||||
"error": "Empty query",
|
||||
"query": query,
|
||||
"matched_entities": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"node_count": 0,
|
||||
"edge_count": 0,
|
||||
"matched_count": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. Embed query
|
||||
query_embedding = self.embedder.encode(query, convert_to_tensor=False)
|
||||
query_embedding = np.array(query_embedding, dtype=np.float32)
|
||||
|
||||
# 2. Fetch all entities with embeddings from Neo4j
|
||||
fetch_cypher = """
|
||||
MATCH (n:Entity)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
RETURN {
|
||||
id: n.id,
|
||||
label: n.label,
|
||||
type: n.type,
|
||||
confidence: n.confidence,
|
||||
embedding: n.embedding
|
||||
} AS entity
|
||||
LIMIT $fetch_limit
|
||||
"""
|
||||
entity_results = await self.adapter.execute_cypher(
|
||||
fetch_cypher,
|
||||
{"fetch_limit": 50000}, # Safety limit
|
||||
)
|
||||
|
||||
if not entity_results:
|
||||
return {
|
||||
"query": query,
|
||||
"query_embedding_dimension": len(query_embedding),
|
||||
"matched_entities": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"node_count": 0,
|
||||
"edge_count": 0,
|
||||
"matched_count": 0,
|
||||
"warning": "No entities with embeddings found",
|
||||
}
|
||||
|
||||
# 3. Compute similarities
|
||||
similarities = []
|
||||
for result in entity_results:
|
||||
entity = result["entity"]
|
||||
if not entity.get("embedding"):
|
||||
continue
|
||||
|
||||
try:
|
||||
entity_embedding = np.array(entity["embedding"], dtype=np.float32)
|
||||
# Cosine similarity
|
||||
similarity = float(
|
||||
np.dot(query_embedding, entity_embedding)
|
||||
/ (np.linalg.norm(query_embedding) * np.linalg.norm(entity_embedding) + 1e-8)
|
||||
)
|
||||
|
||||
if similarity >= min_similarity:
|
||||
similarities.append({
|
||||
"id": entity["id"],
|
||||
"label": entity["label"],
|
||||
"similarity": similarity,
|
||||
"type": entity["type"],
|
||||
"confidence": entity.get("confidence", 0.5),
|
||||
})
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
# 4. Sort by similarity and select top_k
|
||||
similarities.sort(key=lambda x: x["similarity"], reverse=True)
|
||||
top_matches = similarities[:top_k]
|
||||
|
||||
if not top_matches:
|
||||
return {
|
||||
"query": query,
|
||||
"query_embedding_dimension": len(query_embedding),
|
||||
"matched_entities": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"node_count": 0,
|
||||
"edge_count": 0,
|
||||
"matched_count": 0,
|
||||
}
|
||||
|
||||
# 5. Extract neighborhood around matched entities
|
||||
matched_ids = [m["id"] for m in top_matches]
|
||||
all_node_ids = set(matched_ids)
|
||||
|
||||
# Get N-hop neighbors
|
||||
if hops > 0:
|
||||
neighbors_cypher = f"""
|
||||
MATCH (center:Entity)
|
||||
WHERE center.id IN $matched_ids
|
||||
MATCH (center)-[*1..{hops}]-(neighbor:Entity)
|
||||
RETURN distinct neighbor.id AS id
|
||||
"""
|
||||
neighbor_results = await self.adapter.execute_cypher(
|
||||
neighbors_cypher,
|
||||
{"matched_ids": matched_ids},
|
||||
)
|
||||
for result in neighbor_results:
|
||||
all_node_ids.add(result["id"])
|
||||
|
||||
all_node_ids = list(all_node_ids)[:limit]
|
||||
|
||||
# 6. Fetch all nodes
|
||||
nodes_cypher = """
|
||||
MATCH (n:Entity)
|
||||
WHERE n.id IN $ids
|
||||
RETURN {
|
||||
id: n.id,
|
||||
label: n.label,
|
||||
type: n.type,
|
||||
confidence: n.confidence
|
||||
} AS node
|
||||
"""
|
||||
node_results = await self.adapter.execute_cypher(
|
||||
nodes_cypher,
|
||||
{"ids": all_node_ids},
|
||||
)
|
||||
nodes = [r["node"] for r in node_results]
|
||||
|
||||
# 7. Fetch edges within neighborhood
|
||||
edges_cypher = """
|
||||
MATCH (source:Entity)-[r:RELATES]->(target:Entity)
|
||||
WHERE source.id IN $ids AND target.id IN $ids
|
||||
RETURN {
|
||||
source_id: source.id,
|
||||
target_id: target.id,
|
||||
predicate: r.predicate,
|
||||
confidence: r.confidence
|
||||
} AS edge
|
||||
"""
|
||||
edge_results = await self.adapter.execute_cypher(
|
||||
edges_cypher,
|
||||
{"ids": all_node_ids},
|
||||
)
|
||||
edges = [e["edge"] for e in edge_results]
|
||||
|
||||
logger.info(
|
||||
f"Retrieved semantic context for query '{query}': "
|
||||
f"{len(top_matches)} matched entities, "
|
||||
f"{len(nodes)} total nodes, {len(edges)} edges"
|
||||
)
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"query_embedding_dimension": len(query_embedding),
|
||||
"matched_entities": top_matches,
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"node_count": len(nodes),
|
||||
"edge_count": len(edges),
|
||||
"matched_count": len(top_matches),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve by semantic query: {e}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"query": query,
|
||||
"matched_entities": [],
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"node_count": 0,
|
||||
"edge_count": 0,
|
||||
"matched_count": 0,
|
||||
}
|
||||
|
||||
async def retrieve_neighborhood(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user