two-thirteen
그래프
그래프 DB 없이 지식 지도 만들기: 관계에 이름 붙이고 근거 남기기
2026-11-10 Web & Mobile

그래프 DB 없이 지식 지도 만들기: 관계에 이름 붙이고 근거 남기기

이 블로그 Graph 메뉴를 만들며 적용한 온톨로지·그래프 모델 기록입니다. 그래프 DB 대신 빌드 때 노드·엣지 파일을 만든 이유, 관계 어휘 25종, 엣지의 네 경로, LLM 관계 추출과 검증, 노드 해설로 바꾼 과정을 다룹니다.

1편에서 검색을 위해 작은 온톨로지를 만들었습니다. 개념과 개체를 나누고, BigQuery가 데이터베이스의 한 종류라는 걸 검색이 알게 했습니다.

그러고 나니 욕심이 생겼습니다. 검색 결과 옆에 "이 관계를 따라왔다"는 이유를 붙이는 것만으로는 아까웠습니다. 관계 자체를 보여 주는 화면이 있으면 좋겠다 싶었습니다. 그게 지금 상단 메뉴의 Graph입니다.

이 글은 그 화면을 만들며 시도한 것과 버린 것의 기록입니다.

그래프 DB는 띄우지 않았다

그래프라고 하면 그래프 데이터베이스부터 떠오릅니다. 그런데 이 블로그는 빌드한 HTML을 그대로 올리는 정적 사이트입니다. 검색 때와 같은 이유로, 글 몇십 편의 관계를 보여 주자고 DB 서버를 하나 더 운영하고 싶지 않았습니다.

대신 모델링 방식만 빌려 왔습니다. Spanner Graph는 노드 테이블과 엣지 테이블을 선언하고, 엣지마다 시작 노드와 도착 노드를 키로 가리킵니다. 이 블로그는 같은 일을 YAML 파일로 합니다. 개념 66개와 개체 89개를 파일에 적고, 빌드가 글을 읽어 엣지를 만든 다음 kgraph.js 한 파일로 씁니다.

지금 이 파일에는 노드 222개와 엣지 1,259개가 들어 있습니다. 크기는 약 250KB이고 Graph 화면을 열 때만 불러옵니다. 질의 언어도 인덱스도 없지만, 이 규모에서는 브라우저가 전부 메모리에 올려 놓고 따라가도 충분히 빨랐습니다.

노드는 네 층입니다.

층 예 개수
도메인 데이터 플랫폼, AI 애플리케이션 7
서브도메인 에이전트, 검색 25
키워드 시맨틱 레이어, BigQuery 123
글 게시글 하나하나 67

첫 지도는 실패작이었다

원 위에 노드를 가지런히 늘어놓는 안도 있었습니다. 보기에는 정돈돼 있었지만 아무것도 알려 주지 않아 버렸습니다.

남은 건 외부 라이브러리 없이 캔버스에 직접 그린 힘 기반 배치입니다. 노드끼리 밀어내고 엣지가 당기는 방식입니다. 움직임은 그럴듯했는데 문제는 선이었습니다. 대부분이 "같은 글에 함께 나왔다"는 선이라, 뭉쳐 있다는 것 말고는 읽을 게 없었습니다.

선이 많은 게 문제가 아니었습니다. 선에 이름이 없는 게 문제였습니다.

관계에 이름을 붙이다

그래서 관계 어휘를 따로 만들었습니다. relations.yaml 한 파일에 7개 성격, 25종의 관계를 적었습니다.

성격 관계
포함·구성 포함, 구성, 사례
인과·해결 원인, 완화, 해결, 가능하게 함, 의존, 선행
대비·상충 상충, 맞바꿈, 대안, 대체
유사·연관 유사, 연관
활용·연동 사용, 연동, 구현, 측정, 제작
근거·글 뒷받침, 글이 다룬 것
함께 다룸 같은 글에서 함께, 다른 도메인끼리

관계마다 화면 색과 선 모양, 짧은 이름, 기본 무게, 방향이 있는지를 적습니다. 문장 틀도 같이 둡니다.

causes: {category: causal, short: "원인", s: "{A은} {B을} 일으킨다", w: 0.60, source: asserted}

{A은}은 이름의 받침을 보고 '은'과 '는' 중 하나를 고르라는 표시입니다. 글이 늘어 새 관계가 필요하면 이 파일에 한 줄만 더합니다.

어휘를 정할 때 세 군데를 참고했습니다.

  • Palantir 링크 타입: 링크 타입 하나는 실제 관계 하나입니다. 같은 두 대상 사이라도 관계가 다르면 링크 타입을 따로 둡니다. 양쪽이 각자의 이름으로 건너간다는 점도 가져왔습니다.
  • W3C SKOS: 상위(broader)·하위(narrower)와 연관(related)을 나눕니다. related는 방향이 없습니다.
  • GraphRAG 계열: 본문에서 관계를 뽑을 때 근거 문장을 같이 보관합니다. AWS GraphRAG Toolkit은 여러 문서에 걸쳐 나오는 사실과 개체를 징검다리 삼아 서로 다른 출처를 잇습니다.

SKOS를 다시 읽다가 하나 걸리는 걸 발견했습니다. SKOS는 broader를 전이 관계로 보지 않습니다. 직간접 상위를 다 따지려면 broaderTransitive라는 별도 속성으로 추론합니다. 이 블로그 스키마는 broader에 transitive: true를 달아 두었습니다. 검색에서 상위·하위를 여러 단계 건너는 데는 편하지만, SKOS의 정의와는 다릅니다. 이름을 빌려 온 이상 이 차이는 기억해 두기로 했습니다.

엣지가 생기는 네 경로

한 쌍의 노드 사이에는 관계를 하나만 둡니다. 여러 경로에서 관계가 겹치면 뜻이 분명한 쪽이 이깁니다.

  1. 사람이 적은 관계 (relations_curated.yaml, 31줄): 상충, 완화, 대체처럼 판단이 필요한 관계. 줄마다 이유를 한 줄 붙입니다.
  2. 본문에서 LLM이 뽑은 관계 (relations_extracted.yaml): 아래에서 자세히 다룹니다.
  3. 온톨로지 파일의 관계와 분류 구조: BigQuery는 데이터베이스의 사례이고, 데이터 플랫폼은 데이터베이스를 포함한다는 식입니다.
  4. 규칙으로 계산한 관계: 같은 글에 함께 나왔는지, 이웃을 많이 공유하는지(이웃 집합의 겹침 0.34 이상, 공유 이웃 2개 이상).

막상 세어 보니 결과가 예상과 달랐습니다. 엣지 1,259개의 내역은 이렇습니다.

종류 개수
글이 다룬 것 507
같은 글에서 함께 다룸 353
포함(분류 구조) 164
구조적 유사 · 다른 도메인끼리 41
이름 있는 의미 관계(원인·해결·사용·대안 등 16종) 194

이름을 붙이자고 시작한 일인데, 뜻이 담긴 관계는 여전히 15% 정도입니다. 나머지는 "함께 나왔다", "이 글이 다뤘다"입니다. 그래서 화면은 이 둘을 낮춰 봅니다. 노드를 눌렀을 때 인과·대비·활용 관계가 든 길을 먼저 보이고, 포함과 함께 다룸만으로 이어진 길은 남는 자리에만 둡니다.

LLM에게 관계를 뽑게 할 때

의미 관계를 사람이 다 적을 수는 없습니다. 그래서 글 본문에서 관계를 뽑는 일을 업스테이지 Solar에 맡겼습니다. 다만 그대로 믿지 않으려고 조건을 겹겹이 걸었습니다.

  • 닫힌 어휘: 관계 종류는 위 25종 안에서만, 대상은 그 글이 다룬 노드 이름 안에서만 고릅니다.
  • 근거 문장 복사: 관계마다 본문 문장을 고치지 말고 그대로 복사하게 합니다. 본문에 실제로 없는 문장이면 버립니다.
  • 두 이름이 다 나올 것: 근거 문장에 두 대상의 이름이 모두 글자 그대로 있어야 합니다. 엉뚱한 문장을 붙여 오는 경우가 있었기 때문입니다.
  • 2차 검증: 같은 모델에게 "이 문장이 이 관계를 직접 말하는가"만 한 번 더 묻습니다. 같은 문장에 함께 나온 것만으로는 관계로 치지 않습니다.

그래도 틀립니다. 처음 돌렸을 때 13개가 나왔고, 직접 읽어 보니 6개가 틀렸습니다. 지우고 7개만 남겼습니다.

지금은 글 67편에서 관계 28개가 남아 있습니다. 관계가 하나라도 나온 글은 19편뿐입니다. 적어 보이지만 이 정도가 맞다고 봅니다. 행사 소개 글은 제품 이름이 많아도 "A가 B를 일으킨다"처럼 관계를 직접 말하는 문장은 드뭅니다. 억지로 늘리면 지도에 그럴듯한 거짓말이 생깁니다.

여러 단계 따라가기

노드를 누르면 관계를 따라 나갑니다. 규칙은 단순합니다.

  • 최대 3단계까지 갑니다.
  • 길의 무게는 지나온 엣지 무게의 곱입니다. 0.12 아래로 떨어지면 버립니다.
  • 단계마다 무게가 큰 노드 14개만 계속 따라갑니다. 글이 늘어도 한 번 누를 때 계산이 폭주하지 않게 하려는 장치입니다.
  • 글 노드는 길의 끝일 뿐, 거쳐 가지 않습니다.

두 노드를 골라 그 사이 길을 찾는 기능도 있습니다. 이미 쓴 엣지의 무게를 깎아 가며 다시 찾아서 서로 다른 길을 최대 세 가지 보입니다.

틀 문장은 날것이었다

처음에는 찾은 길을 문장 틀에 넣어 보였습니다. "BigQuery measures는 시맨틱 레이어를 구현하고, 시맨틱 레이어는 지표 정의 불일치를 완화한다." 조사도 받침에 맞게 골랐습니다.

문법은 맞았습니다. 그런데 읽어 보면 날것이었습니다. 관계 이름을 이어 붙인 문장이라 "그래서 뭐가 중요한데"가 빠져 있었습니다. 구조 데이터를 자연스러운 문장으로 바꾸는 건 생각보다 어려운 일이었습니다.

한때는 지도 자체를 접고, 개념마다 주제 페이지를 만드는 쪽으로 돌아서기도 했습니다. 전체 그래프는 커질수록 읽기 어렵다는 연구들을 보고 내린 판단이었습니다. 주제 페이지 126개를 만들었지만 메뉴 어디에서도 들어갈 길이 없었고, 지금은 검색엔진 색인에서도 뺐습니다. Graph 메뉴는 다시 지도로 돌아와 있습니다.

결국 바꾼 건 노드를 눌렀을 때 오른쪽에 나오는 내용입니다. Microsoft GraphRAG 논문에서 힌트를 얻었습니다. 이 논문은 개체 그래프를 만든 뒤, 밀접한 개체 묶음마다 요약을 미리 만들어 둡니다. 질문이 올 때 그래프를 즉석에서 문장으로 바꾸지 않습니다.

그래서 노드마다 해설을 미리 만들어 YAML에 저장해 둡니다. 화면은 그 파일만 읽습니다.

  • 그 노드를 다룬 글 최대 4편에서, 노드 이름이 나오는 문단만 모아 Solar에 줍니다.
  • 합니다체 한 문장 30~120자, 노드 이름을 글자 그대로 쓰고, 발췌에 없는 숫자는 쓰지 않습니다.
  • 2차 검증을 두 번 합니다. 발췌와 맞는지, 그리고 행동 주체가 발췌와 같은지. 주어를 슬쩍 회사 이름으로 바꿔 쓰는 오류가 한 번 판정으로는 새어 나왔기 때문입니다.

처음 돌렸을 때는 117개 노드 중 78개만 통과했습니다. 범위가 넓어 실패하던 노드는 글마다 따로 한 문장씩 받는 방식으로 다시 시도했습니다. 지금은 해설 대상 125개 노드 모두에 해설이 있습니다.

해설 아래에는 본문에서 뽑은 관계만 근거 문장과 함께 붙입니다. 규칙으로 계산한 관계는 지도에 선으로는 그리지만, 근거 문장 목록에는 넣지 않습니다.

돌아보면

Graph 메뉴를 만들며 남은 판단은 세 가지입니다.

첫째, 이 규모에서는 그래프 DB보다 그래프 모델이 먼저였습니다. 노드와 엣지 타입을 선언하고 타입이 안 맞으면 빌드가 실패하게 한 것만으로도, DB 없이 충분히 그래프답게 굴러갔습니다.

둘째, 관계는 이름과 근거가 있어야 쓸모가 있습니다. 이름 없는 선 천 개보다 근거 문장이 붙은 관계 수십 개가 더 많은 걸 말해 줬습니다.

셋째, LLM이 만든 건 미리 만들고, 검증하고, 저장합니다. 화면에서 즉석으로 만들지 않습니다. 틀린 걸 사람이 지울 수 있고, 다음 빌드에서도 같은 결과가 나옵니다.

온톨로지, 지식 그래프, 시맨틱 레이어가 각각 무엇인지는 따로 정리한 글에 있습니다. 이 블로그 기준으로 말하면, concepts.yaml·entities.yaml과 관계 어휘가 온톨로지에 가깝고 kgraph.js가 지식 그래프에 가깝습니다. 다만 OWL 같은 형식 의미는 쓰지 않으니 엄밀한 의미의 온톨로지라기보다 어휘에 가깝습니다.