콘텐츠로 이동

Microsoft GraphRAG 해부: 인덱싱 8단계와 검색 두 갈래를 코드에서 읽어내기

  • 출처: How GraphRAG Works Step-by-Step
  • 저자: Mariana Avelino
  • 발행: 2025-05-06, Towards AI (Medium)
  • 원논문: From Local to Global: A Graph RAG Approach to Query-Focused Summarization (Edge et al., 2024, arXiv:2404.16130)
  • 실험 대상: Pablo Rivero의 소설 Penitencia 한 권을 기본 설정으로 인덱싱
  • 읽은 날짜: 2026-09-08
  • 태그: #GraphRAG #Microsoft #KnowledgeGraph #Leiden #LocalSearch #GlobalSearch #RAG

한 줄 요약

Microsoft GraphRAG의 인덱싱 8단계와 Local/Global Search를 실제 코드와 중간 산출 테이블을 열어가며 따라간 글. 원논문이 아예 언급하지 않은 Local Search의 우선순위 규칙과 토큰 예산 배분까지 들어 있어, 문서만 읽어서는 안 보이던 기본값들이 드러난다.

이 글의 값어치

원논문은 Global Search 중심으로 쓰였고 Local Search는 다루지 않는다. 공식 문서도 파라미터가 어떤 순서로 어떤 것을 자르는지까지는 말해주지 않는다. 저자는 그 빈칸을 코드를 읽어 메웠다. 특히 어떤 후보를 어떤 기준으로 정렬해 컨텍스트에 넣고, 예산이 모자라면 무엇부터 버리는지가 이 글의 핵심이다. 튜닝할 때 실제로 손대야 하는 지점이 거기다.

GraphRAG는 크게 둘로 나뉜다.

Graph Creation (인덱싱)      graphrag index --root ./ragtest
  └ 개체 추출 -> 그래프 확정 -> 커뮤니티 분할 -> 리포트 생성 -> 임베딩

Querying                    graphrag query --method {local|global|drift}
  ├ Local Search   구체적인 질문
  ├ Global Search  전체를 아우르는 질문
  └ Drift Search   (이 글에서 다루지 않음)

그래프 생성 8단계

workflows 디렉터리의 모듈 이름 그대로 따라간다.

# 모듈 하는 일
1 create_base_text_units 문서를 N 토큰 단위 청크로 자른다 (예제는 1,200 토큰)
2 create_final_documents 문서와 청크를 잇는 조회 테이블 생성
3 extract_graph 청크마다 LLM으로 개체와 관계 추출, 이후 중복 병합과 설명 재작성
4 finalize_graph NetworkX로 노드·엣지 구성, degree 등 구조 정보 부여
5 create_communities Leiden 알고리즘으로 계층적 커뮤니티 분할
6 create_final_text_units 청크마다 개체·관계·covariate ID를 매핑
7 create_community_reports 커뮤니티별 리포트 생성, 컨텍스트 초과 시 축약
8 generate_embeddings 청크·개체 설명·리포트 전문을 임베딩

3단계: 중복은 병합되지만 동일 인물은 합쳐지지 않는다

청크마다 독립적으로 추출하므로 같은 개체가 반복해서 나온다. 예제에서 주인공 Jon은 82개 청크에서 언급돼 82번 추출됐다. 개체 테이블의 frequency가 82이고 text_unit_ids와 description에는 항목이 82개씩 담긴다.

병합은 두 기준으로만 한다. 개체는 제목과 타입이 같으면 묶고, 관계는 출발 노드와 도착 노드가 같으면 묶는다. 그다음 LLM이 짧은 설명 여러 개를 읽고 통합 설명을 새로 쓴다.

여기서 저자가 짚는 한계가 실무에 직결된다. GraphRAG는 개체 중의성 해소를 하지 않는다. Jon과 Jon Márquez는 같은 사람을 가리켜도 별개 노드로 남는다. 제목 문자열이 다르면 다른 개체다.

기본 개체 타입은 네 가지다: Geo, Person, Event, Organization.

관계의 weight도 오해하기 쉽다. 연결 횟수가 아니라 LLM이 매긴 관계 강도의 합이다.

5단계: 커뮤니티의 level은 숫자가 클수록 구체적이다

Leiden은 계층적 군집화라 서로 다른 구체성의 커뮤니티가 함께 나온다. level 0이 루트이고 가장 포괄적이며, level이 올라갈수록 좁고 구체적이다. 예제 책에서는 루트 커뮤니티가 15개 나왔다.

커뮤니티마다 LLM이 1에서 10 사이의 rank(중요도)와 그 판단 근거를 붙인다.

7단계: 컨텍스트가 넘칠 때 두 단계로 줄인다

커뮤니티가 크면 개체·관계·주장을 다 넣은 컨텍스트가 max_input_length를 넘는다. 이때 순서가 정해져 있다.

계층 치환(Hierarchical Substitution)이 먼저다. 하위 커뮤니티의 원본 텍스트를 그 하위 커뮤니티의 리포트로 바꿔치기한다. 이때 큰 하위 커뮤니티부터 적용한다. 토큰이 가장 많이 줄기 때문이다.

커뮤니티 C (level 0)
  ├ S1 (level 1, 큼)   -> C 안의 S1 소속 개체·관계를 S1 리포트로 대체
  └ S2 (level 1, 작음) -> 그래도 넘치면 S2도 대체

그래도 넘치면 잘라낸다(Trimming). 개체는 node degree, 관계는 combined_degree 기준으로 정렬해 낮은 것부터 버린다. 하위 커뮤니티가 애초에 없던 커뮤니티도 이 경로로 간다.

리포트는 발견 사항 5~10개와 요약을 합쳐 만든다.

6단계에서 놓치기 쉬운 것: covariate는 기본으로 꺼져 있다

covariate는 사실상 주장(claim)이다. 예제로 든 것이 "Celia가 남편과 아이를 살해했다(의심됨)" 같은 문장이다. LLM이 청크에서 추론해 뽑는다.

기본 설정에서는 추출하지 않는다. 주장 단위 근거를 기대하고 GraphRAG를 붙였다면 설정부터 확인해야 한다.

graphrag query --root ./ragtest --method local \
  --query "What kind of retribution is Laura seeking, and why?"

1단계: 개체 임베딩과의 유사도로 진입점을 고른다

질의를 임베딩해 개체 설명 임베딩과의 유사도를 잰다. 청크가 아니라 개체가 진입점이라는 점이 일반 RAG와 다르다. 상위 N개를 가져오며 N은 top_k_mapped_entities다.

여기서 저자가 "이상하다"고 표현한 구현 세부가 나온다. GraphRAG는 2배로 과표집한다. top_k_mapped_entities가 10이면 20개를 가져온다. 검색된 개체의 ID가 유효하지 않은 경우가 있어 개수를 확보하려는 장치다. 예제에서는 20개 중 17개만 유효해 17개가 실제로 쓰였다.

2단계: 진입 개체에서 후보를 넓힌다

후보 정의
후보 개체 검색된 개체 전부
후보 커뮤니티 검색된 개체를 하나 이상 포함하는 모든 커뮤니티
후보 관계 검색된 개체가 출발 또는 도착 노드인 모든 엣지
후보 청크 검색된 개체가 하나 이상 등장하는 모든 청크

3단계: 토큰 예산을 나누고 각각 다른 기준으로 정렬한다

이 절이 이 글의 핵심이다. 기본값 기준 예산 배분은 이렇다.

항목 설정 키 비율 max_tokens=12000 기준
청크 text_unit_prop 0.5 6,000 토큰
커뮤니티 리포트 community_prop 0.1 1,200 토큰
개체 + 관계 설명 (나머지) 0.4 4,800 토큰

정렬 기준이 항목마다 다르다.

  • 커뮤니티: matches 기준으로 정렬한다. matches는 그 커뮤니티의 검색된 개체들이 등장하는 서로 다른 청크의 수다. 동점이면 LLM이 매긴 rank로 가른다. 리포트는 잘라 넣지 않는다. 통째로 들어가거나 아예 빠진다.
  • 개체: 정렬하지 않는다. 질의와의 의미 유사도 순서를 그대로 유지한 채 예산이 허락하는 만큼 넣는다.
  • 관계: in-network와 out-network로 갈라 다르게 다룬다. 양쪽 끝이 모두 검색된 개체면 in-network, 한쪽만이면 out-network다. in-network를 먼저 넣고 combined_degree로 정렬한다. out-network는 바깥 개체가 안쪽 개체들과 몇 개나 연결돼 있는지를 1순위로, combined_degree를 2순위로 정렬한다. 예산이 찰 때까지 반복해서 채운다.
  • 청크: 검색된 개체의 순서를 1순위로 정렬하고, 그 청크에 걸린 검색 개체 관계의 수를 2순위로 쓴다. 질의와 가장 유사한 개체가 나온 청크가 위로 올라간다는 뜻이다.

4단계: 순서까지 정해져 있다

최종 컨텍스트는 커뮤니티 리포트, 개체, 관계, 청크 순으로 이어 붙여 LLM에 넘긴다.

graphrag query --root ./ragtest --method global \
  --query "What themes are explored in the book?"

구조가 map-reduce다. 그래프를 타고 들어가는 것이 아니라 커뮤니티 리포트를 나눠 읽고 합친다.

  1. 커뮤니티마다 occurence_weight를 계산한다. 그 커뮤니티에 속한 개체들이 등장하는 서로 다른 청크 수를 정규화한 값으로, 문서 전체에서 그 커뮤니티가 얼마나 퍼져 있는지를 나타낸다.
  2. 전체 커뮤니티를 섞은 뒤 배치로 나눈다. 섞는 이유가 명확하다. 관련성 높은 커뮤니티가 한 배치에 몰리면 편향이 생기기 때문이다. 배치 안에서는 community_weight로 정렬한다.
  3. 배치마다 LLM이 답변을 여러 개(보통 5개) 만들고, 각 답변에 질문에 얼마나 도움이 되는지 점수를 스스로 매긴다. 전체 답변을 점수로 정렬하고 0점은 버린다.
  4. 남은 답변 텍스트를 이어 붙여 다시 LLM에 넣고 최종 답변을 만든다.

이 글에서 건진 기본값과 함정

항목 사실 왜 중요한가
개체 중의성 해소 하지 않는다 같은 인물이 표기만 달라도 별개 노드로 갈라진다
covariate(주장) 기본 비활성 주장 단위 근거를 기대했다면 설정을 켜야 한다
관계 weight LLM이 매긴 강도의 합 연결 횟수로 오해하면 해석이 뒤집힌다
커뮤니티 level 0이 루트이자 가장 포괄적 숫자가 클수록 구체적이라 방향을 반대로 잡기 쉽다
Local Search 과표집 top_k_mapped_entities의 2배 유효하지 않은 ID를 감안한 보정이다
커뮤니티 리포트 부분 삽입 없음 통째로 넣거나 뺀다. 예산이 애매하면 통째로 빠진다
컨텍스트 축약 순서 계층 치환 다음 잘라내기 degree 낮은 개체·관계부터 사라진다

읽을 때 감안할 것

  • 글이 2025년 5월 기준이다. GraphRAG는 변화가 잦은 프로젝트라 모듈 이름, 설정 키, 기본값이 지금과 다를 수 있다. 실제로 쓸 때는 현재 버전 코드와 대조해야 한다. 저자도 "2024년 초부터 써왔고 그사이 공식 문서가 많이 좋아졌다"고 적었다.
  • 본문에 오기가 하나 있다. 청크 예산을 설명하는 문단에서 "max_tokens=12000이고 text_unit_prop=0.5이므로 커뮤니티 리포트가 최대 6,000 토큰을 차지한다"고 썼는데, 앞서 자신이 밝힌 대로 6,000은 청크 몫이고 커뮤니티 리포트는 community_prop=0.1에 따라 1,200이다. 문맥상 청크를 쓰려다 잘못 쓴 것으로 보인다.
  • Drift Search는 다루지 않는다. 검색 방식이 셋이라고 소개해놓고 둘만 설명한다.
  • 비용과 성능 수치가 없다. 인덱싱에 LLM 호출이 몇 번 들어가는지, 얼마가 드는지, 검색 품질이 일반 RAG 대비 어떤지는 이 글의 범위 밖이다. 구조 설명서이지 도입 판단 자료가 아니다.
  • 표본이 소설 한 권이다. 등장인물 중심 서사라 개체와 관계가 선명하게 잡히는 텍스트다. 기술 문서나 로그처럼 개체 경계가 흐린 코퍼스에서 같은 그림이 나온다는 보장은 없다.

가져갈 지점

  1. LogicRAG, ROGRAG를 읽을 때의 기준선이 된다

LogicRAG 노트는 "이 사전 그래프 구축을 아예 하지 말자"는 주장이고, ROGRAG 노트는 "그래프는 만들되 검색을 여러 겹으로 쌓자"는 주장이다. 두 논문이 무엇을 대체하거나 개선하려는 것인지 이 글을 읽고 나면 구체적으로 잡힌다. 특히 LogicRAG가 지적한 구축 비용의 실체가 여기 8단계에 그대로 있다.

  1. ROGRAG의 dual-level 검색과 대비해서 볼 것

Local Search가 개체 임베딩으로 진입점을 잡고 거기서 관계와 청크로 넓히는 구조는, ROGRAG의 dual-level 검색이 low-level 키워드로 노드를, high-level 키워드로 엣지를 잡는 방식과 같은 계열이다. ROGRAG가 여기에 logic form 단계를 앞에 덧댄 것이라고 보면 위치가 분명해진다.

  1. 토큰 예산 배분이 실제 튜닝 지점이다

text_unit_prop, community_prop, max_tokens 세 값이 컨텍스트의 성격을 결정한다. 원문 청크를 더 볼지 커뮤니티 요약을 더 볼지의 문제이고, 질문 유형에 따라 답이 다르다. 검색 품질이 안 나올 때 프롬프트보다 먼저 볼 곳이다.

  1. 개체 중의성 해소는 직접 붙여야 한다

표기가 다른 같은 개체가 갈라지는 문제는 한국어 코퍼스에서 더 심하다. 조사가 붙거나 직함이 달라지면 별개 노드가 된다. GraphRAG를 그대로 쓰면 이 부분은 비어 있으므로 정규화 단계를 앞에 두거나 후처리로 병합해야 한다.

결론

GraphRAG를 "지식 그래프를 만들어서 검색한다"로만 알고 있었다면, 이 글은 그 문장 안에 든 결정들을 하나씩 펼쳐 보여준다. 어디서 자르고, 무엇을 먼저 넣고, 넘치면 무엇부터 버리는지가 전부 정해져 있고 그 기본값이 결과를 좌우한다.

값어치는 Local Search 절에 몰려 있다. 원논문에 없고 문서에도 정리돼 있지 않은 우선순위 규칙이라, 검색 결과가 기대와 다를 때 원인을 짚을 근거가 된다.

다만 구조 설명서라는 점은 분명히 하고 읽어야 한다. 도입 여부를 정하려면 비용과 품질 수치가 따로 필요하고, 그건 이 글에 없다.