
Graph RAG with Milvus: 벡터 검색에 지식 그래프를 더하면 AI가 '관계'를 추론한다
'오일러의 스승의 아들이 뭘 했지?' — 단순 벡터 검색은 이 질문에 답하지 못한다. 엔티티 사이의 관계를 추적해야 하기 때문이다. Milvus 벡터 DB 위에 지식 그래프를 결합한 Graph RAG가 멀티홉 추론 문제를 어떻게 해결하는지, 역사적 맥락부터 핵심 구현, 2026년 생태계까지 종합 특집으로 풀어본다.

'오일러의 스승의 아들이 뭘 했지?' — 단순 벡터 검색은 이 질문에 답하지 못한다. 엔티티 사이의 관계를 추적해야 하기 때문이다. Milvus 벡터 DB 위에 지식 그래프를 결합한 Graph RAG가 멀티홉 추론 문제를 어떻게 해결하는지, 역사적 맥락부터 핵심 구현, 2026년 생태계까지 종합 특집으로 풀어본다.
이런 질문을 AI에게 던져보자.
"What contribution did the son of Euler's teacher make?"
단순해 보이지만, 이 질문에 답하려면 AI가 세 단계의 관계를 추적해야 한다:
일반적인 RAG 시스템에 이 질문을 던지면 어떻게 될까? 벡터 검색은 질문과 가장 유사한 문서 청크를 가져온다. "오일러"와 "스승"이 언급된 문단, "베르누이 가문"에 대한 문단은 찾을 수 있다. 하지만 "다니엘 베르누이의 업적"이 담긴 문단은? 원래 질문과의 벡터 유사도가 낮아서 검색되지 않는다.
결과는 이렇다:
같은 데이터, 같은 질문인데 답이 완전히 다르다. Graph RAG가 할 수 있고 Naive RAG가 할 수 없는 것 — 그것은 엔티티 사이의 관계를 추적하는 멀티홉 추론이다.
이 글에서는 Milvus 벡터 데이터베이스를 활용한 Graph RAG의 구현을 중심으로, 이 기술이 왜 필요한지, 어떻게 작동하는지, 그리고 2026년 현재 어디까지 왔는지를 종합적으로 다룬다.
📎 이 글은 Milvus 공식 부트캠프의 Graph RAG with Milvus 노트북을 기반으로 작성되었다.

지식 그래프의 뿌리는 멀리 있다. 2001년, 팀 버너스리(Tim Berners-Lee)는 "The Semantic Web"이라는 사이언티픽 아메리칸 기고에서 "기계가 이해할 수 있는 웹"을 제안했다. 웹 페이지가 단순한 텍스트가 아니라, 구조화된 데이터로 연결되어 기계가 추론할 수 있는 세계 — 그것이 시맨틱 웹의 비전이었다.
이 비전을 실현하기 위한 기술들이 차례로 등장했다:
| 연도 | 기술 | 의미 |
|---|---|---|
| 2001 | RDF (Resource Description Framework) | 모든 지식을 (주어, 술어, 목적어) 트리플렛으로 표현 |
| 2004 | OWL (Web Ontology Language) | 개념 간의 계층과 제약 조건을 정의 |
| 2007 | DBpedia | 위키피디아에서 구조화된 데이터를 추출한 최초의 대규모 지식 베이스 |
| 2008 | Freebase | 커뮤니티 기반 지식 베이스 (후에 Google이 인수) |
하지만 시맨틱 웹은 너무 학술적이었다. 일반 웹 개발자가 RDF와 SPARQL(W3C, 2008년 표준화)을 배워서 웹사이트를 만들기에는 진입 장벽이 높았고, 채택률은 낮았다. 한편 2007년에는 Neo4j가 프로퍼티 그래프(Property Graph) 모델을 대중화하며, 학술적인 RDF 트리플보다 개발자 친화적인 방식으로 그래프 데이터를 다루는 길을 열었다.
2012년 5월, 구글이 Knowledge Graph를 공개하며 게임이 바뀌었다. "검색 결과를 넘어, 사물(things)을 이해하라"라는 슬로건 아래, 구글은 5억 개 이상의 엔티티와 35억 개의 관계를 담은 거대한 지식 그래프를 구축했다.
"레오나르도 다빈치"를 검색하면 단순히 관련 웹페이지 목록이 아니라, 생년월일, 직업, 대표작, 관련 인물이 패널로 정리되어 나타났다. 이것이 지식 그래프의 대중화 순간이었다.
그 뒤를 이어 주요 지식 그래프들이 잇따라 등장했다:
모든 지식 그래프의 기본 구성 요소는 트리플렛(Triplet)이다. (주어, 술어, 목적어) — 이 세 요소로 세상의 모든 관계를 표현한다.

베르누이 가문을 예로 들어보자:
이 트리플렛들을 이으면 그래프가 된다. 노드(node)는 엔티티, 엣지(edge)는 관계다. 그래프 위에서 경로를 따라가면 "오일러 → 요한 베르누이 → 다니엘 베르누이 → 베르누이 원리"라는 추론 체인이 자연스럽게 만들어진다.
바로 이것이 멀티홉 추론의 핵심이다.
RAG(Retrieval-Augmented Generation)는 Facebook AI Research(FAIR)의 Patrick Lewis 등이 2020년 발표한 논문 "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks"(NeurIPS 2020)에서 제안된 이후, LLM의 환각(hallucination)을 줄이는 표준적인 기법이 되었다. 사전 학습된 seq2seq 모델(BART)에 Dense Passage Retriever(DPR)를 결합하여, 생성 시점에 위키피디아 인덱스에서 관련 문서를 검색하는 방식이었다.
기본 원리는 단순하다:
"벡터 검색"이란 질문을 임베딩 벡터로 변환한 뒤, 문서 벡터와의 코사인 유사도가 높은 상위 K개를 가져오는 것이다. "재택근무 정책이 뭐야?"라는 질문에는 잘 작동한다 — 관련 문서에 "재택근무"와 "정책"이라는 의미가 직접 담겨 있기 때문이다.
하지만 멀티홉 질문은 다르다. 다시 원래의 질문으로 돌아가자:
"오일러의 스승의 아들은 뭘 했지?"
이 질문의 벡터와 가장 유사한 문서 청크는 무엇일까?
세 번째 문서가 바로 정답의 핵심인데, 왜 검색이 안 될까? 질문에는 "다니엘 베르누이"도, "유체역학"도, "베르누이 원리"도 언급되지 않기 때문이다. 벡터 유사도가 낮다.

멀티홉 QA 벤치마크(HotpotQA, MuSiQue, 2WikiMultiHopQA)에서 일반 RAG의 정확도가 급락하는 이유도 여기에 있다. 심지어 관련 청크를 찾아오더라도, Liu et al.(2023)이 밝힌 "Lost in the Middle" 문제 — LLM이 컨텍스트 윈도우의 중간에 있는 정보를 놓치는 현상 — 로 인해 정답을 놓칠 수 있다.
이것이 벡터 검색의 구조적 한계다:
Milvus 노트북의 실제 실험 결과를 보자:
| 방식 | 검색된 문서 | 답변 |
|---|---|---|
| Naive RAG | ① 오일러-베르누이 관계 문서 ② 요한 베르누이 업적 문서 | "정보 부족으로 답변 불가" |
| Graph RAG | ① 오일러-베르누이 관계 문서 ② 다니엘 베르누이 업적 문서 | "다니엘 베르누이는 유체역학, 확률론에 기여. 베르누이 원리로 유명" |
Naive RAG는 "요한 베르누이 업적 문서"를 가져왔다. 질문과 유사도는 높지만, 아들인 다니엘 베르누이의 업적이 담긴 문서는 아니다. Graph RAG는 엔티티 관계를 추적하여 정확히 "다니엘 베르누이 업적 문서"를 찾아냈다.
2024년 4월, Microsoft Research의 Darren Edge 등이 발표한 "From Local to Global: A Graph RAG Approach to Query-Focused Summarization"는 Graph RAG 분야를 폭발적으로 성장시킨 기점이 되었다.
핵심 아이디어: 문서에서 엔티티와 관계를 추출하여 지식 그래프를 구축하고, Leiden 알고리즘으로 커뮤니티(클러스터)를 탐지한 뒤, 각 커뮤니티의 요약을 생성한다. 질문에 따라 로컬 검색(특정 엔티티 주변)과 글로벌 검색(커뮤니티 요약 기반)을 사용한다.
자세한 내용은 GraphRAG 완전 가이드를 참고하라.
GraphRAG 이후 여러 변형이 등장했다:
이 글의 주인공인 Milvus Graph RAG는 기존 접근법들과 결이 다르다. 대규모 그래프 구축이나 커뮤니티 탐지 대신, 벡터 데이터베이스 위에서 인접 행렬 연산으로 서브그래프를 확장하는 경량 접근법이다. 작지만 강력하다.
Milvus는 Zilliz(2017년 Xie Chao가 설립)가 개발한 오픈소스 벡터 데이터베이스다. 2019년 10월 첫 공개, 2020년 Linux Foundation AI & Data Foundation에 합류, 2022년 클라우드 네이티브 분산 아키텍처인 Milvus 2.0 출시를 거쳐 GitHub 스타 30,000개를 돌파하며 Pinecone, Weaviate, Qdrant와 함께 벡터 DB 분야의 선두 그룹을 형성하고 있다.
핵심 특징:
기존 Graph RAG 시스템(MS GraphRAG, LightRAG)은 자체 그래프 저장소를 사용하거나 Neo4j 같은 그래프 DB에 의존한다. Milvus 접근법은 다른 철학을 취한다:
이 접근법의 장점은 명확하다:
이제 Milvus Graph RAG의 구현을 살펴보자. 전체 아키텍처는 의외로 간결하다.
먼저, 문서에서 트리플렛을 추출한다. 실제 프로덕션에서는 NER(Named Entity Recognition) 모델이나 LLM을 사용하지만, 핵심 원리를 이해하기 위해 사전 정의된 데이터를 사용한다.
nano_dataset = [
{
"passage": "Jakob Bernoulli (1654–1705): Jakob was one of the earliest members...",
"triplets": [
["Jakob Bernoulli", "made significant contributions to", "calculus"],
["Jakob Bernoulli", "was the older brother of", "Johann Bernoulli"],
# ...
],
},
# ... 총 4개의 문서, 22개의 트리플렛
]
각 트리플렛에서 엔티티(주어/목적어)와 관계(주어 + 술어 + 목적어를 합친 문장)를 추출한다:
entities = [] # ["Jakob Bernoulli", "calculus", "Johann Bernoulli", ...]
relations = [] # ["Jakob Bernoulli made significant contributions to calculus", ...]
passages = [] # [원본 문서 텍스트, ...]
그리고 두 가지 핵심 매핑을 구축한다:
이 매핑이 나중에 서브그래프 확장과 최종 문서 역추적의 핵심이 된다.
데이터를 3개의 벡터 컬렉션에 나누어 저장한다:
각 컬렉션에 데이터를 삽입하는 코드:
from pymilvus import MilvusClient
from langchain_openai import OpenAIEmbeddings
milvus_client = MilvusClient(uri="./milvus.db") # Milvus Lite
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
# 엔티티, 관계, 문서를 각각 임베딩하여 저장
milvus_insert(collection_name="entity_collection", text_list=entities)
milvus_insert(collection_name="relation_collection", text_list=relations)
milvus_insert(collection_name="passage_collection", text_list=passages)
왜 3개로 나누는가? 이것이 이 접근법의 핵심 설계 결정이다:
엔티티와 관계를 분리하여 검색하고, 나중에 병합하는 것이 핵심이다. 이것을 다중 경로 검색(Multi-way Retrieval)이라 부른다.

이 방법의 핵심 아이디어: 하나의 질문을 두 가지 다른 경로로 검색한다.
질문에서 엔티티를 추출(NER)하고, 엔티티 컬렉션에서 유사한 엔티티를 찾는다.
query = "What contribution did the son of Euler's teacher make?"
query_ner_list = ["Euler"] # NER로 추출
# "Euler"의 임베딩으로 Entity Collection 검색
entity_search_res = milvus_client.search(
collection_name="entity_collection",
data=query_ner_embeddings,
limit=3, # Top-3 엔티티
)
이 경로는 명시적으로 언급된 엔티티를 잡아낸다.
질문 전체를 Relation Collection에서 검색하여, 의미적으로 유사한 관계를 찾는다.
# 질문 전체의 임베딩으로 Relation Collection 검색
query_embedding = embedding_model.embed_query(query)
relation_search_res = milvus_client.search(
collection_name="relation_collection",
data=[query_embedding],
limit=3, # Top-3 관계
)
이 경로는 질문과 의미적으로 관련된 관계를 잡아낸다.
| 엔티티 경로 | 관계 경로 | |
|---|---|---|
| 검색 대상 | 엔티티 이름 | 관계 문장 전체 |
| 잡아내는 것 | 명시적으로 언급된 개체 | 의미적으로 관련된 관계 |
| 강점 | 정확한 엔티티 매칭 | 넓은 의미 커버리지 |
| 약점 | NER 정확도에 의존 | 관련 없는 관계 포함 가능 |
| 비유 | GPS로 정확한 주소 찾기 | 주변 탐색으로 관련 장소 발견 |
두 경로를 합치면 정확성과 포괄성을 모두 확보할 수 있다. 마치 지도에서 정확한 주소(엔티티)와 주변 탐색(관계)을 동시에 하는 것과 같다.

검색으로 시작점을 찾았다면, 이제 그 주변을 확장해야 한다. "Euler"를 찾았으니, Euler와 연결된 "Johann Bernoulli"를, 그리고 Johann과 연결된 "Daniel Bernoulli"를 찾아야 한다.
이것을 서브그래프 확장(Subgraph Expansion)이라 하며, 이 노트북은 인접 행렬(Adjacency Matrix)을 사용한 매우 우아한 방법을 제시한다.
엔티티와 관계 사이의 연결을 행렬로 표현한다:
import numpy as np
from scipy.sparse import csr_matrix
# 엔티티-관계 인접 행렬 (엔티티 수 × 관계 수)
entity_relation_adj = np.zeros((len(entities), len(relations)))
for entity_id, entity in enumerate(entities):
entity_relation_adj[entity_id, entityid_2_relationids[entity_id]] = 1
# 희소 행렬로 변환 (효율적 연산)
entity_relation_adj = csr_matrix(entity_relation_adj)
행렬 A의 크기는 (엔티티 수 × 관계 수)다. 값이 1이면 "이 엔티티가 이 관계에 참여한다"는 뜻이다.
여기서 수학의 마법이 시작된다.
엔티티-엔티티 1-degree 인접 행렬:
행렬 와 그 전치행렬 의 곱은 "같은 관계에 참여하는 엔티티 쌍"을 찾아낸다. 이면 엔티티 와 가 적어도 하나의 관계를 공유한다는 뜻이다.
관계-관계 1-degree 인접 행렬:
마찬가지로, 같은 엔티티를 공유하는 관계 쌍을 찾는다.
# 1-degree 인접 행렬
entity_adj_1_degree = entity_relation_adj @ entity_relation_adj.T
relation_adj_1_degree = entity_relation_adj.T @ entity_relation_adj
2-degree 이웃이 필요하면? 행렬을 한 번 더 곱하면 된다:
이렇게 하면 "친구의 친구"까지 도달할 수 있다. 대부분의 경우 1~2 degree면 충분하다.
target_degree = 1 # 대부분 1이면 충분
# target degree 인접 행렬 계산
entity_adj_target = entity_adj_1_degree
for _ in range(target_degree - 1):
entity_adj_target = entity_adj_target * entity_adj_1_degree
# 엔티티에서 도달 가능한 관계 찾기
entity_relation_adj_target = entity_adj_target @ entity_relation_adj
검색된 엔티티와 관계 각각에서 서브그래프를 확장한 뒤, 두 결과를 합집합(Union)으로 병합한다:
# 관계 경로에서 확장
expanded_from_relation = set()
for hit_id in filtered_hit_relation_ids:
expanded_from_relation.update(
relation_adj_target[hit_id].nonzero()[1].tolist()
)
# 엔티티 경로에서 확장
expanded_from_entity = set()
for hit_id in filtered_hit_entity_ids:
expanded_from_entity.update(
entity_relation_adj_target[hit_id].nonzero()[1].tolist()
)
# 합집합으로 후보 관계 구성
candidate_ids = list(expanded_from_relation | expanded_from_entity)
이 접근법이 우아한 이유는 계산 효율성에 있다:
그래프 DB의 BFS/DFS 탐색과 비교하면, 행렬 연산은 배치 처리에 최적화되어 있어 벡터 DB 환경에 자연스럽게 통합된다.
서브그래프 확장으로 후보 관계를 모았다면, 이제 진짜 관련 있는 것만 골라야 한다. 여기서 LLM의 추론 능력이 등장한다.
LLM에게 후보 관계 목록과 질문을 함께 주고, 사고 과정(thought process)과 함께 가장 유용한 관계 3개를 선택하도록 한다.
이 One-Shot 예제를 바탕으로, 실제 질문에 대해서도 같은 패턴으로 추론한다:
def rerank_relations(query, candidate_texts, candidate_ids):
# Chain-of-Thought 프롬프트
rerank_prompts = ChatPromptTemplate.from_messages([
HumanMessage(one_shot_input), # One-shot 예제 입력
AIMessage(one_shot_output), # One-shot 예제 출력
HumanMessagePromptTemplate(...) # 실제 질문 + 후보 관계
])
# JSON 형식으로 응답 강제
rerank_chain = (
rerank_prompts
| llm.bind(response_format={"type": "json_object"})
| JsonOutputParser()
)
result = rerank_chain.invoke({
"question": query,
"relation_des_str": relation_descriptions
})
return result["useful_relationships"]
| 설계 요소 | 선택 | 이유 |
|---|---|---|
| Few-shot 수 | One-shot (1개) | 토큰 효율 + 충분한 형식 안내 |
| 추론 방식 | Chain-of-Thought | 멀티홉 추론 경로를 명시적으로 추적 |
| 출력 형식 | JSON | 파싱 안정성 (json_object 모드) |
| 선택 수 | 3개 | 정밀도-재현율 균형 |
벡터 유사도는 "의미적 가까움"을 측정하지만, 추론 체인의 유용성은 측정하지 못한다. "Euler was born in Basel"은 질문과 벡터 유사도가 높을 수 있지만, 답에 필요한 관계가 아니다. LLM은 추론 경로를 따라 정말 필요한 관계를 판별할 수 있다.
지금까지의 모든 단계를 하나의 파이프라인으로 정리하자:
리랭킹된 관계에서 원본 문서를 역추적한다:
final_passages = []
for relation_id in rerank_relation_ids:
for passage_id in relationid_2_passageids[relation_id]:
if passage_id not in final_passage_ids:
final_passages.append(passages[passage_id])
# Top-2 문서로 최종 답변 생성
answer = rag_chain.invoke({
"question": query,
"context": "\n".join(final_passages[:2])
})

같은 질문, 같은 데이터. 결과는 완전히 다르다.
Naive RAG가 가져온 두 문서는 오일러와 요한 베르누이에 대한 것이다. 질문의 핵심인 "아들(다니엘 베르누이)의 업적"이 없다.
답변: "검색된 문맥에서 오일러 스승의 아들이 한 기여에 대한 정보를 찾을 수 없습니다."
Graph RAG는 "Euler → Johann Bernoulli" 관계를 찾고, 서브그래프를 확장하여 "Johann Bernoulli → Daniel Bernoulli"를 거쳐, 다니엘 베르누이의 업적 문서를 정확히 가져왔다.
답변: "오일러의 스승인 요한 베르누이의 아들, 다니엘 베르누이는 유체역학, 확률론, 통계학에 주요한 기여를 했습니다. 그는 유체 흐름의 행동을 설명하는 베르누이 원리로 가장 유명하며, 이는 공기역학 이해의 근본이 됩니다."
Naive RAG는 직접 유사도 검색에서는 Graph RAG와 비슷하지만, 관계 추적이 전혀 안 된다. 멀티홉 질문에서의 성패가 여기서 갈린다.
2026년 현재, Graph RAG 생태계는 빠르게 성숙하고 있다. 각 접근법의 특성을 이해하고 상황에 맞게 선택하는 것이 중요하다.
| MS GraphRAG | LightRAG | PathRAG | Milvus Graph RAG | |||||
|---|---|---|---|---|---|---|---|---|
| 출시 | 2024.04 | 2024.10 | 2025 | 2024 | ||||
| 그래프 구축 | LLM 추출 + Leiden 클러스터링 | LLM 추출 + 키-값 | LLM 추출 + 경로 프루닝 | 트리플렛 + 인접 행렬 | ||||
| 검색 방식 | 로컬/글로벌 이중 | 저수준/고수준 듀얼 레벨 | 최단 경로 탐색 | 다중 경로 + LLM 리랭킹 | ||||
| 별도 그래프 DB | 필요 | 불필요 | 불필요 | 불필요 | ||||
| 인덱싱 비용 | 매우 높음 | 낮음 | 중간 | 낮음 | ||||
| 멀티홉 추론 | 강함 | 중간 | 강함 | 강함 | ||||
| 글로벌 질문 | 매우 강함 | 중간 | 약함 | 약함 | ||||
| 증분 업데이트 | 어려움 | 쉬움 | 중간 | 쉬움 | ||||
| 적합한 규모 | 대규모 | 중소규모 | 중규모 | 중소규모 |
이 표에 담기지 않은 중요한 연구들도 있다:
1. 하이브리드 접근법의 부상
단일 방법론에 의존하기보다, 상황에 따라 Naive RAG, Graph RAG, Agentic RAG를 결합하는 하이브리드 파이프라인이 주류가 되고 있다. 질문 유형을 분류(단순 사실 vs 멀티홉 vs 글로벌)한 뒤, 적합한 검색 전략을 선택하는 라우터 패턴이 보편화되었다.
2. 자동 트리플렛 추출의 성숙
LLM 기반 트리플렛 추출의 품질이 크게 향상되었다. GPT-4o, Claude 등의 모델이 긴 문서에서도 안정적으로 엔티티와 관계를 추출하며, 특히 도메인 특화 파인튜닝된 소형 모델(7B~13B)이 비용 효율적인 대안으로 떠올랐다.
3. 벡터 DB와 그래프 DB의 융합
Milvus를 포함한 주요 벡터 DB들이 그래프 구조를 네이티브로 지원하기 시작했다. 반대 방향에서는 Neo4j가 LangChain과의 통합으로 벡터 검색을 지원하고, Amazon Neptune Analytics가 그래프+벡터 하이브리드 쿼리를 도입했다. 별도의 인접 행렬을 관리하는 대신, 벡터 검색과 그래프 탐색을 하나의 쿼리로 실행하는 방향으로 양쪽 모두 수렴하고 있다.
Graph RAG는 엔티티 간 관계가 핵심인 도메인에서 빛난다.
부서, 프로젝트, 고객, 이슈 — 네 종류의 엔티티를 관계로 연결해야 답할 수 있다. Naive RAG로는 불가능하다.
환자의 증상(A) → 가능한 질병(B) → 권장 검사(C) → 약물 상호작용(D). 의료 지식은 본질적으로 멀티홉 관계의 체인이다. Graph RAG를 활용한 의료 QA 시스템은 진단 보조에서 특히 유망하다.
법 조항(A) → 판례(B) → 인용된 다른 판례(C) → 관련 법 조항(D). 법률 문서 분석에서 "이 조항과 관련된 모든 판례의 최종 결론은?"이라는 질문은 전형적인 멀티홉 문제다.
기업 A → 공급 계약 → 기업 B → 대출 관계 → 은행 C. 공급망 리스크의 연쇄 전파를 추적하는 데 Graph RAG가 강력하다. 2025년 이후 금융 규제(Basel III+)에서도 이런 연결 분석이 요구되고 있다.
"오일러의 스승의 아들은 뭘 했지?"라는 질문은 단순해 보이지만, AI에게는 깊은 도전이다. 단어의 유사도가 아니라 엔티티 사이의 관계를 추적해야 하기 때문이다.
Graph RAG는 이 도전에 대한 우아한 답이다. 그리고 Milvus 접근법은 그 답을 가장 가볍고 실용적인 방식으로 구현할 수 있음을 보여준다.
벡터 검색이 "의미"를 이해하게 해주었다면, Graph RAG는 "관계"를 이해하게 해준다. 다음 단계는 무엇일까? 아마도 "맥락"과 "의도"까지 이해하는 AI 검색 — Agentic RAG가 그 답이 될지도 모른다.