앞 장에서 우리는 RAG가 무엇인지, LLM과 함께 RAG 흐름을 구현하는 간단한 예제 몇 가지를 보았다. 이 장에서는 지식그래프가 무엇이고, 그래프가 어떻게 검색 증강 생성(RAG)을 더 효과적으로 만드는지를 본다. 지식그래프를 모델링하는 방법과 그 목적에 Neo4j를 쓰는 법을 탐구하고, Neo4j의 데이터 지속 접근을 활용한 데이터 모델링이 더 강력한 지식그래프 구축에 어떻게 기여하는지 살핀다. 관계형 데이터베이스 관리 시스템(Relational Database Management System, RDBMS)에서 Neo4j 지식그래프에 이르는 데이터 저장소의 지속 접근들을 견주며, 다양한 데이터 모델을 통해 데이터를 더 깊이 이해하게 될 것이다.
RAG 모델과 Neo4j의 강건한 그래프 데이터베이스 능력의 융합이, 구조화된 지식 베이스를 활용해 성능과 결과를 끌어올리는 지능형 애플리케이션의 창조를 어떻게 가능케 하는지, 그 흥미로운 여정을 시작한다.
기술 요건
Neo4j와의 RAG 통합을 위한 지식그래프 구축의 실무로 뛰어들기 전에, 필요한 도구와 환경부터 갖추어야 한다.
- Neo4j 데이터베이스로컬 설치라면 Neo4j Desktop(다운로드 센터), 클라우드라면 Neo4j Aura를 쓴다. Neo4j는 두 가지 주요 클라우드 서비스를 제공한다. AuraDB는 지능형 애플리케이션을 만드는 개발자를 위한 완전 관리형 그래프 데이터베이스 서비스로, 유연한 스키마, 관계의 네이티브 저장, Cypher 언어의 효율적 질의를 지원하며 무료 티어가 있다. AuraDS는 데이터 과학 애플리케이션 구축에 쓸 수 있는 완전 관리형 Neo4j Graph Data Science 인스턴스다.
- DB Browser for SQLiteSQLite 데이터베이스를 쉽게 질의하는 도구다(sqlitebrowser.org).
- Cypher 질의 언어이 장을 시작하기 전에 Neo4j의 질의 언어인 Cypher에 익숙해져야 한다. 낯설다면 GraphAcademy의 훌륭한 튜토리얼과 기초 과정으로 시작할 수 있고, Graph Data Processing with Cypher라는 책으로 상세히 배울 수도 있다.
- Python 환경Python 3.8 이상을 권장한다.
- Neo4j Python 드라이버Python에서 Neo4j 데이터베이스와 상호작용하게 해 준다:
pip install neo4j-driver - GitHub 저장소이 장의 모든 코드와 리소스는 책의 저장소
ch3폴더에 있다.
(:Section {n: 1})-[:REMODELS]->(:Data)
그래프 데이터 모델링의 중요성
GraphRAG 흐름이 Neo4j와 어떻게 작동하는지 보기 전에, 한 걸음 물러나 지식그래프를 모델링하는 방법부터 이해하자. 간단한 데이터를 놓고 RDBMS와 그래프에서 각각 어떻게 모델링하는지, 그리고 데이터를 바라보는 방식에 따라 모델링이 어떻게 달라지는지를 볼 것이다.
그래프는 우리에게 다르게 생각하기를, 풀려는 문제에 따라 데이터를 다른 관점에서 보기를 강요한다. 문제처럼 보일지 모르지만, 실은 수많은 문을 여는 일이다. 오랫동안 우리는 RDBMS 저장 접근을 개체-관계(Entity-Relationship, ER) 다이어그램의 틀로 생각하도록 배워 왔다. 기술에 한계가 있고 저장 비용이 매우 비쌌던 시절, 이 접근은 데이터를 표현하고 지속시키기에 좋았다. 그러나 기술이 진화하고 하드웨어가 저렴해지면서 새로운 길이 열렸고, 데이터를 모델링하는 새로운 접근이 가능해졌다. 그래프는 이 이점을 취하기에 알맞은 자리에 서 있다.
데이터 모델링의 새 길을 생각하려면, ER 다이어그램으로 데이터를 표현하던 익숙한 방식 몇 가지를 탈학습(unlearn)해야 할지도 모른다. 말은 간단하지만 실제로는 조금 어려운 일이다. 이 학습과 탈학습의 과정은 신경가소성 연구의 프리즘 고글 실험과 닮았다.
실험 참가자는 시야를 굴절시키는 프리즘 고글을 쓰고 단순한 과제를 수행한다. 어긋난 시야에 마음이 적응해 과제를 제대로 해내기까지는 시간이 걸린다. 고글을 벗으면, 같은 과제를 다시 해내는 데 또 시간이 걸린다. 데이터 모델링도 마찬가지다. 더 나은 그래프 데이터 모델을 만들려면, 기대어 온 접근 몇 가지를 먼저 탈학습해야 할 수 있다. 실험의 자세한 내용은 미네소타대학교의 자료에서 읽을 수 있다.
좋은 그래프 데이터 모델을 만드는 데 도움이 될 다른 접근이 있는지, 실생활에서 우리가 데이터를 소비하는 방식을 들여다보자.
도서관이나 서점을 떠올려 보자. 책은 분류와 저자의 성(姓)에 따라 배열된다. 우리가 인덱스로 데이터를 찾는 방식과 닮았다. 그런데 도서관 입구에는 신간과 인기 도서를 도드라지게 놓은 별도의 구역이 있다. 사람들이 그 책들을 빨리 찾게 하려는 배치다. 이런 측면을 RDBMS로 모델링하기는 어렵다. 그러나 Neo4j의 그래프 데이터베이스 접근은 다중 레이블을 활용해 이를 아주 쉽게 해낸다.
— 정보 소비가 배치를 결정한다, Chapter 3, p.40그래프 데이터베이스는 데이터의 쉽고 효율적인 소비를 돕는 데이터 모델을 만들게 해 준다. 그래프와 함께라면 사고의 과정을 바꾸고 몇 가지 다른 모델링 접근을 시도해야 할 수도 있다. 첫 접근이 완전히 옳지 않을 수 있지만, 우리에게 통하는 수용 가능한 모델에 이를 때까지 데이터 모델을 계속 조정하면 된다. RDBMS를 비롯한 다른 기술에서는 데이터 모델이 경직되어 있어, 제대로 잡지 못하면 파장이 크다. 바로 여기서 Neo4j가 빛난다. 선택적이고 유연한 스키마 접근 덕분에, 처음에는 최적이 아닐 수 있는 데이터 모델로 시작하더라도 백지에서 다시 시작할 필요 없이 점진적으로 조율할 수 있다.
이제 작고 단순한 데이터를 놓고 RDBMS와 그래프의 데이터 모델링을 견주어 보자. 모델링할 데이터는 이렇다. 한 사람(firstName, lastName)과, 그 사람이 살았던 다섯 곳의 렌탈(주소 1행, 도시, 주, 우편번호, 입주 시점, 퇴거 시점). 단순해 보여도 이 데이터가 RDBMS와 그래프에서 어떻게 표현되는지의 미묘한 결을 이해하기에는 충분하다. 이 데이터로 답하려는 질문은 셋이다.
John Doe라는 사람이 지금 살고 있는 최신 주소는 어디인가?
John Doe가 살았던 첫 번째 주소는 어디인가?
John Doe가 살았던 세 번째 주소는 어디인가?
RDBMS 데이터 모델링
먼저 이 데이터가 RDBMS에서 어떻게 모델링되는지 본다. 다음 그림이 데이터 모델을 ER 다이어그램으로 나타낸 것이다.
이 데이터 모델에는 테이블이 셋이다. Person 테이블은 인물 상세를, Address 테이블은 주소 상세를 담는다. Person_Address 테이블은 렌탈 상세와 함께 두 테이블에 대한 참조를 담는다. Person이나 Address 개체의 데이터 중복을 피하려고 이 조인 테이블로 렌탈 상세를 표현하는 것이다. 이런 데이터 모델을 만들 때는 세부를 각별히 확신할 수 있어야 한다. 변경의 규모에 따라 모델을 고치는 일이 꽤 많은 시간을 잡아먹을 수 있고, 테이블 하나를 여러 테이블로 쪼개는 경우라면 데이터 마이그레이션이 만만치 않은 작업이 되기 때문이다. SQLite 데이터베이스는 이 튜토리얼로 만들 수 있다. 그 데이터베이스에 데이터를 적재하고, 앞서 정의한 질문들에 답하는 질의를 검증한다. 다음 SQL 스크립트가 테이블을 생성한다.
-- Person 테이블 정의 CREATE TABLE IF NOT EXISTS person ( id INTEGER PRIMARY KEY, name varchar(100) NOT NULL, gender varchar(20), UNIQUE(id) ); -- Address 테이블 정의 CREATE TABLE IF NOT EXISTS address ( id INTEGER PRIMARY KEY, line1 varchar(100) NOT NULL, city varchar(20) NOT NULL, state varchar(20) NOT NULL, zip varchar(20) NOT NULL, UNIQUE(id) ); -- Person_Address 테이블 정의 CREATE TABLE IF NOT EXISTS person_address ( person_id INTEGER NOT NULL, address_id INTEGER NOT NULL, start varchar(20) NOT NULL, end varchar(20), FOREIGN KEY (person_id) REFERENCES person (id) ON DELETE CASCADE ON UPDATE NO ACTION, FOREIGN KEY (address_id) REFERENCES address (id) ON DELETE CASCADE ON UPDATE NO ACTION );
-- 인물 레코드 INSERT INTO person (id, name, gender) VALUES (1, 'John Doe', 'Male'); -- 주소 레코드 INSERT INTO address VALUES (1, '1 first ln', 'Edison', 'NJ', '11111'); INSERT INTO address VALUES (2, '13 second ln','Edison', 'NJ', '11111'); INSERT INTO address VALUES (3, '13 third ln', 'Edison', 'NJ', '11111'); INSERT INTO address VALUES (4, '1 fourth ln', 'Edison', 'NJ', '11111'); INSERT INTO address VALUES (5, '5 other ln', 'Edison', 'NJ', '11111'); -- 렌탈 레코드 INSERT INTO person_address VALUES (1,1,'2001-01-01','2003-12-31'); INSERT INTO person_address VALUES (1,2,'2004-01-01','2008-12-31'); INSERT INTO person_address VALUES (1,3,'2009-01-01','2015-12-31'); INSERT INTO person_address VALUES (1,4,'2016-01-01','2020-12-31'); INSERT INTO person_address VALUES (1,5,'2021-01-01', null);
| id | name | gender |
|---|---|---|
| 1 | John Doe | Male |
| p_id | a_id | start | end |
|---|---|---|---|
| 1 | 1 | 2001-01-01 | 2003-12-31 |
| 1 | 2 | 2004-01-01 | 2008-12-31 |
| 1 | 3 | 2009-01-01 | 2015-12-31 |
| 1 | 4 | 2016-01-01 | 2020-12-31 |
| 1 | 5 | 2021-01-01 | null |
| id | line1 | city | state | zip |
|---|---|---|---|---|
| 1 | 1 first ln | Edison | NJ | 11111 |
| 2 | 13 second ln | Edison | NJ | 11111 |
| 3 | 13 third ln | Edison | NJ | 11111 |
| 4 | 1 fourth ln | Edison | NJ | 11111 |
| 5 | 5 other ln | Edison | NJ | 11111 |
그림 3.3 — RDBMS에 저장된 데이터
이제 RDBMS에서 데이터를 질의하는 방법을 본다.
SELECT line1, city, state, zip FROM person p, person_address pa, address a WHERE p.name = 'John Doe' AND pa.person_id = p.id AND pa.address_id = a.id AND pa.end IS NULL
이 질의는 end 컬럼 값이 null이라는 사실에 기대어 어느 것이 최신 주소인지 판별한다. 무엇이 마지막 주소인지 결정하는 논리가 SQL 질의 안에 들어 있는 것이다.
SELECT line1, city, state, zip FROM person p, person_address pa, address a WHERE p.name = 'John Doe' AND pa.person_id = p.id AND pa.address_id = a.id ORDER BY pa.start ASC LIMIT 1
SELECT line1, city, state, zip FROM person p, person_address pa, address a WHERE p.name = 'John Doe' AND pa.person_id = p.id AND pa.address_id = a.id ORDER BY pa.start ASC LIMIT 2, 1
두 질의 모두 원하는 데이터에 이르기 위해 검색-정렬-필터(search-sort-filter) 패턴에 의존한다. 논리는 여전히 SQL 질의 안에 있다. 이제 같은 데이터를 그래프로 모델링하는 방법을 본다.
그래프 데이터 모델링 — 기본 접근
설명을 위해 이 데이터를 그래프로 모델링하는 가장 흔하고 단순한 방식을 쓴다.
이 모델은 우리가 정보를 영어로 표현하는 방식과 그대로 정렬된다. Person lives at Address. 이 문장에서 명사는 노드로, 동사는 관계로 표현된다. 이 데이터 모델 접근은 꽤 단순하고 RDBMS 데이터 모델의 ER 다이어그램과 거의 닮았다. 유일한 차이는 렌탈을 나타내던 조인 테이블이 관계로 모델링된다는 점이다. 이런 데이터 지속의 이점은 인덱스 조회 비용의 절감이다. RDBMS에서 데이터 검색의 가장 큰 비용은 조인 테이블의 인덱스 조회 비용이고, 데이터 크기가 커질수록 그 비용은 계속 불어난다. 이 접근으로 그 비용을 줄일 수 있다.
Neo4j Desktop을 쓴다면 이 튜토리얼로 데이터베이스를 만들 수 있다. 클라우드에 만들고 싶다면 Aura 시작 가이드를 따르면 되고, 무료 옵션도 있다. Neo4j Desktop을 로컬에 설치하고 싶지 않은 사람에게 최적의 선택이다. Neo4j Aura는 완전 관리형 그래프 데이터베이스 서비스(graph-database-as-a-service)다.
다음 Cypher 스크립트가 더 빠른 데이터 적재와 검색을 위한 인덱스를 설정한다. 일종의 스키마라고 생각해도 좋다.
CREATE CONSTRAINT person_id_idx FOR (n:Person) REQUIRE n.id IS UNIQUE; CREATE CONSTRAINT address_id_idx FOR (n:Address) REQUIRE n.id IS UNIQUE; CREATE INDEX person_name_idx FOR (n:Person) ON n.name;
이 스크립트는 Person과 Address 노드의 중복을 막는 고유 제약 둘을 만들고, 이름으로 인물을 빨리 찾기 위한 인덱스 하나를 더한다. 스키마가 갖춰지면 이 Cypher로 데이터를 적재한다.
CREATE (p:Person {id:1, name:'John Doe', gender:'Male'}) CREATE (a1:Address {id:1, line1:'1 first ln', city:'Edison', state:'NJ', zip:'11111'}) CREATE (a2:Address {id:2, line1:'13 second ln', city:'Edison', state:'NJ', zip:'11111'}) CREATE (a3:Address {id:3, line1:'13 third ln', city:'Edison', state:'NJ', zip:'11111'}) CREATE (a4:Address {id:4, line1:'1 fourth ln', city:'Edison', state:'NJ', zip:'11111'}) CREATE (a5:Address {id:5, line1:'5 other ln', city:'Edison', state:'NJ', zip:'11111'}) CREATE (p)-[:HAS_ADDRESS {start:'2001-01-01', end:'2003-12-31'}]->(a1) CREATE (p)-[:HAS_ADDRESS {start:'2004-01-01', end:'2008-12-31'}]->(a2) CREATE (p)-[:HAS_ADDRESS {start:'2009-01-01', end:'2015-12-31'}]->(a3) CREATE (p)-[:HAS_ADDRESS {start:'2016-01-01', end:'2020-12-31'}]->(a4) CREATE (p)-[:HAS_ADDRESS {start:'2021-01-01'}]->(a5)
이제 앞 절의 RDBMS 질의와 대응하는 Cypher 질의를 만든다.
MATCH (p:Person {name:'John Doe'})-[r:HAS_ADDRESS]->(a) WHERE r.end IS NULL RETURN a
앞서 본 SQL보다 훨씬 단순하다. 그러나 결과는 여전히 마지막 주소를 표시하는 방식, 곧 관계에 end 프로퍼티가 없다는 사실에 의존한다. 마지막 주소가 무엇인지 아는 논리는 SQL에서처럼 여전히 질의의 일부다. 다만 SQL에서 조인 테이블 위에서 쓰던 인덱스 검사(pa.person_id = p.id, pa.address_id = a.id)를 피할 수 있고, 이 인덱스를 피하는 것만으로도 성능이 좋아진다.
MATCH (p:Person {name:'John Doe'})-[r:HAS_ADDRESS]->(a) WITH r, a ORDER BY r.start ASC WITH r, a RETURN a LIMIT 1
MATCH (p:Person {name:'John Doe'})-[r:HAS_ADDRESS]->(a) WITH r, a ORDER BY r.start ASC WITH r, a RETURN a SKIP 2 LIMIT 1
둘 다 SQL 질의와 마찬가지로 검색-정렬-필터 패턴에 기대어 원하는 데이터에 도달한다. 첫 번째·세 번째 주소를 판별하는 논리는 Cypher 질의의 일부다. 이제 더 섬세한 그래프 데이터 모델링 접근으로 들어간다.
그래프 데이터 모델링 — 고급 접근
이번에는 데이터를 다르게 바라보고 데이터 모델을 세운다. 이 모델은 우리가 데이터를 소비하는 방식의 영향을 받는다.
얼핏 보면 RDBMS의 ER 다이어그램에 더 가까워 보인다. Person, Address, 그리고 Rental 노드가 있다. 그러나 닮음은 거기까지다. Person은 FIRST 또는 LATEST 관계로 Rental 노드에 연결된다. Rental은 다른 Rental 노드로 향하는 NEXT 관계를 가질 수 있고, Address에도 연결된다. 모델이 조금 복잡해 보일 수 있지만, 데이터를 적재해 연결된 모습을 보면 이치가 통한다.
인덱스 설정은 앞의 모델과 같다. Rental 노드에는 어떤 인덱스나 제약도 더하지 않았다. 다음 Cypher가 데이터를 적재한다.
CREATE (p:Person {id:1, name:'John Doe', gender:'Male'}) -- Address 노드 5개 생성 (기본 모델과 동일) -- CREATE (p)-[:FIRST]->(r1:Rental {start:'2001-01-01', end:'2003-12-31'})-[:HAS_ADDRESS]->(a1) CREATE (r1)-[:NEXT]->(r2:Rental {start:'2004-01-01', end:'2008-12-31'})-[:HAS_ADDRESS]->(a2) CREATE (r2)-[:NEXT]->(r3:Rental {start:'2009-01-01', end:'2015-12-31'})-[:HAS_ADDRESS]->(a3) CREATE (r3)-[:NEXT]->(r4:Rental {start:'2016-01-01', end:'2020-12-31'})-[:HAS_ADDRESS]->(a4) CREATE (r4)-[:NEXT]->(r5:Rental {start:'2021-01-01'})-[:HAS_ADDRESS]->(a5) CREATE (p)-[:LATEST]->(r5)
그래프에 저장된 데이터가 이전과 사뭇 다르다. Person은 첫 번째와 마지막 렌탈에만 연결되고, 첫 렌탈부터 마지막 렌탈까지는 NEXT 관계로 이어진다. 이제 세 질의를 다시 던져 보자.
MATCH (p:Person {name:'John Doe'})-[:LATEST]->()-[:HAS_ADDRESS]->(a) RETURN a
이 질의는 앞의 그래프 질의나 SQL 질의와 확연히 다르다. 이전 그래프 모델에서 Cypher 질의는 마지막 주소를 판별하는 방식이 SQL과 비슷했다. 그런데 여기서는 질의가 영어 문장(Person's latest address)을 닮았다. 질의가 더 단순하고 대부분의 사람에게 이해하기 쉬워 보이지만, 이렇게 데이터를 표현할 가치가 있을까. 이 시나리오에서는 데이터를 정교하게 표현하기 위해 저장 공간을 더 쓴다. 처음 그래프 모델과 이 모델의 질의를 프로파일링해 이점이 있는지 확인하자.
MATCH (p:Person {name:'John Doe'})-[:FIRST]->()-[:HAS_ADDRESS]->(a) RETURN a
순회하는 관계만 다를 뿐 질의 1과 정확히 같은 모습이다. 더는 검색-정렬-필터 패턴을 쓰지 않는다. 이것이 이 데이터 모델의 가장 큰 이점이다. 그래프 자체를 데이터 검색의 구조로 쓰기 쉬워지고, 어떤 데이터를 보고 있는지 판별하는 논리가 프로퍼티 비교의 형태로 질의에 새겨지지 않는다.
MATCH (p:Person {name:'John Doe'})-[:FIRST]->()-[:NEXT*2..2]->()-[:HAS_ADDRESS]->(a) RETURN a
첫 렌탈로 순회한 뒤 다음 렌탈을 건너뛰어 세 번째 렌탈에 이르는 방식으로 쓰였다. 우리가 평소 데이터를 바라보는 방식 그대로이고, 데이터를 검색하는 질의를 이렇게 표현하는 것이 자연스럽게 느껴진다. 역시 검색-정렬-필터 패턴에 기대지 않는다. 세 질의의 프로파일을 비교하면 이렇다.
질의 프로파일 비교 — 기본 모델 vs 고급 모델
질의 1 · 최신 주소
질의 2 · 첫 번째 주소
질의 3 · 세 번째 주소
※ 그림 3.8–3.10의 PROFILE 결과 수치 재구성 · 막대는 db hits 비례
질의 1에서 처음 그래프 모델은 18번의 db 접근(hits)과 312바이트의 메모리를 썼고, 새 모델은 12번의 접근과 312바이트를 썼다. 데이터가 커지면 이전 모델의 db 접근은 인물이 가진 관계 수에 선형으로 늘어나지만, 새 모델에서는 상대적으로 일정하게 유지된다. 질의 2에서는 차이가 더 벌어진다. 처음 모델의 실행 계획은 더 크고 복잡해 19번의 접근과 1,020바이트를 썼지만, 새 모델은 질의 1과 거의 같은 계획으로 12번의 접근과 312바이트에 그쳤다. 정렬이 더 많은 메모리를 쓰게 만들고 더 많은 CPU 사이클을 소모하게 한다는 사실이 드러난다. 질의 3의 프로파일은 조금 더 복잡하지만 결과는 같은 방향이다. 처음 모델이 19번 접근과 1,028바이트, 새 모델이 16번 접근과 336바이트다.
질의 프로파일링은 질의가 어떻게 작동하는지 이해하는 최선의 방법이다. 질의 성능이 만족스럽지 않을 때, 프로파일링은 실행의 어느 영역을 개선하거나 바꾸어야 할지 이해하도록 돕는다. 자세한 내용은 Cypher 매뉴얼에서 읽을 수 있다.
질의와 데이터 모델의 분석에서 확인했듯, 데이터 모델을 정의하는 방식을 새로운 눈으로 바라보는 것만으로도 같은 연산의 성능과 비용에 커다란 영향을 줄 수 있다.
이 모델의 또 다른 이점이 있다. 렌탈이 주소의 관점에서 어떻게 돌아가는지 추적하고 싶다면, 같은 주소의 렌탈들 사이에 NEXT_RENTAL 같은 관계 하나만 더하면 된다. 같은 데이터에 대한 다른 관점이 열리는 것이다. RDBMS나 다른 데이터 지속 계층에서 이런 표현을 시도하기는 어렵다. 조인 인덱스 비용을 피하는 관계의 지속과 선택적 스키마라는 유연성을 지닌 Neo4j가 지식그래프 구축에 더 알맞은 이유가 여기에 있다.
좋은 그래프 데이터 모델은 RAG 흐름의 리트리버를 더 유능하게 만든다. 여기서 탐구했듯, 관련 데이터의 검색이 더 빠르고 쉬워지기 때문이다. 다음은 지식그래프를 RAG 흐름의 일부로 쓰는 방법이다.
(:Section {n: 2})-[:COMBINES]->(:GraphRAG)
GraphRAG — RAG와 Neo4j 지식그래프의 결합
앞 장에서 우리는 RAG 흐름의 심장인 리트리버를 보았다. 리트리버는 데이터 저장소를 활용해 관련 정보를 검색하고, 그것을 LLM에 제공해 질문에 대한 최선의 응답을 얻는다. 리트리버는 필요에 따라 다양한 데이터 저장소와 일할 수 있는데, 저장소의 능력이 검색된 정보가 얼마나 유용하고 빠르고 효과적인지를 크게 좌우한다. 바로 여기서 그래프가 큰 역할을 한다. GraphRAG는 그렇게 태어났다.
GraphRAG가 무엇이고 왜 효과적인지는 Microsoft Research 블로그와 공식 문서에서 읽을 수 있다. 포괄적 이해를 원한다면 Microsoft의 연구 논문 From Local to Global: A Graph RAG Approach to Query-Focused Summarization(arxiv.org/abs/2404.16130)을 참고하면 된다. Microsoft는 GraphRAG 프로젝트를 GitHub에 공개해 이 접근을 구현하기 위한 리소스와 도구를 제공한다.
Neo4j 그래프 데이터베이스는 데이터를 노드와 관계로 이루어진 속성 그래프(property graph)로 지속시키는 데 탁월하다. 직관적인 방식으로 데이터를 저장하고 검색하기 쉬워지므로, RAG 리트리버의 데이터 저장소 노릇을 하기에 알맞다. 이 접근은 더 정확하고, 맥락을 알고, 신뢰할 수 있는 AI 기반 애플리케이션을 가능케 한다. 이제 RAG와 지식그래프의 힘을 결합해 LLM 응답을 개선하는 GraphRAG 흐름을 만든다.
GraphRAG — Neo4j로 RAG 모델을 강화하다
앞 장에서 우리는 RAG 모델이 있는 채팅 애플리케이션의 정보 흐름을 논했다. 이제 그 워크플로가 어떻게 증강되어 채팅 애플리케이션의 응답을 개선하는지 본다. 그림 3.11은 GraphRAG의 워크플로다. 사용자의 프롬프트가 LLM API를 거쳐 처리되고, Neo4j에서 관련 정보가 검색되고, 프롬프트와 결합된 뒤 LLM API로 보내진다.
LLM API는 프롬프트와 Neo4j 지식그래프의 관련 정보를 둘 다 사용해 응답을 생성하고, 사용자에게 정확하고 맥락이 풍부해진 결과를 제공한다. Neo4j와 RAG 모델의 능력을 결합함으로써, GraphRAG는 더 많은 도메인 맥락으로 적합성을 끌어올린다. 이 GraphRAG 흐름을 보여줄 간단한 그래프를 만들어 보자.
RAG 통합을 위한 지식그래프 구축
이 예제에서는 시연을 위해 제한된 데이터로 그래프를 만들며, 영화와 그 줄거리에 집중한다. 제공된 코드 예제를 따라가며 Neo4j 데이터베이스를 설정하고, 노드와 관계를 정의하고, Cypher로 기본 질의를 수행하는 법을 배운다. 준비는 이렇다. 먼저 Neo4j 데이터베이스에 접근할 수 있어야 한다. Neo4j Desktop(로컬)이나 Neo4j AuraDB(클라우드) 어느 쪽이든 좋다. 데이터베이스 인스턴스를 시작하고 접속 자격증명(URI, 사용자명, 비밀번호)을 기록해 둔다. 필요한 Python 라이브러리는 Neo4j Python 드라이버와 pandas다: pip install neo4j pandas
외부 데이터셋은 쓰지 않는다. 그래프 모델과 GraphRAG 흐름을 보여주기 위해 하드코딩된 데이터셋을 쓴다. 본격적인 데이터 로딩과 GraphRAG 흐름은 4장과 5장에서 탐구한다. 이 예제는 GraphRAG 흐름의 측면만 보여주기 위한 것이다.
먼저 간단한 그래프를 만든다. 이 그래프로 Neo4j가 GraphRAG 흐름의 어디에, 어떻게 자리 잡는지 보여줄 것이다.
# 1) GraphDatabase 라이브러리 임포트, 접속 정보 정의 from neo4j import GraphDatabase uri = "bolt://localhost:7687" # 자신의 Neo4j URI로 교체 username = "neo4j" # 자신의 사용자명으로 교체 password = "password" # 자신의 비밀번호로 교체 # 2) 노드 생성 def create_graph(tx): tx.run("CREATE (m:Movie {title: 'The Matrix', year: 1999})") # ... # 줄거리 노드 생성 tx.run("CREATE (p:Plot {description: 'A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.'})") # 3) 관계 생성 tx.run(""" MATCH (m:Movie {title: 'The Matrix'}), (p:Plot {description: 'A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.'}) CREATE (m)-[:HAS_PLOT]->(p) """) # 4) 시각화 질의 MATCH p=(:Movie)-[:HAS_PLOT]->() RETURN p LIMIT 5
def query_graph(tx): # 영화와 줄거리를 가져오는 질의 result = tx.run(""" MATCH (m:Movie)-[:HAS_PLOT]->(p:Plot) RETURN m.title AS movie, m.year AS year, p.description AS plot """) # 결과 출력 for record in result: print(f"Movie: {record['movie']} ({record['year']}) - Plot: {record['plot']}")
Movie: The Matrix (1999) - Plot: A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.
전체 코드는 ch3/imdb_kg.py에 있다. 기본 그래프가 완성되었으니, 이제 이 그래프를 GraphRAG 흐름에 쓴다.
RAG를 Neo4j 지식그래프와 통합하기
RAG 모델을 Neo4j와 통합하려면 모델이 그래프 데이터베이스를 질의하도록 설정해야 한다. 통상 RAG 모델과 Neo4j 사이의 통신을 매개하는 API나 미들웨어 계층을 두는 방식이다. 통합 워크플로의 예를 보자.
- 사용자 입력. 사용자가 프롬프트를 제공한다. 예제 코드에는 프롬프트가 스크립트 안에 미리 정의되어 있고(
prompt = "The Matrix"), 다른 영화나 프롬프트로 바꿔 시험할 수 있다. - 질의 생성. 프롬프트가 처리되고, Neo4j에서 관련 정보를 가져올 Cypher 질의가 생성된다. 이를테면 프롬프트에 언급된 영화의 줄거리를 가져오는 질의다:
MATCH (m:Movie)-[:HAS_PLOT]->(p:Plot) WHERE m.title CONTAINS '{prompt}' RETURN m.title, m.year, p.description - 데이터 검색. Cypher 질의가 실행되고, 관련 데이터(예: The Matrix의 줄거리)가 지식그래프에서 가져와진다. 세션에서 질의를 실행해 plot이 null이 아닌 레코드들을 딕셔너리 리스트로 모은다.
- RAG 모델 처리. 검색된 데이터가 원본 프롬프트와 결합되어 RAG 모델로 넘어간다. 모델이 더 풍부하고 맥락을 아는 응답을 생성할 수 있게 된다.
combined_input으로 딕셔너리들을 단일 문자열로 합치고, 출력 여유분을 남긴 채(max_input_length = 512 - 50) 절단(truncation)과 함께 토크나이즈한다. - 응답 생성. RAG 모델이 풍부해진 프롬프트로 응답을 생성한다. 생성 파라미터는
max_length=150, temperature=0.7, top_k=50, top_p=0.9, num_beams=5, no_repeat_ngram_size=3, early_stopping=True로 조율되어 있다.
The Matrix
: the matrix ( 1999 ) : a computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.
이 장의 전체 코드는 ch3/neo4j_rag.py에 있다.
이 코드 조각은 numpy 2 미만 버전에서만 작동한다. numpy 2 이상을 쓰고 있다면, 터미널에서 깨끗한 가상 환경을 만들어 문제를 격리한다: python3 -m venv my_env && source my_env/bin/activate && pip install numpy==1.26.4 neo4j transformers torch faiss-cpu datasets
기본 지식그래프를 만들고 질의하는 법, RAG 모델을 Neo4j와 통합하는 법을 이해했으니, 이제 지능적이고 맥락을 아는 애플리케이션을 만드는 데 필요한 기초 역량이 갖춰졌다. 다음으로 지식그래프를 강화하는 몇 가지 접근을 본다. 여기서는 개념만 소개하고, 지능형 애플리케이션 구축을 위한 상세한 탐구는 이후 장들에서 이어간다.
(:Section {n: 3})-[:ENHANCES]->(:KnowledgeGraph)
지식그래프의 강화
앞 절에서 만든 것은 단순한 그래프다. 지식그래프를 더 효과적으로 만드는 몇 가지 접근이 있고, 이후 장들에서 이 접근들로 우리의 지식그래프를 강화하게 된다.
온톨로지 개발
온톨로지(ontology)는 그래프의 구조와 내용을 정의할 수 있다. 온톨로지를 그래프 안에 지속시키면 데이터와 그 연결성을 더 직관적으로 설명할 수 있게 된다. 그래프가 모범 사례를 따르고 도메인 특화 요구에 정렬되도록 보장하며, 서로 다른 데이터셋에 걸친 일관성 유지와 시간에 따른 그래프 확장에도 도움을 준다. 5장에서 이 장에서 만든 단순한 영화 지식그래프를 강화한다. 더 배우고 싶다면 Neo4j의 온톨로지 글을 보라.
그래프 데이터 과학 (GDS)
그래프로 적재된 데이터만으로도 지식그래프 구실을 하지만, 그래프를 훨씬 효과적으로 만드는 접근이 몇 가지 더 있다. 예컨대 링크 예측(link prediction)이나 커뮤니티 탐지(community detection)를 수행해, 그래프에 이미 있는 데이터로부터 추론된 추가 관계를 노드 사이에 만들 수 있다. 그래프에 저장된 지능이 강화되어 질의할 때 더 나은 답을 얻는다. 10장에서 KNN 유사도와 커뮤니티 탐지 알고리즘을 활용해 그래프를 강화하고 더 많은 지능을 얻는다.
요약
이 장에서 우리는 Neo4j로 RAG 통합을 위한 지식그래프를 만드는 기초를 탐구했다. Neo4j 지식그래프의 중요성과 GraphRAG에서의 역할을 이해하는 데서 출발해, Neo4j 데이터베이스를 설정하고, 노드와 관계를 만들고, 관련 정보를 가져오는 질의를 수행했다. 같은 데이터를 RDBMS와 기본 그래프, 소비 관점의 고급 그래프로 각각 모델링하고 프로파일을 비교함으로써, 데이터 모델을 새로운 눈으로 바라보는 일이 성능과 비용에 얼마나 큰 차이를 만드는지도 수치로 확인했다.
RAG 모델과 Neo4j의 통합 워크플로도 다루었다. 이제 2부, Integrating Haystack with Neo4j: A Practical Guide to Building AI-Powered Search로 나아갈 준비가 되었다. 다음 부에서는 이 장에서 놓은 토대 위에서 Haystack을 Neo4j와 통합해 강력한 AI 기반 검색 능력을 만드는 법을 탐구한다. Haystack과 Neo4j 양쪽의 강점을 활용하는 정교한 검색 애플리케이션의 개발로, 지식과 기술이 자연스럽게 확장될 것이다.