편집자 노트 · B2B 도입 관점 — 온톨로지가 기술적으로 동작하는 것과 고객 업무에서 안정적으로 쓰이는 것은 다른 문제입니다. 고객은 그래프의 복잡성보다 원하는 답이 정해진 시간 안에 반복해서 나오고, 근거 데이터가 어디에 있으며, 새 문서가 언제 검색 결과에 반영되는지를 중요하게 봅니다. 이 글은 그래프와 벡터의 역할을 질문 유형에 따라 나누고, 이를 응답 SLA·기준 저장소·갱신 주기의 문제로 연결합니다.
여덟 번 도는 루프를 한 번의 융합으로 바꿨습니다
이전 버전의 검색은 LLM이 벡터, SPARQL, 이웃, 경로, 집계 도구를 골라 쓰는 최대 8턴 루프였습니다. 관계를 여러 단계 따라가야 하는 질문에는 유용했지만, 턴마다 LLM을 부르니 느렸고, LLM이 그래프 도구만 타다가 본문 청크를 통째로 놓치는 재현율 문제가 반복됐습니다. 탐색 능력을 넓히는 대신 흔들림도 같이 커진 셈입니다.
그래서 그래프가 벡터보다 나은 지점이 정확히 어디인지부터 정리했습니다. 임베딩 검색은 연속적인 유사도이고 그래프 질의는 집합 연산입니다. 몇 개인지, 전부 무엇인지, 무엇과 연결됐는지 같은 질문은 그래프가 더 완전한 근거를 주지만 단순 사실 조회는 동급입니다. 이 셋을 클래스 전수 열거, 1홉 관계, 개체 확장이라는 별도 채널로 두고 벡터 채널과 대등하게 합쳤습니다.
벡터 시드 ∥ 라벨 링킹 → 1홉 관계 ∥ 클래스 전수 ∥ 개체 확장 → PPR 재순위
↓
MMR 근거 선별 → LLM 합성 1회
탐색 단계에서는 LLM을 쓰지 않습니다. 질문어를 형태소와 한글 2-gram으로 노드 라벨에 매칭하고, 링킹된 노드가 클래스면 인스턴스를 전부 열거하되 상한 150을 두고, 회수된 청크에 등장한 개체에서 1홉을 확장해 그 개체가 나온 청크를 최대 12개까지 더하고, 등장 청크가 적은 노드일수록 변별력이 높다고 보아 그 노드를 시드로 개인화 페이지랭크를 전파합니다. 이 부분은 HippoRAG에서 착안했습니다. 후보를 늘리지 않고 순위만 조정하므로 벡터 점수와 85대 15로 섞습니다. 상위 k는 고정하지 않고 점수 분포로 정하고, 근거는 MMR로 골라 비슷한 청크가 상위를 독차지해 예외나 경고 같은 결정적인 소수 근거가 밀려나는 것을 막습니다. 탐색이 LLM 없이 수백 밀리초에 끝나야 에이전트 워크플로에서 합성 없이 근거층만 넘기는 모드로 쓸 수 있기 때문입니다.
합성은 LLM 한 번이고 규칙을 함께 줍니다. 관계와 소속과 계층은 그래프 관계에서, 수치와 고유명사와 표 값은 본문에서 인용하고, 근거에 없는 것은 지어내지 말라는 규칙입니다. 답변에 실제로 등장한 노드만 근거로 표시해 화면의 강조가 실제보다 넓어지는 것을 막습니다. 검색 로직은 별도 라이브러리로 떼어 다른 코퍼스에서 먼저 검증한 뒤 제품이 가져다 쓰는 구조로 두었습니다.
| 측정 | 조건 | 결과 |
|---|---|---|
| 관계형 질문 15문항 | 관계 형성 후 관계 2,185개, 벡터 단독 대비 | cov@3 0.57→0.63, 1위 완비 33%→40%, 관계 블록에 정답 직접 등장 93% (벡터 단독 0%) |
| 관계형 질문 15문항 (재생성) | 새 추출기 그래프 | 정답 커버리지 0.67 vs 0.60, 관계 블록 정답 직접 80% (벡터 단독 0%) |
| 사실 조회형 40문항 | 같은 컬렉션 | 두 방식 동급 (2% 차) |
| 다른 컬렉션 QA 20문항 | 관계 형성 없이 기본 빌드만 | 관계 블록 정답 직접 20%, cov@3 0.92 (벡터 단독과 같음) |
| 검색 소요 | 420청크 컬렉션, 그래프+벡터 | 중앙값 0.43초 (0.21~1.27) |
마지막에서 두 번째 줄도 눈여겨볼 만합니다. 관계 형성 없이 기본 빌드만 한 컬렉션에서는 그래프를 붙여도 벡터 단독과 같았습니다. 그래프의 이득은 개체가 있다고 나오는 것이 아니라 개체 사이의 관계가 있어야 나옵니다. 검색 자체는 빠릅니다. 사용자가 느끼는 지연은 전부 LLM이 답변을 만드는 시간입니다. 그리고 사실 조회 40문항에서 두 방식이 동급이라는 결과가 관계형 결과만큼 중요합니다. 그래프는 벡터를 대신하는 것이 아니라 벡터가 원래 못 하는 종류의 질문을 맡습니다. 그 경계를 숫자로 확인하고 나서야 어디에 루프가 필요 없는지도 말할 수 있었습니다. 수치 비교 질문은 여전히 약한데, 그 원인은 뒤에서 말할 값의 저장 방식과 닿아 있습니다.
온톨로지는 데이터이면서 인덱스였습니다
그래프는 원래 Fuseki에 RDF로 저장했습니다. 온톨로지니까 RDF 저장소가 정본이어야 한다고 생각했습니다. 그런데 운영에서 빌드 중에 조회가 막히고 진행 상태 관리가 불안정해 빌드 지연의 원인이 됐습니다. 온톨로지 저장소가 온톨로지 빌드를 느리게 만들고 있었습니다.
PostgreSQL 세 테이블을 정본으로 옮겼습니다. 노드, 엣지, 그리고 노드에서 출처 청크로 가는 표입니다. 노드는 클래스와 개체와 속성 세 종류이고, 엣지는 소속과 상하위와 개체 간 관계와 클래스 수준 선언 네 종류입니다. 모든 개체와 클래스 노드는 출처 청크나 DB 행 식별자를 가리킵니다. Fuseki는 설정으로 켤 때만 쓰기 미러로 동작하고 기본은 꺼져 있으며, OWL 문서는 클래스와 속성 정의까지만 생성합니다. Neo4j 같은 속성 그래프 데이터베이스도 쓰지 않습니다.
저장을 옮기면서 온톨로지가 무엇인지에 대한 입장도 정리됐습니다. 구조는 온톨로지가 정답 소스이고 내용은 청크와 SQL이 정답 소스입니다. 무엇이 무엇에 속하는지, 무엇과 연결되는지, 상위와 하위가 무엇인지, 어떤 종류가 몇 개인지 같은 질문에는 노드와 엣지가 그 자체로 답이 됩니다. 수치와 조건과 본문 서술에 대해서는 온톨로지가 원문 청크나 정형 테이블을 가리키는 역할을 합니다. 그러니까 저장 형식으로 보면 데이터이고 검색에서의 역할로 보면 메타데이터입니다. 개체와 관계를 청크에 매달린 태그가 아니라 독립된 데이터로 둔 것은 여러 문서에 흩어진 같은 개체를 하나로 모아 그 개체 중심으로 관계와 근거를 회수하기 위해서이고, 노드에 출처 청크를 붙인 것은 근거 추적과 개체 확장의 출발점이 필요했기 때문입니다.

그래프 뷰어의 군집 개요. 클래스마다 인스턴스 수를 숫자로 보여주고, 전체를 다 그리는 대신 구조가 보이는 표본만 그립니다. 컬렉션을 식별할 수 있는 라벨 하나는 가렸습니다.
저장을 옮기고 나니 화면도 따라 바꿔야 했습니다. 그래프 뷰가 적재 순서대로 앞 500개만 보여주고 있었는데, 5만 노드 컬렉션이면 0.95%를 보는 셈이고 그것도 하필 맨 처음 들어온 것들이었습니다. 연결이 많은 노드부터 남기도록 바꿨고, 검색 근거 노드는 그 상한과 무관하게 항상 포함하게 했습니다. 큰 컬렉션에서 근거 하이라이트가 통째로 안 보이던 이유가 근거가 거의 항상 화면 밖에 있었기 때문이었습니다. 이후 22,001개 노드 컬렉션에서 그리는 노드를 7,469개에서 2,899개로 줄이고 CPU 점유를 25%에서 10%로 내렸습니다.
화면 쪽에서 지금 제공하는 것은 이렇습니다. 지식관리의 컬렉션 화면에서 도메인명과 새 문서 자동 관계 확장 토글, 빌드와 재빌드, 남은 청크 수를 보여주는 관계 확장 실행, 관계 형성에 쓸 모델 선택, DB 연결 적재, 빌드 중단, 진행 단계와 개념·관계·트리플·토큰 수를 다룹니다. 그래프 뷰어는 2D 기본에 3D 선택이고 노드 종류별 색과 엣지 종류 범례, 같은 이름 노드의 병합 표시, 노드 검색, 통계와 품질 배지, 변경 이력이 있습니다. 노드 카드는 상위·하위 계층, 술어별 관계를 방향과 종류와 출처로 구분해 보여주고, 속성값과 출처 청크 미리보기에서 문서로 이동할 수 있으며, 관계 추가·편집·삭제와 노드 삭제가 됩니다. 검색 패널은 질문에 스트리밍 답변과 관계 수·청크 수, 탐색 경로 요약, 근거 노드 강조와 카메라 이동을 제공하고, 워크플로의 정보검색 노드는 그래프가 있는 컬렉션이면 자동으로 온톨로지 검색 모드로 들어가 근거 수, 청크 길이, 폴더 스코프, 합성 답변 포함 여부를 정할 수 있습니다.
편집 도구의 범위는 여기까지입니다. 클래스와 속성 정의 편집, 노드 병합, 라벨 수정은 아직 지원하지 않고, 재빌드하면 수동 편집은 사라집니다. 사용자가 확정한 스키마를 빌드가 다시 읽게 하는 되먹임이 없기 때문인데, 이것은 뒤의 갈림길과 이어집니다.
증분이라면서 매번 전량을 읽고 있었습니다
증분 빌드는 추출 단계만 증분이었습니다. 빌드 끝의 마무리 정리 7단계는 컬렉션 전량을 매번 다시 훑었습니다. 라벨 15만 개 컬렉션에서 이름 정규화 키를 다시 계산하는 데 42초, 계층 유도를 위해 형태소 경계를 다시 계산하는 데 103초가 매 빌드마다 들었습니다. 새 청크가 0건이어도 그랬습니다.
근본 원인은 "같은 대상인가", "상하위인가"의 판정 근거를 어디에도 남기지 않는 것이었습니다. 형태소 경계와 정규화 키를 매번 계산하고 버리니 문서를 한 개 더해도 처음부터 다시 계산합니다. 그래서 계산 결과를 노드에 적었습니다. 이름 키 컬럼과 그 인덱스를 두고, 노드가 처음 들어올 때 한 번만 키를 채웁니다. 같은 대상 묶기는 파이썬에서 전량 그룹핑하던 것을 인덱스 위의 그룹 조회로 바꿔 형태소 분석이 0회가 됐고, 계층 유도 대상은 새 이름과 새 이름이 붙을 수 있는 옛 이름으로 좁혔습니다. 옛 이름을 고르는 비용은 15만 건에 0.06초입니다.
동치를 확인할 때 한 가지가 걸렸습니다. 옛 이름 열 개로 빌드한 뒤 새 이름 여섯 개를 더해 전량 유도와 증분 유도의 결과 엣지를 비교하니 4건이 같았는데, 그 4건이 전부 옛 이름이 새 이름의 하위가 되는 역방향이었습니다. 새 이름만 보고 옛 이름을 안 훑으면 통째로 빠지는 경우입니다. 증분이 전량과 같은 답을 내려면 새 것만 보는 것이 아니라 새 것 때문에 달라질 옛 것까지 봐야 했습니다.
지금 빌드는 이렇게 돕니다. 업로드 후 임베딩이 끝나면 즉시 기본 빌드가 새 청크만 대상으로 돌고, 놓친 컬렉션은 10분 주기 스윕이 회수합니다. 직전 완료 작업의 처리 청크 목록과 차집합만 읽어 새 청크가 0건이면 즉시 끝납니다. 노드 28,083개 컬렉션의 전체 재빌드가 기본 253초에 관계 형성 273초, 문서 두 개는 30초 이내입니다. 외부 DB는 워터마크 컬럼 이후의 행만 읽고 200행 적재에 10초가 걸리며, 기본키 기반 안정 식별자라 워터마크를 리셋해 전량을 다시 넣어도 중복이 생기지 않습니다. 재실행이 멱등해야 운영에서 안심하고 다시 돌릴 수 있습니다.
| 트리거 | 조건 | 대상 |
|---|---|---|
| 업로드 훅 | 문서 임베딩 완료 직후 | 새 청크만 기본 빌드 |
| 관계 형성 체인 | 위 훅 뒤, 컬렉션 설정에서 자동 관계 확장이 켜져 있으면 | 관계 미추출 청크 |
| 주기 스윕 | 기동 90초 후 10분마다, 빌드된 청크 수가 컬렉션 청크 수보다 작으면 | 최대 20컬렉션 순차 |
| 설정 변경 | 관계 형성 토글을 새로 켤 때 | 이미 쌓인 문서에 소급 |
| 수동 실행 | 빌드·재빌드·관계 확장 버튼 | 재빌드는 전체 삭제 후 |
| 외부 DB 작업 | 워터마크 컬럼 기준 | 새 행만, 멱등 |
그래서 저희가 가정한 허용 지연은 분 단위입니다. 사람이 문서를 올리고 그 뒤에 검색하므로 초 단위 반영까지는 필요하지 않다고 봤습니다. 다만 이 가정을 고객사와 확인한 적은 아직 없습니다. 정형 데이터에서는 심사나 승인 진행 상태처럼 일 단위 갱신이면 충분한 데이터와 주문이나 재고처럼 분·초 단위로 바뀌는 데이터를 후보로 보고 있는데, 후자를 온톨로지에 두는 것이 맞는지 자체를 아직 확신하지 못하고 있습니다. 색인 비용 면에서 저희 구조는 Lazy GraphRAG 쪽에 가깝습니다. 색인 때 LLM 요약을 만들지 않고 관계 조회와 전수 열거와 재순위를 질의 시점에 즉석 계산합니다. 남는 것은 빌드 시점에 LLM으로 뽑는 관계 추출까지 질의 시점으로 미룰 수 있는가인데, 이것은 아래 갈림길에 들어 있습니다.
지금 쓰고 있는 것
| 영역 | 기술 | 쓰임 |
|---|---|---|
| 저장 | PostgreSQL (JSONB 속성, 트라이그램 색인, 이름 색인) | 그래프 정본, 라벨·속성값 문자열 매칭 |
| 벡터 | Qdrant | 청크 임베딩 검색, 개체가 등장한 청크 회수 |
| 한국어 | Kiwi 형태소 분석 | 개체 이름 경계, 이름 색인, 질의어 분리, 흔한 낱말 판정, Hearst 패턴 |
| 결정론 추출 | 표 격자 파서, Hearst 패턴, 복합명사 머리 유도 | 기본 빌드 |
| LLM | OpenAI·Anthropic·Google·vLLM, 구조화 출력 | 관계 형성 추출, 동의어 통합, 영문 URI 번역, 검색 합성 1회 |
| 검색 | omnifuse (자체 라이브러리, PyPI) | 원샷 융합, 적응형 상위 k, MMR, 관계 순위 |
| 그래프 알고리즘 | 개인화 페이지랭크, Louvain | 청크 재순위, 뷰어 군집 표시 |
| RDF | rdflib, Jena Fuseki (옵션 미러) | OWL TBox 생성, 필요 시 SPARQL 병행 |
| 화면 | Next.js, Canvas 2D, Three.js, d3-force | 그래프 뷰어, 노드 카드, 검색 패널 |
참고한 공개 기법은 HippoRAG의 개체 확장과 PPR과 노드 변별력, LightRAG의 델타 인덱스, EDC의 스키마 되먹임, Graphiti의 후보 색인, Hearst 패턴, MMR입니다.
B2B 도입을 검토한다면
고객 환경에서는 대표 질문을 관계·목록형, 유사 문서형, 정확한 수치·조건형으로 나누고 그래프, 벡터, SQL 가운데 어느 채널이 답을 책임질지 먼저 정하는 편이 좋습니다. 그다음 질문 유형별 품질과 지연, 호출 비용을 함께 측정해야 합니다. 하나의 종합 정확도만으로는 특정 업무 질문이 왜 실패하는지 설명하기 어렵습니다.
기준 저장소와 쓰기 권한, 재빌드 중 서비스 동작, 신규 문서 반영 주기와 허용 가능한 지연도 운영 합의에 포함되어야 합니다. 아키텍처가 안정되어도 삭제·수정, 동명이인, 출처 추적과 같은 현실의 데이터 문제는 남습니다. 다음 편에서는 아직 해결하지 못한 항목을 도입 전 의사결정표와 검증 기준으로 바꿔봅니다.
이전 편 → 온톨로지 빌드·검색 개선 대장정 1편 — 자동 빌드에서 LLM을 뺀 이유
다음 편 → 온톨로지 빌드·검색 개선 대장정 3편 — 도입 전에 정해야 할 것들
시리즈 전체 보기 → 온톨로지 개발기

