기술

본문을 벡터 payload로 옮겼더니, GraphRAG 검색이 2홉에서 1홉이 됐습니다

2026.09.129분 읽기

사내 제안서 지식검색 도구를 벡터 검색에서 GraphRAG로 확장하던 중이었습니다. 의미 검색은 벡터 저장소가 맡고, 문서와 개념 사이의 관계는 지식그래프가 맡는 구성입니다. 저장소가 둘이 되면 곧바로 따라오는 질문이 하나 생깁니다. 청크 본문을 어느 쪽에 둘 것인가입니다.

처음에는 그래프에 두었습니다. 문서와 청크와 엔티티가 한 그래프 안에 이어져 있으면 구조가 한눈에 들어온다고 봤습니다. 정규화 관점으로도 본문은 청크 노드의 속성이 맞았습니다. 이 판단이 검색 경로에서 비용으로 돌아왔습니다.


검색이 매 질의마다 두 번 움직였습니다

질의가 들어오면 임베딩을 만들어 벡터 저장소에서 유사한 청크를 찾습니다. 이때 돌아오는 것은 청크 식별자와 유사도 점수입니다. 사람이 읽을 본문은 아직 손에 없습니다. 그래서 그래프로 한 번 더 가서 해당 청크 노드를 조회해야 본문이 나왔습니다.

한 번의 검색이 저장소 두 곳을 왕복하는 구조였습니다. 지연이 두 번 쌓이고, 실패 지점도 두 개가 됩니다. 그래프 쪽이 잠깐 느려지면 벡터 검색이 정상적으로 끝나도 사용자 화면에는 아무 문장도 뜨지 않습니다.

검색 품질 문제로 보이던 것이 실제로는 배치 문제였습니다. 본문이 검색 진입점에서 한 칸 떨어진 자리에 놓여 있었습니다.


본문 위치를 정규화가 아니라 접근 경로로 정했습니다

선택지는 두 개였습니다. 본문을 그래프 노드의 속성으로 두거나, 벡터 저장소의 payload로 옮기거나입니다. 둘을 같은 표에 올려놓고 비교했습니다.

기준

그래프 노드에 저장

벡터 payload에 저장

검색 진입점과의 거리

한 칸 떨어져 있음

같은 자리

본문까지 홉 수

2홉

1홉

관계 탐색

노드 속성으로 바로 읽음

관계는 그래프가 따로 담당

저장소 부피

그래프가 커짐

벡터 저장소가 커짐

갱신 시 다룰 것

본문만

본문과 임베딩을 함께

판단 기준을 정규화에서 접근 경로로 바꾸자 답이 하나로 좁혀졌습니다. 이 본문을 실제로 읽는 주체가 무엇인가를 물으면 답은 언제나 검색이었습니다. 관계 탐색이 본문을 통째로 읽는 경우는 없었고, 그쪽에 필요한 것은 엔티티와 관계였습니다.

그래서 청크 본문을 벡터 저장소의 payload로 옮겼습니다. 검색은 벡터 kNN 한 번으로 유사 청크와 본문을 함께 받아옵니다. 이미 쌓여 있던 540청크는 backfill로 채웠습니다. 그래프의 청크 노드는 식별자와 최소 메타데이터만 남기고 비웠습니다.

BEFORE  질의 → 임베딩 → 벡터 kNN(청크 id) → 그래프 조회(본문)      = 2홉
AFTER   질의 → 임베딩 → 벡터 kNN(청크 id + 본문 payload)          = 1홉

157-1.jpg

옮기고 나서 응답 시간보다 먼저 달라진 것은 코드였습니다. 검색 경로에서 그래프 세션을 열고 닫는 구간이 통째로 사라졌습니다. 두 저장소의 상태가 어긋났을 때를 대비하던 방어 코드도 함께 정리했습니다.


그래프는 본문 창고에서 엔티티 관계망으로 돌아갔습니다

본문이 빠지자 그래프의 역할이 한 문장으로 정리됩니다. 무엇이 무엇과 어떻게 이어져 있는지를 표현하는 자리입니다. 저장소마다 맡을 일을 하나씩만 준다는 원칙을 여기서 다시 적용했습니다. 한 저장소가 검색과 관계를 동시에 맡으면 양쪽 다 어정쩡해집니다.

역할이 정해지자 온톨로지를 단일 출처로 확장했습니다. 엔티티 6타입과 Community, 그리고 언급·연관·소속을 나타내는 세 종류의 관계를 한 곳에서 정의했습니다. 추출 코드와 질의 코드가 각자 아는 스키마를 따로 들고 있으면, 둘이 갈렸을 때 어느 쪽이 맞는지 확인할 방법이 없어집니다.

스키마 정의를 온톨로지 한 곳으로 모았을 때 같은 질의의 답이 어떻게 달라지는지는 앞서 한 번 정리해 두었습니다.


정규화는 표기까지만 하고 의미는 남겼습니다

청크에서 엔티티와 관계를 뽑는 일은 LLM에 맡기고 병렬로 돌렸습니다. 추출된 결과는 APOC로 그래프에 병합했습니다. 손이 많이 간 지점은 추출 자체가 아니라 그다음이었습니다.

같은 개념이 문서마다 다른 표기로 등장합니다. 공백이 들어가거나, 접두어가 붙거나, 괄호가 따라옵니다. 이걸 그대로 두면 하나의 개념이 노드 서너 개로 갈라져 그래프가 부풀기만 합니다. 그래서 표기 변형은 LLM 정규화로 병합했습니다.

여기서 한 칸 더 나가면 다른 종류의 사고가 납니다. 표기가 닮았다는 이유로 뜻이 다른 개념까지 묶는 경우입니다. 정보보호관리체계와 정보전략계획은 문서에서 비슷하게 줄여 쓰이지만 전혀 다른 사업입니다. 이 둘이 한 노드로 합쳐지면 그래프는 조용히 거짓말을 시작합니다.

그래서 정규화 지침을 표기 단위로 제한했습니다. 띄어쓰기와 접두어 같은 표기 변형은 합치고, 의미가 다르면 표기가 닮아도 분리합니다. 누락은 나중에 채우면 되지만, 과병합은 잘못된 연결을 만들어 두고 그 결과를 검색 답변에 실어 보냅니다.

157-2.jpg


부산물로 사업을 가로지르는 엔티티 514개가 나왔습니다

540청크에서 엔티티 2,766개가 나왔습니다. 여기까지는 예상 범위였습니다. 뽑아놓고 세어 보니 그중 514개가 두 개 이상의 사업에 동시에 걸려 있었습니다.

AI, 클라우드, 정보보호, RAG 같은 공통 개념이 서로 다른 사업 문서를 잇고 있었습니다. 문서 단위로 볼 때는 보이지 않던 연결입니다. 사람이 미리 정리해 둔 분류 체계에도 없던 축이었습니다.

검색을 거들려고 만든 그래프가 무엇이 무엇과 닿아 있는지를 보여주는 자산이 됐습니다. 새 제안서를 쓸 때 비슷한 사업을 찾아 들어가는 경로가 하나 더 생긴 셈입니다. 여기까지 작업하는 동안 테스트 65개는 통과 상태를 유지했습니다.

157-3.jpg


1홉이 항상 빠른 것은 아닙니다

이 선택에는 대가가 따라옵니다. 같은 구조를 검토하는 자리라면 아래 세 가지를 먼저 확인하는 편이 낫습니다.

payload에 본문이 들어가면 벡터 저장소가 커집니다. 본문이 아주 크면 별도 본문 스토어를 두고 식별자만 참조하는 쪽이 나을 수 있습니다.

payload가 비대해지면 검색 응답 자체가 무거워집니다. 목록은 가볍게 받고 본문은 필요한 순간에 따로 가져오는 방식이 맞을 때도 있습니다.

갱신할 때 본문과 임베딩을 함께 다루게 됩니다. 본문만 고치고 벡터를 그대로 두면 검색 결과와 화면에 뜨는 문장이 어긋납니다.

LLM 정규화도 완전하지 않습니다. 표기가 닮은 개념을 사람은 문맥으로 바로 구분하는데, 모델은 주변 문맥이 얇을 때 틀립니다. 뜻이 다른 동형 표기의 분리는 사전을 보강하거나 사람이 한 번 훑는 구간을 남겨두는 편이 안전합니다.


저장 위치를 정할 때 확인한 것

이 데이터를 실제로 읽는 주체가 무엇인지 먼저 적습니다. 정규화상 어디가 맞는지는 그다음에 봅니다.

읽는 경로에서 몇 홉 떨어져 있는지 셉니다. 한 홉은 지연 하나와 실패 지점 하나를 뜻합니다.

저장소마다 맡을 역할을 한 문장으로 적습니다. 두 문장이 되면 역할이 겹치고 있다는 신호입니다.

정규화 규칙은 표기와 의미를 나눠서 정합니다. 과병합은 누락보다 되돌리기 어렵습니다.

부산물로 나온 데이터를 한 번 들여다봅니다. 원래 목적과 다른 쓸모가 붙어 있을 때가 있습니다.


저장 위치는 취향이 아니라 설계 결정입니다

같은 데이터인데 어디 두느냐로 검색 비용이 갈렸습니다. 본문을 옮긴 것 말고는 임베딩 모델도 검색 알고리즘도 그대로였습니다. 저장 위치는 정규화 취향의 문제가 아니라 접근 경로가 정하는 설계 결정입니다.

벡터와 그래프를 같이 쓸 때 두 저장소의 역할을 어떻게 갈랐는지는 따로 정리해 둔 글이 있습니다.

설계 문서에 저장 위치를 적기 전에, 그 데이터를 읽는 쪽이 어디에 서 있는지를 먼저 그려보면 홉 하나는 미리 줄일 수 있습니다.

#GraphRAG#데이터지역성#지식그래프#벡터검색#엔티티정규화