설계 문서 · 구현 진행 중
eDiscovery 커뮤니케이션 관계망을 온톨로지 기반 지식그래프로 재구성하고, 실무자가 탐색할 수 있게 만드는 도구.
브레인스토밍에서 결정된 전제. 뒤집으려면 아래 설계도 함께 바뀝니다.
| 항목 | 결정 |
|---|---|
| 목적 | 실무자가 실제로 쓰는 제품. 논문용 벤치마크가 아님 |
| 공략 지점 | 인물·조직 관계망 분석 — 엔티티 해소와 커뮤니케이션 네트워크 |
| 입력 데이터 | Enron 공개 코퍼스로 시작. 실데이터 연동은 이후 |
| 핵심 동작 | 그래프 시각적 탐색 |
| 온톨로지 수준 | 하이브리드 — RDF/OWL로 정의, property graph로 투영 |
| v1 범위 | 수직 슬라이스. 커스토디언 상위 N명을 끝까지 관통 — N은 아래 실측을 보고 확정 중 |
설계 단계의 추정치를 실제 데이터로 대체한 결과. 하나는 크게 틀렸습니다.
| 항목 | 값 |
|---|---|
| 배포본 | 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 성능 측정 결과를 보고 정합니다 —
추론기가 감당하는 규모가 상한이 되기 때문입니다.
| # | 커스토디언 | 파일 |
|---|---|---|
| 1 | kaminski-v | 28,465 |
| 2 | dasovich-j | 28,234 |
| 3 | kean-s | 25,351 |
| 4 | mann-k | 23,381 |
| 5 | jones-t | 19,950 |
| 6 | shackleton-s | 18,687 |
| 7 | taylor-m | 13,875 |
| 8 | farmer-d | 13,032 |
| 9 | germany-c | 12,436 |
| 10 | beck-s | 11,830 |
| 11 | symes-k | 10,827 |
| 12 | nemec-g | 10,655 |
| 13 | scott-s | 8,022 |
| 14 | rogers-b | 8,009 |
| 15 | bass-e | 7,823 |
상위권이 임원이 아니라 리서치·대외협력·법무 담당이라는 점은 짚어둘 만합니다. 메일 수가 많은 사서함과 사건의 중심 인물은 다릅니다. 관계망 탐색 데모로는 문제없지만, "핵심 인물 자동 랭킹" 같은 기능을 나중에 붙일 때는 통신량만으로 중요도를 매길 수 없다는 근거가 됩니다.
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는 그 결과를 탐색용으로 투영한 것입니다. 단방향이라 두 모델을 양방향 동기화하는 문제가 생기지 않습니다.
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. 빌드 도구도 없습니다. 필요해지면 그때 올립니다.
"온톨로지 썼습니다"가 장식이 되지 않으려면, 추론기가 없으면 직접 짜야 했을 코드를 대체해야 합니다.
① 별칭 클러스터링 — 핵심
: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 | 개별 이메일 |
:Thread | In-Reply-To/References로 묶인 대화 |
:Role | Executive / Trader / Counsel 등 |
Actor와 Person을 분리하는 게 중요합니다. 원본에 있던 것(관찰)과
우리가 판단한 것(추론)을 섞으면, 나중에 "왜 이 둘을 같은 사람으로 봤나"에 답할 수 없습니다.
커스토디언 15명이면 actor 가 만 단위로 나옵니다. 모든 쌍을 비교하면 억 단위이고 쌍마다 SQL 을 때리므로 파이프라인이 끝나지 않습니다. 같은 블록에 든 쌍만 비교합니다.
블록 키는 표시이름의 성, 로컬파트 토큰, 그리고 로컬파트에서 첫 글자를 뗀
형태입니다. 마지막 것이 핵심입니다 — jskilling 이
skilling 블록에 들어가서 표시이름 "Skilling, Jeff" 를
가진 jeff.skilling 과 만납니다. 60명을 넘는 블록은 버립니다
(enron 같은 토큰이 만드는 블록은 아무것도 구별해주지 않습니다).
@ect.enron.com → @enron.com), 표시이름을 성, 이름으로bsmith ↔ Bob Smith),
그리고 공통 연락처 중복도 — 같은 사람들과 소통하면 동일인 확률이 오릅니다.
그래프 구조를 해소에 되먹이는 부분이고, 순수 문자열 매칭 대비 개선을 보여줄 지점입니다:sameActorAs 생성 → 추론기가 클러스터로 닫음:merge_0142 a prov:Activity ;
prov:used :actor_bsmith, :actor_robert_smith ;
:rule "localpart_name_match" ;
:confidence 0.87 .
실무자가 "이 두 계정을 왜 동일인으로 묶었냐"고 물으면 답할 수 있어야 합니다. 답 못 하는 자동화는 법률 영역에서 안 쓰입니다. 그리고 이 기록이 있으면 신뢰도 낮은 병합만 골라 사람이 검토하는 화면을 만들 수 있습니다.
Enron 커스토디언 약 150명에 대해 수작업 라벨링한 골드셋 몇백 쌍으로 precision/recall을 측정합니다. 규칙을 고칠 때마다 이 숫자가 오르는지 보면서 진행합니다. 감으로 튜닝하면 반드시 과적합됩니다.
노드를 그대로 다 그리면 아무것도 읽을 수 없는 털뭉치가 됩니다. 세 안을 놓고 A(앵커 + 점진 확장) 로 정했습니다.
인물을 검색해 하나 고르면 그 사람의 이웃만. 클릭할 때마다 한 겹씩 자람. 화면엔 사용자가 직접 펼친 것만.
부서 덩어리 10~20개로 시작. 선 굵기가 통신량. 전체 구도를 먼저 잡고 조직을 열어 개인으로.
왼쪽 검색·필터·인물 랭킹이 주인공. 오른쪽 그래프는 근거를 확인하는 창.
A를 고른 이유: 엔티티 해소의 가치가 화면에 바로 드러나고, B는 조직 소속 추론이 선행돼야 해서 v1 범위가 커지며, A가 가장 작습니다(API 2개). 다만 "실무자가 찾을 사람을 이미 알고 시작한다"는 전제는 검증되지 않은 추측입니다. 실사용 피드백에서 뒤집히면 B를 재검토합니다.
① 이웃을 무엇으로 자를 것인가 — 1-hop 이웃이 수백 명일 때 상위 15명만
보여주는데, 기준이 단순 메시지 수면 안 됩니다. Enron 에서 그렇게 하면
상위권이 메일링리스트·자동알림·뉴스레터로 채워집니다. 기본값은
양방향 가중치 min(A→B, B→A) — 한쪽으로만 쏟아지는
브로드캐스트는 자동으로 밀려나고 실제로 주고받은 관계가 올라옵니다.
② 확장 폭발 방지 — 노드 상한 150개. 초과하면 확장을 막고 접기를 안내합니다. 확장 취소(undo)를 제공하고, 자동으로 접지 않습니다 — 무엇이 사라졌는지 모르면 혼란스럽습니다.
③ 비교 장치를 UI에 내장 — 우측 상단 온톨로지 토글.
ON 이면 Person 단위(별칭 통합), OFF 면 Actor 단위(이메일 주소
그대로). 같은 인물을 검색한 채로 토글하면 노드가 합쳐지고 엣지가 굵어지는 게 눈앞에서
일어납니다. 성능 향상을 말로 설명할 필요가 없어지고, 동시에
엔티티 해소가 틀렸을 때 바로 보이는 디버깅 도구가 됩니다.
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 단계에서 미리 계산해두므로 요청 시엔 조회만 합니다.
태스크마다 새 에이전트가 구현하고, 별도 리뷰어가 diff 를 검증합니다. 리뷰가 통과할 때까지 다음 태스크로 넘어가지 않습니다.
| # | 태스크 | 상태 | 비고 |
|---|---|---|---|
| 1 | 프로젝트 뼈대 · 경로 | ✅ 완료 | 수정 1라운드 |
| 2 | maildir 파싱 | ✅ 완료 | 수정 1라운드 · Critical 2건 |
| 3 | 정규화 · Actor 추출 | 🔄 수정 중 | Critical 1건 · Important 1건 |
| 4 | 엔티티 해소 | 대기 | |
| 5 | 온톨로지 · 추론 | 대기 | 설계 재검토 필요 (아래 P2) |
| 6 | property graph 투영 | 대기 | |
| 7 | API | 대기 | |
| 8 | 실데이터 파이프라인 | 대기 | 코퍼스 준비 완료 |
| 9 | 탐색 UI | 대기 | |
| 10 | 문서 갱신 · 배포 | 대기 |
현재 테스트 19개 통과 (경로 3 · 인제스트 7 · 정규화 6 · 추출 3). 코퍼스는 다운로드·압축 해제·커스토디언 순위 계산까지 끝나 Task 8 이 준비된 상태입니다.
구현을 시작한 뒤 드러난 결함들. 대부분 계획 문서 자체의 오류였고, 구현자가 계획을 충실히 옮긴 결과 그대로 코드에 들어왔습니다. 각각 판정하고 계획을 고쳤습니다.
localpart_name_match 규칙이 실제로 검증되게 함import email 만으로는 email.message 가
바인딩되지 않음. pytest 가 부수효과로 끌어와서 테스트는 통과했지만,
Task 8 의 python -m edisco.cli 는 로직 실행 전에 죽었을 것FileNotFoundError → 관례를 버림
eDiscovery 에서 custody 는 증거입니다. 누가 그 문서를 보관하고
있었는지는 사건에 관한 사실이고, 한 번 버리면 복원할 수 없습니다.
message_custodian 테이블을 추가했습니다 —
지금 넣는 비용은 테이블 하나와 삽입 한 줄이고,
Task 8 이 돈 뒤에 넣는 비용은 코퍼스 전체 재처리입니다.
name_key 가 중간 이름에서 두 형식이 어긋남.
"Mary Jo Watkins" → watkins, mary 이지만
"Watkins, Mary Jo" → watkins, mary jo.
중간 이름은 헤더마다 있다 없다 하므로, 실제 코퍼스에서 같은 사람이 두 키로 갈라짐.
Task 4 가 전적으로 이 불변식에 기댐 → 이름은 첫 토큰만 쓰도록 통일notenron.com 이 enron.com 으로 접힘.
endswith("enron.com") 에 점 경계 검사가 없었음.
주소가 곧 신원인 단계에서 외부인을 Enron 네임스페이스로 편입시키는 provenance 결함owlrl 이 메시지 그래프를 감당하지 못할 가능성.
Task 5 는 스키마 + 전체 인스턴스 트리플에 DeductiveClosure 를
돌리도록 되어 있습니다. owlrl 은 순수 Python 고정점 계산이고,
메시지당 트리플 4~5개면 24만 메시지에서 100만 트리플이 넘습니다.
정작 필요한 추론(sameActorAs 폐포)은 actor 노드만 건드립니다.
현재 성능 측정 진행 중 — 확인되면 추론 범위를 actor 서브그래프로 좁힙니다개발 머신이 기관 프록시 뒤에 있어 도구가 기본 설정으로 동작하지 않습니다.
| 증상 | 대응 |
|---|---|
pip 가 SSLCertVerificationError 로 죽음 |
--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 로 지정하는 편이 낫습니다.
owlrl 성능 측정 결과가 상한을 정합니다