Models & Algorithms

GraphRAG: Microsoft의 글로벌-로컬 이중 검색 전략

왜 기존 RAG는 "이 문서들의 주요 테마가 뭐야?"라는 질문에 답하지 못할까? Microsoft Research의 GraphRAG가 제시하는 커뮤니티 기반 검색의 비밀.

GraphRAG: Microsoft의 글로벌-로컬 이중 검색 전략

GraphRAG: Microsoft의 글로벌-로컬 이중 검색 전략

왜 기존 RAG는 "이 문서들의 주요 테마가 뭐야?"라는 질문에 답하지 못할까? Microsoft Research의 GraphRAG가 제시하는 커뮤니티 기반 검색의 비밀.

들어가며: 기존 RAG의 치명적 맹점

기존 RAG 시스템에게 이런 질문을 해보세요:

"이 1000개 문서에서 드러나는 주요 트렌드와 패턴은 무엇인가요?"

결과는? 실패. 혹은 아무 의미 없는 단편적 답변.

왜 실패하는가?

기존 RAG의 작동 방식을 떠올려보세요:

  1. 질문을 임베딩으로 변환
  2. 가장 유사한 청크 K개 검색
  3. 검색된 청크로 답변 생성

문제는 "유사한 청크"가 "전체를 대표하는 청크"가 아니라는 것입니다.

비유: 숲을 보려는데 가장 가까운 나무 3그루만 보여주는 격입니다.

질문 유형기존 RAGGraphRAG
특정 사실 검색잘함잘함
전체 요약/테마실패해결
패턴/트렌드 분석실패해결
다중 문서 종합제한적해결
관계 추론불가해결

GraphRAG란 무엇인가?

GraphRAG는 Microsoft Research가 2024년 4월에 발표한 새로운 RAG 패러다임입니다.

핵심 아이디어는 단순합니다:

"질문 시점이 아닌, 인덱싱 시점에 요약을 미리 만들어두자"

하지만 단순 요약이 아닙니다. 커뮤니티 기반 계층적 요약입니다.

4단계 파이프라인

text
Documents → Entity Extraction → Graph Construction → Community Detection → Hierarchical Summarization
  1. Entity Extraction: 문서에서 엔티티와 관계 추출
  2. Graph Construction: 엔티티를 노드, 관계를 엣지로 하는 그래프 구축
  3. Community Detection: Leiden 알고리즘으로 밀접하게 연결된 엔티티 그룹화
  4. Hierarchical Summarization: 각 커뮤니티에 대해 미리 요약 생성

이제 "전체 테마가 뭐야?"라는 질문이 오면, 모든 커뮤니티 요약을 조합하여 답변합니다.

Local vs Global 검색

GraphRAG는 두 가지 검색 모드를 제공합니다.

Local Search

용도: 특정 엔티티에 대한 질문

예시: "알파테크는 어떤 회사와 협력하고 있나요?"

작동 방식:

  1. 쿼리에서 "알파테크" 엔티티 추출
  2. 그래프에서 알파테크의 이웃 노드 탐색
  3. 1-hop, 2-hop 관계 정보 수집
  4. 관련 정보로 답변 생성

Global Search

용도: 전체 데이터셋에 대한 질문

예시: "이 문서들의 주요 테마와 트렌드는?"

작동 방식:

  1. 모든 커뮤니티 요약 수집
  2. 각 요약에서 관련 정보 추출
  3. 부분 답변들을 종합하여 최종 답변 생성

이것이 기존 RAG가 불가능했던 "숲을 보는" 능력입니다.

실습 환경 설정

필수 패키지 설치

python
# Microsoft GraphRAG 공식 라이브러리
pip install graphrag

# 추가 의존성
pip install networkx matplotlib pandas numpy
pip install tiktoken openai python-dotenv

Python 버전 요구사항

GraphRAG는 Python 3.10~3.12를 지원합니다.

Step 1: Entity Extraction (엔티티 추출)

GraphRAG의 첫 단계는 문서에서 엔티티와 관계를 추출하는 것입니다.

실제 GraphRAG는 LLM을 사용하지만, 핵심 로직을 이해하기 위해 직접 구현해봅시다.

샘플 데이터 준비

엔터프라이즈 시나리오를 시뮬레이션하기 위해 뉴스와 기술 문서를 혼합한 데이터를 사용합니다.

python
SAMPLE_DOCUMENTS = [
    {
        "id": "news_1",
        "type": "news",
        "title": "AI 스타트업 알파테크, 시리즈 B 투자 유치",
        "content": """
        AI 스타트업 알파테크가 VC인 블루벤처스로부터 500억원 규모의 시리즈 B 투자를 유치했다.
        알파테크의 CEO 김철수는 "이번 투자로 RAG 기술 고도화에 집중하겠다"고 밝혔다.
        알파테크는 삼성전자, LG전자와 협력하여 엔터프라이즈 AI 솔루션을 제공하고 있다.
        """
    },
    {
        "id": "news_2",
        "type": "news",
        "title": "삼성전자, AI 반도체 신제품 발표",
        "content": """
        삼성전자가 차세대 AI 반도체 'Exynos AI'를 발표했다.
        이 칩은 알파테크의 RAG 엔진과 호환되며, 현대자동차의 자율주행 시스템에도 탑재될 예정이다.
        """
    },
    # ... 더 많은 문서
]

엔티티 데이터 구조

python
from dataclasses import dataclass, field
from typing import List

@dataclass
class Entity:
    """추출된 엔티티"""
    name: str
    type: str  # ORGANIZATION, PERSON, TECHNOLOGY, PRODUCT
    description: str = ""
    source_docs: List[str] = field(default_factory=list)

@dataclass
class Relationship:
    """엔티티 간 관계"""
    source: str
    target: str
    relation_type: str  # INVESTED_IN, PARTNERED_WITH, DEVELOPED
    weight: float = 1.0
    source_docs: List[str] = field(default_factory=list)

엔티티 추출기 구현

python
class EntityExtractor:
    """문서에서 엔티티와 관계를 추출"""

    def __init__(self, entity_definitions: dict):
        self.entity_definitions = entity_definitions

    def extract_entities(self, documents: List[dict]) -> List[Entity]:
        """문서에서 엔티티 추출"""
        entities = {}

        for doc in documents:
            content = doc['content']
            doc_id = doc['id']

            for name, (entity_type, description) in self.entity_definitions.items():
                if name in content:
                    if name not in entities:
                        entities[name] = Entity(
                            name=name,
                            type=entity_type,
                            description=description,
                            source_docs=[doc_id]
                        )
                    else:
                        if doc_id not in entities[name].source_docs:
                            entities[name].source_docs.append(doc_id)

        return list(entities.values())

    def extract_relationships(self, documents: List[dict], entities: List[Entity]) -> List[Relationship]:
        """같은 문장에 등장하는 엔티티 간 관계 추출"""
        relationships = []
        entity_names = {e.name for e in entities}

        for doc in documents:
            sentences = doc['content'].split('.')

            for sentence in sentences:
                # 문장에 등장하는 엔티티들 찾기
                found = [e for e in entities if e.name in sentence]

                # 같은 문장의 엔티티들 간 관계 생성
                for i, e1 in enumerate(found):
                    for e2 in found[i+1:]:
                        relationships.append(Relationship(
                            source=e1.name,
                            target=e2.name,
                            relation_type=self._infer_relation_type(e1, e2),
                            source_docs=[doc['id']]
                        ))

        return self._deduplicate(relationships)

실행 결과:

text
추출된 엔티티: 39개
추출된 관계: 35개

=== 엔티티 유형별 분포 ===
ORGANIZATION: 15개
PERSON: 6개
TECHNOLOGY: 7개
PRODUCT: 5개
TOOL: 5개

Step 2: Graph Construction (그래프 구축)

추출한 엔티티와 관계를 NetworkX 그래프로 구축합니다.

python
import networkx as nx

class KnowledgeGraph:
    """GraphRAG용 Knowledge Graph"""

    def __init__(self):
        self.graph = nx.Graph()  # 무방향 (커뮤니티 탐지용)
        self.directed_graph = nx.DiGraph()  # 방향 (쿼리용)
        self.entities = {}

    def add_entities(self, entities: List[Entity]):
        for entity in entities:
            self.entities[entity.name] = entity
            self.graph.add_node(
                entity.name,
                type=entity.type,
                description=entity.description
            )

    def add_relationships(self, relationships: List[Relationship]):
        for rel in relationships:
            self.graph.add_edge(
                rel.source, rel.target,
                relation=rel.relation_type,
                weight=rel.weight
            )

허브 노드 분석

연결이 많은 엔티티(허브 노드)를 찾으면 데이터셋의 핵심 주제를 파악할 수 있습니다.

python
degree_centrality = nx.degree_centrality(kg.graph)
top_hubs = sorted(degree_centrality.items(), key=lambda x: x[1], reverse=True)[:5]

print("=== 허브 노드 (연결이 많은 엔티티) ===")
for node, centrality in top_hubs:
    print(f"{node}: {kg.graph.degree(node)}개 연결")
text
=== 허브 노드 ===
알파테크: 15개 연결
삼성전자: 9개 연결
RAG: 6개 연결
LG전자: 6개 연결
현대자동차: 5개 연결

Step 3: Community Detection (커뮤니티 탐지)

GraphRAG의 핵심 비밀: Leiden 알고리즘으로 밀접하게 연결된 엔티티들을 그룹화합니다.

왜 커뮤니티가 중요한가?

커뮤니티는 의미적으로 관련된 엔티티들의 집합입니다. 각 커뮤니티는 하나의 "주제" 또는 "테마"를 대표합니다.

예를 들어:

  • Community 0: AI 스타트업 생태계 (알파테크, 블루벤처스, 투자자들)
  • Community 1: 자율주행/반도체 (삼성전자, 현대자동차, NVIDIA)
  • Community 2: 스마트홈 AI (LG전자, OpenAI, 아마존)

구현

python
from networkx.algorithms import community

class CommunityDetector:
    """커뮤니티 탐지 및 계층 구조 생성"""

    def __init__(self, graph: nx.Graph):
        self.graph = graph
        self.communities = []
        self.node_to_community = {}

    def detect_communities(self, resolution: float = 1.0) -> List[set]:
        """
        Louvain 알고리즘으로 커뮤니티 탐지
        (Leiden 알고리즘의 간소화 버전)
        """
        communities = community.louvain_communities(
            self.graph,
            resolution=resolution,
            seed=42
        )

        self.communities = [set(c) for c in communities]

        # 노드 → 커뮤니티 매핑
        for i, comm in enumerate(self.communities):
            for node in comm:
                self.node_to_community[node] = i

        return self.communities

실행 결과:

text
=== 탐지된 커뮤니티 ===

Community 0 (11 members):
  주요 멤버: 알파테크, 블루벤처스, 김철수, 이영희, 박지민
  추정 주제: AI 스타트업 및 투자 생태계

Community 1 (8 members):
  주요 멤버: 삼성전자, 현대자동차, NVIDIA, 테슬라, 웨이모
  추정 주제: 자율주행 및 AI 하드웨어

Community 2 (9 members):
  주요 멤버: LG전자, OpenAI, 구글, 아마존, 한미래
  추정 주제: 스마트홈 및 AI 어시스턴트

Community 3 (6 members):
  주요 멤버: RAG, Knowledge Graph, Vector Store, Embedding
  추정 주제: RAG 및 검색 기술

Community 4 (5 members):
  주요 멤버: LLM, 양자화, TensorRT, vLLM
  추정 주제: LLM 최적화 및 추론

Step 4: Hierarchical Summarization (계층적 요약)

각 커뮤니티에 대해 요약을 미리 생성합니다.

이것이 GraphRAG의 핵심 비밀: 질문 시점이 아닌, 인덱싱 시점에 요약을 생성합니다.

python
class CommunitySummarizer:
    """커뮤니티별 요약 생성"""

    def __init__(self, graph: nx.Graph, communities: List[set]):
        self.graph = graph
        self.communities = communities
        self.summaries = {}

    def generate_summary(self, community_idx: int) -> str:
        """커뮤니티 요약 생성 (실제로는 LLM 사용)"""
        members = list(self.communities[community_idx])
        subgraph = self.graph.subgraph(members)

        # 엔티티 정보 수집
        entities_info = []
        for node in members[:5]:
            node_data = self.graph.nodes[node]
            entities_info.append({
                'name': node,
                'type': node_data.get('type'),
                'description': node_data.get('description')
            })

        # 관계 정보 수집
        relations_info = []
        for u, v, data in subgraph.edges(data=True):
            relations_info.append({
                'source': u,
                'target': v,
                'type': data.get('relation')
            })

        # 템플릿 기반 요약 생성
        summary = f"""이 커뮤니티는 주로 {self._get_main_types(members)} 엔티티로 구성됩니다.

주요 엔티티:
"""
        for e in entities_info:
            summary += f"- {e['name']} ({e['type']}): {e['description']}\n"

        summary += "\n핵심 관계:\n"
        for r in relations_info[:5]:
            summary += f"- {r['source']} --{r['type']}--> {r['target']}\n"

        return summary

요약 예시

text
=== Community 0 Summary ===
이 커뮤니티는 주로 조직, 인물 엔티티로 구성됩니다.

주요 엔티티:
- 알파테크 (ORGANIZATION): AI 스타트업, RAG 기술 전문
- 블루벤처스 (ORGANIZATION): 벤처캐피탈
- 김철수 (PERSON): 알파테크 CEO
- 이영희 (PERSON): 알파테크 CTO, 스탠포드 출신
- 박지민 (PERSON): 블루벤처스 파트너

핵심 관계:
- 알파테크 --PARTNERED_WITH--> 블루벤처스
- 알파테크 --EMPLOYS--> 김철수
- 알파테크 --EMPLOYS--> 이영희

GraphRAG Query Engine 구현

이제 Local 검색과 Global 검색을 모두 지원하는 쿼리 엔진을 구현합니다.

python
class GraphRAGQueryEngine:
    """
    GraphRAG 쿼리 엔진
    - Local Search: 특정 엔티티 관련 질문
    - Global Search: 전체 데이터셋 관련 질문
    """

    def __init__(self, graph, communities, summaries, node_to_community):
        self.graph = graph
        self.communities = communities
        self.summaries = summaries
        self.node_to_community = node_to_community

    def local_search(self, query: str, top_k: int = 5) -> dict:
        """
        Local Search: 특정 엔티티에 대한 질문
        """
        # 1. 쿼리에서 엔티티 찾기
        found_entities = []
        for entity_name in self.graph.entities.keys():
            if entity_name.lower() in query.lower():
                found_entities.append(entity_name)

        if not found_entities:
            return {'mode': 'local', 'context': "관련 엔티티를 찾지 못했습니다."}

        # 2. 관련 노드 수집 (1-hop, 2-hop)
        related_nodes = set()
        for entity in found_entities:
            neighbors = self.graph.get_neighbors(entity)
            related_nodes.update(neighbors)

            for neighbor in neighbors[:3]:
                second_hop = self.graph.get_neighbors(neighbor)
                related_nodes.update(second_hop[:2])

        # 3. 컨텍스트 구성
        context_parts = []
        for node in list(related_nodes)[:top_k]:
            node_info = self.graph.get_node_info(node)
            if node_info:
                context_parts.append(
                    f"- {node} ({node_info.get('type')}): {node_info.get('description')}"
                )

        return {
            'mode': 'local',
            'entities_found': found_entities,
            'context': '\n'.join(context_parts),
            'related_nodes': list(related_nodes)
        }

    def global_search(self, query: str) -> dict:
        """
        Global Search: 전체 데이터셋에 대한 질문
        """
        # 모든 커뮤니티 요약 수집
        all_summaries = []
        for idx, summary in self.summaries.items():
            all_summaries.append(f"[Community {idx}]\n{summary}")

        # 전역 컨텍스트 구성
        global_context = f"""=== 데이터셋 개요 ===
총 {len(self.communities)}개의 커뮤니티, {sum(len(c) for c in self.communities)}개의 엔티티

=== 커뮤니티별 요약 ===

"""
        global_context += '\n\n'.join(all_summaries)

        return {
            'mode': 'global',
            'context': global_context
        }

    def search(self, query: str, mode: str = 'auto') -> dict:
        """통합 검색 인터페이스"""
        if mode == 'local':
            return self.local_search(query)
        elif mode == 'global':
            return self.global_search(query)
        else:
            # 자동 모드 결정
            global_keywords = ['전체', '요약', '주요', '트렌드', '테마', '개요']
            is_global = any(kw in query.lower() for kw in global_keywords)

            if is_global:
                return self.global_search(query)
            else:
                return self.local_search(query)

테스트 결과

text
Query: 알파테크는 어떤 회사와 협력하고 있나요?
Mode: local
Found Entities: ['알파테크']

Context:
- 삼성전자 (ORGANIZATION): 대기업, 반도체/전자
- LG전자 (ORGANIZATION): 대기업, 전자/가전
- 블루벤처스 (ORGANIZATION): 벤처캐피탈
- 알파테크 --PARTNERED_WITH--> 삼성전자
- 알파테크 --PARTNERED_WITH--> LG전자
- 알파테크 --PARTNERED_WITH--> 블루벤처스
text
Query: 이 데이터셋의 주요 테마와 트렌드는 무엇인가요?
Mode: global

Context:
=== 데이터셋 개요 ===
총 5개의 커뮤니티, 39개의 엔티티

=== 커뮤니티별 요약 ===

[Community 0]
AI 스타트업 및 투자 생태계...

[Community 1]
자율주행 및 AI 하드웨어...

Microsoft GraphRAG 공식 라이브러리 사용법

위에서 핵심 로직을 직접 구현해봤습니다. 이제 MS 공식 라이브러리를 사용하는 방법을 알아봅시다.

CLI 사용법

bash
# 1. 프로젝트 디렉토리 생성
mkdir -p ./my_graphrag/input

# 2. 입력 문서 저장 (.txt 파일들)
cp my_documents/*.txt ./my_graphrag/input/

# 3. 초기화
graphrag init --root ./my_graphrag

# 4. API 키 설정 (.env 파일)
echo "GRAPHRAG_API_KEY=your-openai-api-key" > ./my_graphrag/.env

# 5. 인덱싱 실행 (시간 소요)
graphrag index --root ./my_graphrag

# 6. Global 검색
graphrag query --root ./my_graphrag --method global \
  --query "이 문서들의 주요 테마는?"

# 7. Local 검색
graphrag query --root ./my_graphrag --method local \
  --query "알파테크에 대해 알려줘"

Python API 사용법

python
import asyncio
from graphrag.query.indexer_adapters import (
    read_indexer_entities,
    read_indexer_relationships,
    read_indexer_reports,
    read_indexer_text_units,
)
from graphrag.query.llm.oai.chat_openai import ChatOpenAI
from graphrag.query.llm.oai.typing import OpenaiApiType
from graphrag.query.structured_search.global_search.community_context import GlobalCommunityContext
from graphrag.query.structured_search.global_search.search import GlobalSearch

# LLM 설정
llm = ChatOpenAI(
    api_key="your-api-key",
    model="gpt-4o-mini",
    api_type=OpenaiApiType.OpenAI,
)

# 인덱스 데이터 로드
INPUT_DIR = "./my_graphrag/output/artifacts"

entities = read_indexer_entities(INPUT_DIR)
relationships = read_indexer_relationships(INPUT_DIR)
reports = read_indexer_reports(INPUT_DIR)
text_units = read_indexer_text_units(INPUT_DIR)

# Global Search 설정
context_builder = GlobalCommunityContext(
    community_reports=reports,
    entities=entities,
    token_encoder=token_encoder,
)

global_search = GlobalSearch(
    llm=llm,
    context_builder=context_builder,
    token_encoder=token_encoder,
)

# 쿼리 실행
result = await global_search.asearch("이 데이터셋의 주요 테마는?")
print(result.response)

기존 RAG vs GraphRAG: 실제 비교

같은 질문에 대한 두 시스템의 응답을 비교해봅시다.

질문: "이 문서들의 주요 테마와 핵심 인물들은 누구인가요?"

기존 RAG 방식:

text
청크 1: AI 스타트업 알파테크가 VC인 블루벤처스로부터 500억원 규모...
청크 2: 삼성전자가 차세대 AI 반도체 'Exynos AI'를 발표했다...
청크 3: 현대자동차가 자율주행 레벨4 기술을 달성했다고 발표했다...

→ 문제: 개별 청크만 보여주고 "전체 테마"에 대한 답변 불가

GraphRAG 방식:

text
=== 데이터셋 개요 ===
총 5개의 커뮤니티, 39개의 엔티티

핵심 테마:
1. AI 스타트업 생태계 (알파테크, 블루벤처스, 김철수, 이영희)
2. 자율주행/반도체 (삼성전자, 현대자동차, NVIDIA)
3. 스마트홈 AI (LG전자, OpenAI, 한미래)
4. RAG/검색 기술 (RAG, Knowledge Graph, Vector Store)
5. LLM 최적화 (LLM, 양자화, TensorRT)

→ 해결: 커뮤니티 요약을 통해 "숲"을 볼 수 있음

비용과 성능 트레이드오프

GraphRAG는 강력하지만 비용이 있습니다.

인덱싱 비용

항목기존 RAGGraphRAG
임베딩문서당 1회문서당 1회
LLM 호출없음엔티티 추출 + 요약 생성
추정 비용문서당 ~$0.001문서당 ~$0.1-1.0

쿼리 비용

항목기존 RAGGraphRAG LocalGraphRAG Global
검색벡터 검색그래프 탐색전체 요약 로드
LLM 입력~2000 토큰~3000 토큰~10000+ 토큰

언제 GraphRAG를 사용해야 할까?

상황추천
특정 팩트 검색기존 RAG
전체 요약/트렌드GraphRAG 필수
비용 민감기존 RAG
관계 추론 필요GraphRAG
실시간 응답 필요기존 RAG

프로덕션 적용 가이드

1. 점진적 도입

모든 문서에 GraphRAG를 적용하지 마세요. 먼저:

  1. 가장 중요한 문서 집합 식별
  2. 작은 파일럿으로 시작 (100-1000개 문서)
  3. 비용과 품질 측정
  4. 점진적 확대

2. 프롬프트 튜닝

기본 프롬프트로는 충분하지 않습니다:

bash
graphrag prompt-tune --root ./my_graphrag \
  --config ./settings.yaml \
  --no-entity-types

도메인에 맞는 엔티티 유형과 관계 유형을 정의하세요.

3. 하이브리드 접근

실제 프로덕션에서는 하이브리드가 최선입니다:

python
def hybrid_search(query: str):
    # 1. 질문 유형 분류
    if is_global_question(query):
        return graphrag.global_search(query)
    elif contains_entity(query):
        return graphrag.local_search(query)
    else:
        return traditional_rag.search(query)

4. 캐싱 전략

커뮤니티 요약은 자주 변하지 않습니다. 캐싱으로 비용 절감:

python
# 커뮤니티 요약 캐시 (Redis 등)
community_summaries = cache.get("community_summaries")
if not community_summaries:
    community_summaries = generate_all_summaries()
    cache.set("community_summaries", community_summaries, ttl=3600)

온톨로지 KG vs GraphRAG: 언제 무엇을?

이전 글에서 다룬 온톨로지 기반 Knowledge Graph와 GraphRAG는 다른 문제를 해결합니다.

특성온톨로지 KGGraphRAG
그래프 구조사전 정의된 스키마자동 추출
쿼리 방식SPARQL/Cypher자연어
관계 정확도높음 (명시적)중간 (추론)
구축 비용높음 (수동)낮음 (자동)
유지보수어려움쉬움
적합한 도메인의료, 법률, 금융일반 문서

추천 조합

  1. 정형 지식 + 비정형 문서: 온톨로지 KG + GraphRAG 병행
  2. 빠른 프로토타이핑: GraphRAG 먼저
  3. 높은 정확도 필요: 온톨로지 KG 필수

Summary

핵심 개념

  1. 문제: 기존 RAG는 "숲"을 보지 못함
  2. 해결: 커뮤니티 기반 계층적 요약
  3. Local Search: 특정 엔티티 → 이웃 탐색
  4. Global Search: 모든 커뮤니티 요약 → 통합 답변

구현 단계

  1. Entity Extraction (엔티티 추출)
  2. Graph Construction (그래프 구축)
  3. Community Detection (Leiden/Louvain)
  4. Hierarchical Summarization (계층적 요약)
  5. Query Engine (Local/Global 검색)

다음 단계

  • Multi-hop QA: 다중 홉 추론 RAG 시스템
  • Temporal KG: 시간 축을 가진 Knowledge Graph
  • 자동 KG 구축: LLM 기반 트리플 자동 추출

References