온톨로지로 똑똑한 검색 만들기
벡터 검색의 한계에서 시작해
GraphRAG까지 가는 7단계
오늘의 핵심 질문
RAG 검색을 만들어봤는데
왜 정확한 답이 안 나올까요?
- "Claude Code 시리즈 첫 글"을 물어도 엉뚱한 글이 섞입니다.
- 비슷한 단어가 많은 글이 가까운 글이 아닙니다.
- 답에 근거가 없으니 환각인지 사실인지 알기 어렵습니다.
결론 한 줄
벡터 검색은 의미가 비슷한 것은 잘 찾지만
관계가 맞는 것은 못 찾습니다.
→ 그 약점을 온톨로지·지식 그래프가 메웁니다.
→ 합치면 GraphRAG.
벡터 검색의 한계 1
벡터는 단어가 비슷한 문서를 잘 찾습니다.
하지만 관계를 표현하지 못합니다.
| 질문 | 벡터가 찾는 것 | 사용자가 원한 것 |
|---|---|---|
| "시리즈 1편" | "시리즈"·"1편"이 본문에 등장 | 메타데이터상 1번 글 |
| "4월에 쓴 GraphRAG" | "4월"·"GraphRAG" 등장 본문 | 작성일·태그 일치 글 |
| "이 글이 인용한 자료" | 본문이 비슷한 다른 글 | cites 관계 글 |
벡터 검색의 한계 2
검색된 문서에 사실 관계가 명시되지 않음.
LLM이 "아마 이런 뜻일 거야" 추론 → 환각.
반대로 A authored-by B 같은
**명시적 사실(triple)**이 검색 결과에 들어오면
LLM은 그걸 그대로 인용합니다.
온톨로지란?
도메인의 개념·속성·관계를 형식적으로 명세한 것.
클래스(Class) : Post, Author, Tag, Series, Category
속성(Property) : title, publishedAt, slug
관계(Relation) : writtenBy, hasTag, cites일상에서 우리가 매일 쓰는 분류 체계를
명시적으로 적어둔 것 뿐입니다.
온톨로지 vs 지식 그래프
| 구분 | 온톨로지 | 지식 그래프 |
|---|---|---|
| 비유 | 매뉴얼·설계도 | 실제로 채워진 데이터베이스 |
| 위치 | 스키마(schema) | 인스턴스(instance) |
온톨로지는 스키마, 지식 그래프는 그 위에 데이터를 채워 넣은 그래프.
왜 지금 다시 뜨는가?
오래된 기술인데 2024년부터 다시 주목.
- Microsoft GraphRAG (2024) — 벡터 + 그래프 결합
- Neo4j + LangChain/LlamaIndex 통합 본격화
- LLM이 Cypher/SPARQL을 대신 써줌
→ 그래프 질의어 진입 장벽이 사라짐.
→ 남는 건 온톨로지를 잘 설계하느냐의 문제.
설계의 첫 단추
도메인 분석부터? 아니요.
답할 질의 리스트부터 적습니다.
어떤 질문에 답할 시스템인지 모르고 시작하면
만들어 놓고 보니 핵심 질의를 못 푸는 그래프가 됩니다.
블로그 검색용 질의 7개
| # | 질의 | 필요한 정보 |
|---|---|---|
| Q1 | "시리즈의 1번째 글" | 시리즈 + 순번 |
| Q2 | "4월에 쓴 GraphRAG 글" | 날짜 + 태그 |
| Q3 | "AI 카테고리 최신 5개" | 카테고리 + 정렬 |
| Q4 | "이 글이 인용한 외부 자료" | 인용 관계 |
| Q5 | "이 글과 비슷한 글" | 의미 매칭 + 관계 |
| Q6 | "Claude Code·GraphRAG 둘 다인 시리즈" | 태그 교차 + 시리즈 |
| Q7 | "먼저 읽어야 할 글" | 선수 관계 |
이 7개가 온톨로지 테스트 케이스.
설계 4단계
- 답할 질의를 적는다
- 질의에서 명사 → 클래스 후보 뽑기
- 각 클래스에 속성·관계 도출
- 7개 질의를 다 표현할 수 있는지 검증
설계는 늘 가장 어렵고,
잘못 잡으면 구현이 통째로 뒤집힙니다.
설계에서 흔히 빠지는 함정
- 분류 vs 관계 — Category를 속성으로 둘까 클래스로 둘까?
- is-a 남발 —
subClassOf로 트리만 키움 - 양방향 관계를 두 개 만들기
- 너무 잘게 쪼개기
과소 설계가 아니라 과대 설계가 더 비쌉니다.
구축: 스택 선정
| 후보 | 모델 | 질의어 | LLM 통합 |
|---|---|---|---|
| Neo4j | LPG | Cypher | 1급 시민 |
| GraphDB | RDF | SPARQL | 일부 어댑터 |
| Memgraph | LPG | Cypher | 통합 진행 중 |
| Apache Fuseki | RDF | SPARQL | 거의 없음 |
GraphRAG가 목적이라면 Neo4j Community.
도커로 5분 만에 띄우기
services:
neo4j:
image: neo4j:5-community
ports: ["7474:7474", "7687:7687"]
environment:
- NEO4J_AUTH=neo4j/please-change-me
- NEO4J_PLUGINS=["apoc"]
volumes:
- ./neo4j/data:/datadocker compose up -d → Browser(:7474)
ERD → 온톨로지 매핑
이미 RDB에 ERD가 있다면 그게 1차 초안.
| ERD | Neo4j |
|---|---|
| 테이블 | Label |
| PK | Unique constraint |
| FK | Relationship type |
| 일반 컬럼 | Property |
| 조인 테이블(N:N) | 관계로 직접 변환 |
그래프엔 검색·관계에 쓰는 컬럼만 옮긴다.
LLM으로 본문 자동 추출
frontmatter로 못 잡는 인용·관련·선수 관계는
Claude Tool Use로 구조화 JSON 추출.
tools: [extractionTool],
tool_choice: { type: "tool", name: "record_post_extractions" },
system: [
{ type: "text", text: SYSTEM_PROMPT,
cache_control: { type: "ephemeral" } }
]핵심: 출력 스키마 강제 + 프롬프트 캐싱.
비용 — 실측
블로그 글 100개 기준 월 비용:
| 항목 | 월 비용 |
|---|---|
| Neo4j (로컬 도커) | $0 |
| LLM 추출 | ~$0.18 |
| 임베딩 | ~$0.02 |
| 질의 LLM(1000회) | ~$5 |
| 합 | 약 $5.2 |
입력이 큰 작업에선 프롬프트 캐싱이 사실상 필수.
검수 3단계
LLM 추출 결과를 그래프에 적재하기 전:
- URL 실재 확인 — HEAD 요청 200이 아니면 드롭
- slug 존재 확인 — 알려진 slug 집합과 매칭
- evidence 본문 부분문자열 매칭 — 인용이 진짜인지
이 3단계만으로 사람 검수 부담이 1/10로.
GraphRAG 파이프라인
자연어 질의
↓ 라우팅
graph / vector / hybrid
↓ 검색
Cypher 실행 / 벡터 인덱스 조회
↓ 합성
LLM이 결과를 자연어 답변으로
↓
[refs: ...] 근거 첨부100줄도 안 되는 코드로 완성.
자연어 → Cypher
LLM에 스키마를 시스템 프롬프트로 박아두는 게 핵심.
const SCHEMA = `
Labels: Post, Tag, Series, Category, Author, ExternalResource
Relationships:
- (Post)-[:HAS_TAG]->(Tag)
- (Post)-[:BELONGS_TO_SERIES]->(Series)
- (Post)-[:CITES]->(ExternalResource)
...
`안 그러면 존재하지 않는 라벨을 지어냅니다.
안전 장치
LLM이 만든 Cypher를 그대로 실행하지 말 것.
function isReadOnly(cypher) {
return !/\b(CREATE|MERGE|DELETE|SET|REMOVE|DROP)\b/.test(cypher.toUpperCase())
}- 쓰기 명령 차단
LIMIT강제- 실패 시 fallback 1회까지
라우터: 채널 분담
| 질의 성격 | 라우팅 | 예 |
|---|---|---|
| 엔티티·관계 | graph | "시리즈 1편" |
| 의미 매칭 | vector | "RAG 정확도 올리는 방법" |
| 둘 다 | hybrid | "신뢰할 만한 외부 자료" |
모든 질의를 hybrid로 보내면 비용이 조용히 부풉니다.
4-tier 데이터 아키텍처
이미 RDB·DW·ES가 있는 조직이라면.
| 층 | 잘하는 일 | 못하는 일 |
|---|---|---|
| OLTP (PG/MySQL) | 트랜잭션 | 관계 탐색 |
| DW (Snowflake) | 대용량 집계 | 실시간 |
| 검색엔진 (ES) | 자유 텍스트 | 관계 |
| 그래프 (Neo4j) | 관계·의미 | 집계·트랜잭션 |
그래프는 대체가 아니라 보강.
안티패턴 5가지
- 그래프를 새 OLTP로 쓰기
- 모든 RDB 컬럼을 그래프에 미러링
- 그래프를 DW 대체로 사용
- CDC 없이 매번 풀 재 적재
- 검색엔진을 그래프로 대체 시도
잘하는 일은 잘하는 도구에.
어떤 도메인에 추천하나
| 도메인 | 적합도 |
|---|---|
| 블로그·문서 검색 | 좋음 |
| e-commerce | 매우 좋음 |
| 학술 인용망 | 매우 좋음 |
| 법령·규정 | 좋음 |
| 대화 로그 | 보통 |
| 시계열 센서 | 비추천 |
| 단순 FAQ | 비추천 |
분기점: 관계가 본질적으로 풍부한 도메인인가?
다시 한다면
- SCHEMA를 단일 소스 파일로
- 글이 적으면 벡터는 나중에
- 사람 검수 UI를 일찍 만들기
- 회귀 테스트셋 일찍 만들기
- 안 풀리는 질의 로그를 따로 모으기
운영 두 달 차부터 가장 큰 자산이 되는 것들.
결론
관계 기반 정확도는 확실히 올라갑니다.
답변에 **근거(refs)**가 따라붙어 신뢰가 생깁니다.
다만 만능은 아닙니다.