Cypher 고급 쿼리 패턴, PageRank/커뮤니티 감지/중심성 등 그래프 알고리즘의 실전 활용, LLM과 그래프 추론의 결합, Text2Cypher 자연어 변환까지 다룹니다.
3장에서 기본 Cypher 문법을 다루었습니다. 이번 장에서는 실전에서 자주 사용되는 고급 패턴을 살펴봅니다.
두 노드 사이의 관계 체인을 탐색하는 패턴입니다.
// 1~5 홉 이내의 모든 경로 탐색
MATCH path = (start:Technology {name: "GraphRAG"})
-[:DEPENDS_ON*1..5]->
(end:Technology)
RETURN path, length(path) AS depth
ORDER BY depth
// 최단 경로 탐색
MATCH path = shortestPath(
(a:Technology {name: "GraphRAG"})-[:DEPENDS_ON|USES*]-(b:Technology {name: "Java"})
)
RETURN path, length(path) AS hops
// 모든 최단 경로 (여러 개 존재할 수 있음)
MATCH path = allShortestPaths(
(a:Technology {name: "GraphRAG"})-[*]-(b:Technology {name: "Java"})
)
RETURN path, length(path) AS hops데이터가 없을 수 있는 관계를 안전하게 조회합니다.
// 기술과 관련 문서 조회 (문서가 없어도 기술은 반환)
MATCH (t:Technology)
WHERE t.category = "Database"
OPTIONAL MATCH (t)<-[:COVERS]-(d:Document)
OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:Technology)
RETURN t.name,
collect(DISTINCT d.title) AS documents,
collect(DISTINCT dep.name) AS dependencies
// 존재하지 않는 관계 필터링
MATCH (t:Technology)
WHERE NOT EXISTS {
MATCH (t)<-[:COVERS]-(d:Document)
}
RETURN t.name AS undocumented_tech복잡한 쿼리를 모듈화하고, 행 단위 처리가 필요할 때 사용합니다.
// 각 카테고리별 상위 3개 기술 조회
MATCH (cat:Category)
CALL (cat) {
MATCH (t:Technology)-[:BELONGS_TO]->(cat)
OPTIONAL MATCH (t)<-[:COVERS]-(d:Document)
WITH t, count(d) AS docCount
ORDER BY docCount DESC
LIMIT 3
RETURN t.name AS techName, docCount
}
RETURN cat.name AS category, techName, docCount
// UNION을 서브쿼리로 대체
MATCH (p:Person {name: "김개발"})
CALL (p) {
MATCH (p)-[:AUTHORED]->(d:Document)
RETURN d.title AS item, "authored" AS type
UNION
MATCH (p)-[:EXPERT_IN]->(t:Technology)
RETURN t.name AS item, "expertise" AS type
}
RETURN item, type**APOC(Awesome Procedures on Cypher)**는 Neo4j의 확장 라이브러리로, 450개 이상의 유틸리티 프로시저를 제공합니다.
// 날짜 기반 시간대 분석
MATCH (d:Document)
WITH d, apoc.date.format(d.createdAt.epochMillis, 'ms', 'yyyy-MM') AS yearMonth
RETURN yearMonth, count(d) AS docCount
ORDER BY yearMonth DESC
// JSON 데이터 임포트
CALL apoc.load.json("https://api.example.com/technologies")
YIELD value
MERGE (t:Technology {name: value.name})
SET t.category = value.category, t.version = value.version
// 동적 관계 생성 (관계 타입이 변수일 때)
WITH "DEPENDS_ON" AS relType
MATCH (a:Technology {name: "GraphRAG"})
MATCH (b:Technology {name: "Neo4j"})
CALL apoc.create.relationship(a, relType, {since: 2024}, b) YIELD rel
RETURN rel
// 가상 그래프 (결과를 그래프로 시각화)
MATCH path = (t:Technology)-[:DEPENDS_ON*1..3]->(dep:Technology)
WHERE t.name = "GraphRAG"
RETURN pathGDS 라이브러리의 주요 알고리즘을 실전 시나리오에 적용합니다.
PageRank는 그래프에서 가장 "중요한" 노드를 식별합니다. 많은 노드가 의존하는 기술은 높은 PageRank 점수를 받습니다.
// 프로젝션 생성
CALL gds.graph.project(
'dependencyGraph',
'Technology',
'DEPENDS_ON'
)
// PageRank 실행 및 결과 저장
CALL gds.pageRank.write('dependencyGraph', {
writeProperty: 'pageRank',
maxIterations: 20,
dampingFactor: 0.85
})
// 결과 조회: 핵심 기술 상위 10개
MATCH (t:Technology)
WHERE t.pageRank IS NOT NULL
RETURN t.name, round(t.pageRank, 4) AS importance
ORDER BY importance DESC
LIMIT 10
// 정리
CALL gds.graph.drop('dependencyGraph')PageRank 점수가 높은 기술은 생태계에서 핵심적인 역할을 합니다. 예를 들어, Python이나 Java는 수많은 프레임워크가 의존하므로 높은 PageRank를 가질 것입니다. 이 정보는 GraphRAG에서 "가장 중요한 기술은 무엇인가?"와 같은 전역 질문에 답하는 데 활용됩니다.
Louvain 알고리즘은 밀접하게 연결된 노드 그룹(커뮤니티)을 자동으로 식별합니다.
// 비방향 그래프 프로젝션 (커뮤니티 감지에 적합)
CALL gds.graph.project(
'techCommunities',
'Technology',
{
DEPENDS_ON: {orientation: 'UNDIRECTED'},
RELATED_TO: {orientation: 'UNDIRECTED'}
}
)
// Louvain 커뮤니티 감지
CALL gds.louvain.stream('techCommunities')
YIELD nodeId, communityId
WITH gds.util.asNode(nodeId) AS tech, communityId
WITH communityId, collect(tech.name) AS members, count(*) AS size
WHERE size >= 2
RETURN communityId, members, size
ORDER BY size DESC
// Leiden 알고리즘 (Louvain의 개선 버전, GraphRAG에서 사용)
CALL gds.leiden.stream('techCommunities', {
gamma: 1.0,
theta: 0.01
})
YIELD nodeId, communityId
WITH gds.util.asNode(nodeId) AS tech, communityId
RETURN communityId, collect(tech.name) AS members
CALL gds.graph.drop('techCommunities')**Betweenness Centrality(매개 중심성)**는 서로 다른 그룹을 연결하는 "다리" 역할을 하는 노드를 식별합니다.
CALL gds.graph.project(
'bridgeGraph',
['Technology', 'Concept'],
{
DEPENDS_ON: {orientation: 'UNDIRECTED'},
IMPLEMENTS: {orientation: 'UNDIRECTED'}
}
)
CALL gds.betweenness.stream('bridgeGraph')
YIELD nodeId, score
WITH gds.util.asNode(nodeId) AS node, score
WHERE score > 0
RETURN node.name, labels(node) AS type, round(score, 2) AS betweenness
ORDER BY betweenness DESC
LIMIT 10
CALL gds.graph.drop('bridgeGraph')매개 중심성이 높은 노드는 서로 다른 기술 커뮤니티를 연결하는 핵심 개념입니다. 예를 들어, "임베딩"이라는 개념은 NLP, 그래프, 검색 등 여러 영역을 연결하는 브리지 역할을 할 수 있습니다.
그래프에서 추출한 구조적 정보를 LLM의 프롬프트에 주입하여 추론 품질을 향상시킵니다.
from neo4j import GraphDatabase
from anthropic import Anthropic
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
llm = Anthropic()
def answer_with_graph_context(question: str, entity: str) -> str:
"""그래프 컨텍스트를 활용하여 질문에 답변합니다."""
# 1. 엔티티 주변 그래프 정보 수집
records, _, _ = driver.execute_query("""
MATCH (e {name: $entity})
OPTIONAL MATCH (e)-[r]->(target)
WITH e, collect({type: type(r), target: target.name, props: properties(r)}) AS outgoing
OPTIONAL MATCH (source)-[r2]->(e)
WITH e, outgoing, collect({type: type(r2), source: source.name}) AS incoming
RETURN e.name AS name,
labels(e) AS types,
properties(e) AS props,
outgoing,
incoming
""", entity=entity)
if not records:
return "해당 엔티티를 찾을 수 없습니다."
record = records[0]
# 2. 그래프 정보를 자연어 컨텍스트로 변환
context_parts = [f"[{record['name']}]은(는) {', '.join(record['types'])} 타입입니다."]
for rel in record["outgoing"]:
if rel["target"]:
context_parts.append(f"- {record['name']} --{rel['type']}--> {rel['target']}")
for rel in record["incoming"]:
if rel["source"]:
context_parts.append(f"- {rel['source']} --{rel['type']}--> {record['name']}")
graph_context = "\n".join(context_parts)
# 3. LLM에 그래프 컨텍스트와 함께 질문
response = llm.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"""다음 지식 그래프 정보를 바탕으로 질문에 답변하세요.
지식 그래프 컨텍스트:
{graph_context}
질문: {question}
답변:"""
}]
)
return response.content[0].text복잡한 질문을 여러 단계의 그래프 쿼리로 분해합니다.
def multi_hop_reasoning(question: str) -> str:
"""다중 홉 추론을 수행합니다."""
# 1. LLM에게 질문 분해 요청
decomposition = llm.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
messages=[{
"role": "user",
"content": f"""다음 질문을 단계별 하위 질문으로 분해하세요.
각 단계는 이전 단계의 결과를 활용합니다.
질문: {question}
JSON 형식으로 응답:
{{"steps": ["하위 질문 1", "하위 질문 2", ...]}}"""
}]
)
# 2. 각 단계를 순차적으로 실행
steps = parse_json(decomposition.content[0].text)["steps"]
accumulated_context = []
for i, step in enumerate(steps):
# 각 하위 질문을 Cypher로 변환하여 실행
cypher = text_to_cypher(step, accumulated_context)
result = execute_cypher(cypher)
accumulated_context.append(f"단계 {i + 1}: {step}\n결과: {result}")
# 3. 최종 답변 생성
final_response = llm.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"""다음 추론 과정을 바탕으로 원래 질문에 답변하세요.
원래 질문: {question}
추론 과정:
{chr(10).join(accumulated_context)}
최종 답변:"""
}]
)
return final_response.content[0].textText2Cypher는 자연어 질문을 Cypher 쿼리로 자동 변환하는 기법입니다. 사용자가 그래프 쿼리 언어를 모르더라도 지식 그래프에 질문할 수 있게 합니다.
TEXT2CYPHER_PROMPT = """당신은 자연어를 Neo4j Cypher 쿼리로 변환하는 전문가입니다.
## 그래프 스키마
노드:
- Person (name, role, affiliation)
- Technology (name, category, version)
- Document (title, content, publishedAt)
- Concept (name, description)
- Category (name)
관계:
- (Person)-[:AUTHORED]->(Document)
- (Person)-[:EXPERT_IN]->(Technology)
- (Document)-[:COVERS]->(Technology)
- (Technology)-[:BELONGS_TO]->(Category)
- (Technology)-[:DEPENDS_ON]->(Technology)
- (Technology)-[:IMPLEMENTS]->(Concept)
## 규칙
1. 유효한 Cypher만 생성하세요
2. 주입 공격 방지를 위해 파라미터($param)를 사용하세요
3. 결과 수를 LIMIT으로 제한하세요
4. 설명 없이 Cypher 쿼리만 반환하세요
"""
def text_to_cypher(question: str, context: list[str] = None) -> str:
"""자연어 질문을 Cypher 쿼리로 변환합니다."""
messages = [{"role": "user", "content": f"질문: {question}"}]
if context:
messages[0]["content"] += f"\n\n이전 컨텍스트:\n{chr(10).join(context)}"
response = llm.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
system=TEXT2CYPHER_PROMPT,
messages=messages
)
cypher = response.content[0].text.strip()
# 마크다운 코드 블록 제거
if cypher.startswith("```"):
cypher = cypher.split("```")[1]
if cypher.startswith("cypher"):
cypher = cypher[6:]
return cypher.strip()class Text2CypherPipeline:
"""자연어 질의를 Cypher로 변환하고 실행하는 파이프라인입니다."""
def __init__(self, driver, llm_client):
self.driver = driver
self.llm = llm_client
def query(self, question: str) -> dict:
"""자연어 질문에 대한 답변을 생성합니다."""
# 1. 자연어 -> Cypher
cypher = text_to_cypher(question)
print(f"생성된 Cypher: {cypher}")
# 2. Cypher 실행
try:
records, _, _ = self.driver.execute_query(cypher)
results = [record.data() for record in records]
except Exception as err:
# 쿼리 실행 실패 시 재시도
print(f"쿼리 실행 실패: {err}")
cypher = self._retry_with_error(question, cypher, str(err))
records, _, _ = self.driver.execute_query(cypher)
results = [record.data() for record in records]
# 3. 결과를 자연어로 변환
answer = self._generate_answer(question, results)
return {
"question": question,
"cypher": cypher,
"raw_results": results,
"answer": answer
}
def _retry_with_error(self, question: str, failed_cypher: str,
error: str) -> str:
"""실패한 쿼리를 오류 정보와 함께 재생성합니다."""
response = self.llm.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
system=TEXT2CYPHER_PROMPT,
messages=[{
"role": "user",
"content": f"""이전에 생성한 Cypher가 실패했습니다. 수정해 주세요.
질문: {question}
실패한 쿼리: {failed_cypher}
오류: {error}
수정된 Cypher:"""
}]
)
return response.content[0].text.strip()
def _generate_answer(self, question: str, results: list[dict]) -> str:
"""쿼리 결과를 자연어 답변으로 변환합니다."""
response = self.llm.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
messages=[{
"role": "user",
"content": f"""다음 데이터를 바탕으로 질문에 자연스럽게 답변하세요.
질문: {question}
데이터: {results}
답변:"""
}]
)
return response.content[0].textText2Cypher는 편리하지만 보안에 주의해야 합니다. LLM이 생성한 Cypher가 데이터를 삭제하거나 수정하는 쿼리를 포함할 수 있으므로, 읽기 전용 사용자로 실행하거나, 생성된 쿼리의 키워드를 검증하는 단계를 추가해야 합니다. CREATE, DELETE, SET, REMOVE, MERGE 같은 쓰기 키워드가 포함된 쿼리는 차단하는 것이 안전합니다.
이번 장에서는 지식 그래프에 대한 쿼리와 추론 기법을 다루었습니다.
다음 장 미리보기: 9장에서는 지금까지 다룬 기술들을 프로덕션 환경에서 운영하는 방법을 다룹니다. 증분 업데이트, 데이터 품질 검증, 스케일링, 모니터링 등 실전 운영의 핵심을 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
지식 그래프의 증분 업데이트, 데이터 품질 검증, 스케일링 전략, 모니터링, 비용 최적화, 그리고 Graphiti를 활용한 실시간 KG 업데이트까지 프로덕션 운영의 핵심을 다룹니다.
TransE, DistMult, ComplEx 등 관계 예측 모델과 Node2Vec, GraphSAGE 등 노드 임베딩 기법, PyTorch Geometric을 활용한 구현까지 지식 그래프 임베딩의 핵심을 다룹니다.
기술 문서에서 LLM으로 지식 그래프를 구축하고, GraphRAG로 자연어 질의를 처리하며, 벡터 전용 RAG와 성능을 비교하는 엔드투엔드 실전 프로젝트를 구현합니다.