설계 문서 · 작성 중

edisco-graph

eDiscovery 커뮤니케이션 관계망을 온톨로지 기반 지식그래프로 재구성하고, 실무자가 탐색할 수 있게 만드는 도구.

최종 수정 2026-09-07 대상 코퍼스 Enron 스택 Python · RDF/OWL · Cytoscape.js

00 확정된 것

브레인스토밍에서 결정된 전제. 뒤집으려면 아래 설계도 함께 바뀝니다.

항목결정
목적실무자가 실제로 쓰는 제품. 논문용 벤치마크가 아님
공략 지점인물·조직 관계망 분석 — 엔티티 해소와 커뮤니케이션 네트워크
입력 데이터Enron 공개 코퍼스로 시작. 실데이터 연동은 이후
핵심 동작그래프 시각적 탐색
온톨로지 수준하이브리드 — RDF/OWL로 정의, property graph로 투영
v1 범위수직 슬라이스. 커스토디언 15~20명(약 5만 통)을 끝까지 관통

01 아키텍처 확정

7단계 독립 모듈. 각 단계가 결과를 디스크에 남기므로 한 단계를 고쳐도 앞 단계를 다시 돌리지 않습니다.

Enron maildir
   │
   ├─[1] ingest    파싱 → 정규화된 Message 레코드          → SQLite
   ├─[2] extract   주소·표시이름·서명에서 Actor 후보 추출   → SQLite
   ├─[3] resolve   Actor 후보 → Person 클러스터            → SQLite
   ├─[4] ontology  RDF 트리플 생성 + OWL 추론 (owlrl)      → graph.ttl
   ├─[5] project   RDF → property graph 투영 + 지표 계산    → graph.pkl
   ├─[6] serve     FastAPI: ego-network 서브그래프 JSON
   └─[7] ui        Cytoscape.js 렌더링, 클릭 시 확장

하이브리드가 실현되는 지점은 [4]→[5]입니다. RDF/OWL이 온톨로지의 단일 진실 원천이고 추론도 여기서 일어납니다. property graph는 그 결과를 탐색용으로 투영한 것입니다. 단방향이라 두 모델을 양방향 동기화하는 문제가 생기지 않습니다.

Neo4j를 넣지 않는 이유

v1 규모(약 2만 노드 / 20만 엣지)는 networkx 인메모리로 충분합니다. Neo4j를 넣는 순간 Docker·드라이버·Cypher·RDF↔Neo4j 동기화가 따라옵니다. 성능이 실제로 아플 때 [5][6]만 교체하면 되도록 경계를 그어둡니다.

디렉터리

edisco-graph/
├── ontology/ediscovery.ttl    # OWL 스키마 — 사람이 읽고 고치는 파일
├── src/edisco/
│   ├── ingest.py  extract.py  resolve.py
│   └── ontology.py  project.py  api.py
├── web/index.html             # Cytoscape.js 단일 페이지
├── design/index.html          # 이 문서
├── data/                      # gitignore — 원본·중간산출물
└── tests/

프론트엔드는 React 없이 단일 HTML + Cytoscape.js. 빌드 도구도 없습니다. 필요해지면 그때 올립니다.

02 온톨로지와 엔티티 해소 확정

"온톨로지 썼습니다"가 장식이 되지 않으려면, 추론기가 없으면 직접 짜야 했을 코드를 대체해야 합니다.

추론이 밥값을 하는 네 지점

① 별칭 클러스터링 — 핵심

:sameActorAs a owl:SymmetricProperty, owl:TransitiveProperty .

엔티티 해소는 쌍 단위 판단만 내놓습니다. bsmith@enron.com ↔ bob.smith@enron.com, bob.smith@enron.com ↔ robert.smith@ect.enron.com. 이걸 사람 단위 클러스터로 묶는 건 원래 union-find를 짜야 하는 일인데, 대칭·이행 선언 두 줄이면 추론기가 폐포를 계산해줍니다. 규칙이 늘어나도 코드는 그대로입니다.

② 하위 속성으로 질의 단순화

:sentTo  rdfs:subPropertyOf :addressed .
:ccTo    rdfs:subPropertyOf :addressed .
:bccTo   rdfs:subPropertyOf :addressed .

"A가 B에게 보낸 모든 것"을 물을 때 세 속성을 OR로 묶지 않습니다. :replyTo가 추가돼도 질의는 그대로입니다.

③ 하위 클래스로 역할 계층:Executive ⊑ :Employee. "임원과 소통한 사람"에 CEO·CFO·VP가 자동 포함됩니다. :Counsel은 나중에 특권 문서 탐지로 확장할 때 재사용됩니다.

④ domain/range로 오류 검출:sentMessage의 domain이 :Person인데 :Organization이 들어오면 파이프라인 버그입니다. 일관성 검사가 무료 테스트가 됩니다.

핵심 클래스

클래스의미
:Actor원본에서 관찰된 신원 표현 — 이메일 주소 1개 + 표시이름들
:Person해소된 실제 사람. 여러 Actor가 하나로
:Organization조직·부서
:Message개별 이메일
:ThreadIn-Reply-To/References로 묶인 대화
:RoleExecutive / Trader / Counsel 등

ActorPerson을 분리하는 게 중요합니다. 원본에 있던 것(관찰)과 우리가 판단한 것(추론)을 섞으면, 나중에 "왜 이 둘을 같은 사람으로 봤나"에 답할 수 없습니다.

해소 4단계

  1. 정규화 — 주소 소문자화, 서브도메인 통합(@ect.enron.com → @enron.com), 표시이름을 성, 이름으로
  2. 강한 증거 → 즉시 병합 — 동일 주소, 또는 표시이름 완전일치 + 동일 도메인
  3. 약한 증거 → 점수화 — 이름 유사도(Jaro-Winkler), 로컬파트↔이름 매칭(bsmith ↔ Bob Smith), 그리고 공통 연락처 중복도 — 같은 사람들과 소통하면 동일인 확률이 오릅니다. 그래프 구조를 해소에 되먹이는 부분이고, 순수 문자열 매칭 대비 개선을 보여줄 지점입니다
  4. 임계값 초과 → :sameActorAs 생성 → 추론기가 클러스터로 닫음

근거 보존 — 법률 도메인에서는 필수

:merge_0142 a prov:Activity ;
    prov:used   :actor_bsmith, :actor_robert_smith ;
    :rule       "localpart_name_match" ;
    :confidence 0.87 .

실무자가 "이 두 계정을 왜 동일인으로 묶었냐"고 물으면 답할 수 있어야 합니다. 답 못 하는 자동화는 법률 영역에서 안 쓰입니다. 그리고 이 기록이 있으면 신뢰도 낮은 병합만 골라 사람이 검토하는 화면을 만들 수 있습니다.

정확도 확인

Enron 커스토디언 약 150명에 대해 수작업 라벨링한 골드셋 몇백 쌍으로 precision/recall을 측정합니다. 규칙을 고칠 때마다 이 숫자가 오르는지 보면서 진행합니다. 감으로 튜닝하면 반드시 과적합됩니다.

03 탐색 UI 논의 중

2만 노드를 그대로 그리면 아무것도 읽을 수 없는 털뭉치가 됩니다. 이 문제를 푸는 방식이 곧 제품의 성격을 결정합니다.

++

A. 앵커 + 점진 확장

인물을 검색해 하나 고르면 그 사람의 이웃만. 클릭할 때마다 한 겹씩 자람. 화면엔 사용자가 직접 펼친 것만.

TradingLegalExec

B. 조직 집약 → 드릴다운

부서 덩어리 10~20개로 시작. 선 굵기가 통신량. 전체 구도를 먼저 잡고 조직을 열어 개인으로.

C. 리스트 주도 + 그래프 보조

왼쪽 검색·필터·인물 랭킹이 주인공. 오른쪽 그래프는 근거를 확인하는 창.

갈림길: 실무자는 찾을 사람을 이미 알고 시작하나요, 아니면 누가 중요한지 모르는 상태로 시작하나요? 소송 초기(ECA)엔 후자라 B가 강하고, 특정 인물을 파고들 땐 A가 맞습니다. B로 구도 잡고 A로 파고드는 조합이 자연스럽지만, v1에 둘 다 넣으면 범위가 두 배가 됩니다.

04 남은 논의