설계 문서 · 구현 진행 중

edisco-graph

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

최종 수정 2026-09-07 코퍼스 Enron · 150 커스토디언 · 517,401 파일 스택 Python · RDF/OWL · Cytoscape.js 진행 10개 중 3개

00 확정된 것

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

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

01 코퍼스 실측 측정 완료

설계 단계의 추정치를 실제 데이터로 대체한 결과. 하나는 크게 틀렸습니다.

항목
배포본enron_mail_20150507.tar.gz · 443,254,787 bytes
커스토디언150명
전체 파일517,401
상위 15명 합계240,577 (전체의 46.5%)

스펙의 v1 규모 추정이 5배 틀렸습니다. 설계 문서에 "커스토디언 15~20명 ≈ 5만 통"이라 적었지만, 실제 상위 15명은 24만 파일입니다. 검증 없이 넣은 숫자였습니다. 상위 5명이 약 12.5만, 상위 3명이 약 8.2만, 상위 2명이 약 5.7만입니다. 최종 N은 아래 owlrl 성능 측정 결과를 보고 정합니다 — 추론기가 감당하는 규모가 상한이 되기 때문입니다.

파일 수 상위 15 커스토디언

#커스토디언파일
1kaminski-v28,465
2dasovich-j28,234
3kean-s25,351
4mann-k23,381
5jones-t19,950
6shackleton-s18,687
7taylor-m13,875
8farmer-d13,032
9germany-c12,436
10beck-s11,830
11symes-k10,827
12nemec-g10,655
13scott-s8,022
14rogers-b8,009
15bass-e7,823

상위권이 임원이 아니라 리서치·대외협력·법무 담당이라는 점은 짚어둘 만합니다. 메일 수가 많은 사서함과 사건의 중심 인물은 다릅니다. 관계망 탐색 데모로는 문제없지만, "핵심 인물 자동 랭킹" 같은 기능을 나중에 붙일 때는 통신량만으로 중요도를 매길 수 없다는 근거가 됩니다.

02 아키텍처 확정

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. 빌드 도구도 없습니다. 필요해지면 그때 올립니다.

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

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

추론이 밥값을 하는 네 지점

① 별칭 클러스터링 — 핵심

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

블로킹 — 전수 비교는 돌지 않는다

커스토디언 15명이면 actor 가 만 단위로 나옵니다. 모든 쌍을 비교하면 억 단위이고 쌍마다 SQL 을 때리므로 파이프라인이 끝나지 않습니다. 같은 블록에 든 쌍만 비교합니다.

블록 키는 표시이름의 성, 로컬파트 토큰, 그리고 로컬파트에서 첫 글자를 뗀 형태입니다. 마지막 것이 핵심입니다 — jskillingskilling 블록에 들어가서 표시이름 "Skilling, Jeff" 를 가진 jeff.skilling 과 만납니다. 60명을 넘는 블록은 버립니다 (enron 같은 토큰이 만드는 블록은 아무것도 구별해주지 않습니다).

해소 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을 측정합니다. 규칙을 고칠 때마다 이 숫자가 오르는지 보면서 진행합니다. 감으로 튜닝하면 반드시 과적합됩니다.

04 탐색 UI A 확정

노드를 그대로 다 그리면 아무것도 읽을 수 없는 털뭉치가 됩니다. 세 안을 놓고 A(앵커 + 점진 확장) 로 정했습니다.

++

A. 앵커 + 점진 확장  채택

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

TradingLegalExec

B. 조직 집약 → 드릴다운

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

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

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

A를 고른 이유: 엔티티 해소의 가치가 화면에 바로 드러나고, B는 조직 소속 추론이 선행돼야 해서 v1 범위가 커지며, A가 가장 작습니다(API 2개). 다만 "실무자가 찾을 사람을 이미 알고 시작한다"는 전제는 검증되지 않은 추측입니다. 실사용 피드백에서 뒤집히면 B를 재검토합니다.

A안 상세 — 세 가지 설계 결정

① 이웃을 무엇으로 자를 것인가 — 1-hop 이웃이 수백 명일 때 상위 15명만 보여주는데, 기준이 단순 메시지 수면 안 됩니다. Enron 에서 그렇게 하면 상위권이 메일링리스트·자동알림·뉴스레터로 채워집니다. 기본값은 양방향 가중치 min(A→B, B→A) — 한쪽으로만 쏟아지는 브로드캐스트는 자동으로 밀려나고 실제로 주고받은 관계가 올라옵니다.

② 확장 폭발 방지 — 노드 상한 150개. 초과하면 확장을 막고 접기를 안내합니다. 확장 취소(undo)를 제공하고, 자동으로 접지 않습니다 — 무엇이 사라졌는지 모르면 혼란스럽습니다.

③ 비교 장치를 UI에 내장 — 우측 상단 온톨로지 토글. ON 이면 Person 단위(별칭 통합), OFF 면 Actor 단위(이메일 주소 그대로). 같은 인물을 검색한 채로 토글하면 노드가 합쳐지고 엣지가 굵어지는 게 눈앞에서 일어납니다. 성능 향상을 말로 설명할 필요가 없어지고, 동시에 엔티티 해소가 틀렸을 때 바로 보이는 디버깅 도구가 됩니다.

API 두 개

GET /api/search?q=skilling
    → [{ person_id, display_name, alias_count, msg_count }, …]

GET /api/ego?person=P0042&limit=15&resolved=true
    → { nodes: [...], edges: [...] }

전체 그래프는 클라이언트로 보내지 않습니다. 각 Person 의 상위 이웃 리스트는 [5] project 단계에서 미리 계산해두므로 요청 시엔 조회만 합니다.

05 구현 진행 상황

태스크마다 새 에이전트가 구현하고, 별도 리뷰어가 diff 를 검증합니다. 리뷰가 통과할 때까지 다음 태스크로 넘어가지 않습니다.

#태스크상태비고
1프로젝트 뼈대 · 경로✅ 완료수정 1라운드
2maildir 파싱✅ 완료수정 1라운드 · Critical 2건
3정규화 · Actor 추출🔄 수정 중Critical 1건 · Important 1건
4엔티티 해소대기
5온톨로지 · 추론대기설계 재검토 필요 (아래 P2)
6property graph 투영대기
7API대기
8실데이터 파이프라인대기코퍼스 준비 완료
9탐색 UI대기
10문서 갱신 · 배포대기

현재 테스트 19개 통과 (경로 3 · 인제스트 7 · 정규화 6 · 추출 3). 코퍼스는 다운로드·압축 해제·커스토디언 순위 계산까지 끝나 Task 8 이 준비된 상태입니다.

06 리뷰가 찾은 것 기록

구현을 시작한 뒤 드러난 결함들. 대부분 계획 문서 자체의 오류였고, 구현자가 계획을 충실히 옮긴 결과 그대로 코드에 들어왔습니다. 각각 판정하고 계획을 고쳤습니다.

사전 점검에서 (구현 시작 전)

Task 2 리뷰에서

eDiscovery 에서 custody 는 증거입니다. 누가 그 문서를 보관하고 있었는지는 사건에 관한 사실이고, 한 번 버리면 복원할 수 없습니다. message_custodian 테이블을 추가했습니다 — 지금 넣는 비용은 테이블 하나와 삽입 한 줄이고, Task 8 이 돈 뒤에 넣는 비용은 코퍼스 전체 재처리입니다.

Task 3 리뷰에서

실측이 뒤집은 것

07 환경 제약

개발 머신이 기관 프록시 뒤에 있어 도구가 기본 설정으로 동작하지 않습니다.

증상대응
pipSSLCertVerificationError 로 죽음 --trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.python.org
curl 이 exit 35 (SSL) 로 실패 PowerShell Invoke-WebRequest — Windows 인증서 저장소를 씀
파일명 끝 마침표를 만들 수 없음관례를 버림 (위 06 참조)

프록시가 TLS 를 가로채기 때문입니다. CI 로 옮기면 이 우회는 불필요해집니다. 매번 플래그를 붙이는 대신 pip.ini 에 기록하거나 기관 CA 인증서를 PIP_CERT 로 지정하는 편이 낫습니다.

08 남은 논의