Architektura platformy e-Discovery: pozyskiwanie, przetwarzanie i przegląd AI
W 2024 r. przyjęcie sztucznej inteligencji na platformach e-odkrycie legalne i wzrósł z 19% do 79% w ciągu zaledwie jednego roku. Dane te nie są ciekawostką statystyczną: odzwierciedlają ją transformacja strukturalna w sposobie zarządzania kancelariami prawnymi, spółkami i organami sądowymi dowody z dokumentów w postępowaniu cywilnym i karnym. Kiedy pozew obejmuje miliony e-maili i wiadomości Slack, dokumenty SharePoint i pliki dziennika, tradycyjny model ręcznego przeglądania przez asystent prawny nie jest już zrównoważony ekonomicznie ani logistycznie.
L'e-odkrycie (Electronic Discovery) oraz proces, w którym uczestniczą strony postępowania legalnie identyfikować, gromadzić, konserwować, przetwarzać, przeglądać i sporządzać dowody w postaci dokumentów formacie elektronicznym. W Stanach Zjednoczonych i regulowane przez Federalne zasady postępowania cywilnego (FRCP) w Europie z podobnych ram na poziomie krajowym. Nowoczesne platformy muszą obsługiwać petabajty danych, szanować prawnie obowiązujące łańcuchy dostaw i ograniczać liczbę dokumentów do przeglądu utrzymanie bardzo wysokiego poziomu zapamiętywania: żaden istotny dokument nie może zostać utracony.
W tym artykule budujemy kompletną architekturę platformy e-Discovery klasy korporacyjnej: od masowe spożycie heterogenicznych dokumentów przetwarzanie rozproszone, z deduplikacja al kodowanie predykcyjne z modelami AI, w górę doeksport w formacie EDRM XML. Wszystko z prawdziwymi przykładami kodu Python i analizą z wiodących platform na rynku.
Czego dowiesz się w tym artykule
- Model EDRM (Electronic Discovery Reference Model) i jego fazy
- Architektura mikrousług do masowego pozyskiwania: Kafka, Elasticsearch, MinIO
- Potok przetwarzania: ekstrakcja tekstu, metadane, deduplikacja MD5/SHA i prawie deduplikacja
- Przegląd wspomagany technologią (TAR) i ciągłe aktywne uczenie się (CAL)
- Kodowanie predykcyjne za pomocą scikit-learn i transformatorów zdań
- Zarządzanie łańcuchem dostaw i niezmienna ścieżka audytu
- Porównanie platform: Relativity, DISCO, Everlaw, Logikcull
- Eksport EDRM XML i integracja z systemami zarządzania sprawami
Pozycja w serii LegalTech i AI
| # | Przedmiot | Centrum |
|---|---|---|
| 1 | NLP w analizie kontraktów | OCR, NER, klasyfikacja klauzul |
| 2 | Jesteś tutaj — Architektura e-Discovery | Pozyskiwanie, przetwarzanie, przegląd AI |
| 3 | Automatyzacja zgodności | Silnik reguł i RegTech |
| 4 | Inteligentne kontrakty | Solidność, Vyper, wykonalność |
| 5 | Podsumowanie z generatywną sztuczną inteligencją | LLM, RAG, walidacja wyników |
| 6 | Wyszukiwarka orzecznictwa | Osadzanie wektorów i wyszukiwanie semantyczne |
| 7 | Podpis cyfrowy i eIDAS 2.0 | PKI, znaczniki czasu, przepływ pracy |
| 8 | Systemy zgodności z RODO | Prywatność w fazie projektowania, DSR, mapowanie danych |
| 9 | Legalny drugi pilot AI | RAG na korpusie prawnym, poręczach |
| 10 | Integracja danych LegalTech | ECLI, systemy sądowe API, XBRL |
Model EDRM: Ramy odniesienia dla e-Discovery
L'Model referencyjny elektronicznego wykrywania (EDRM) i de facto standardem opisującym fazy procesu odkrywania elektronicznego. Model opracowany w 2005 roku i stale aktualizowany definiuje dziewięć kolejnych faz, które musi obsługiwać każda platforma korporacyjna.
| Faza EDRM | Opis | Komponent techniczny |
|---|---|---|
| 1. Zarządzanie informacjami | Zasady przechowywania, klasyfikacja danych, mapa danych | MDM, silnik polityki, CMDB |
| 2. Identyfikacja | Zlokalizuj potencjalnych opiekunów i odpowiednie źródła danych | Robot indeksujący, skanowanie katalogów, zapytanie LDAP |
| 3. Konserwacja | Zabezpieczenie prawne: zamroź dane, aby uniknąć kradzieży | Zarządzanie wstrzymaniami, niezmienna pamięć, przepływ powiadomień |
| 4. Kolekcja | Kolekcja kryminalistyczna z łańcuchem dostaw | Kolektor kryminalistyczny, weryfikacja skrótu, dziennik aresztowania |
| 5.Przetwarzanie | Ekstrakcja tekstu, metadane, deduplikacja, filtrowanie NIST | Apache Tika, potok pozyskiwania Elasticsearch |
| 6.Przejrzyj | Klasyfikacja istotności/przywilejów, TAR/CAL | Kodowanie predykcyjne, aktywne uczenie się, platforma recenzyjna |
| 7. Analiza | Wzór, oś czasu, sieć podmiotów, modelowanie tematyczne | Analityka grafowa, NLP, LDA/BERTopic |
| 8. Produkcja | Eksport w uzgodnionym formacie (TIFF, natywny, PDF) | Eksport EDRM XML, numeracja Batesa, silnik redakcyjny |
| 9. Prezentacja | Prezentacje procesowe, zeznania, wizualne osie czasu | Dyrektor próbny, zarządzający wystawą |
Architektura mikrousług dla e-Discovery w przedsiębiorstwie
Nowoczesna platforma e-Discovery nie może być monolitem. Ilości danych wahają się od kilku gigabajtów do setek terabajtów w przypadku dużych celów. Architektura musi być elastycznie skalowalne, odporne na błędy i zapewniające niezmienne ścieżki audytu spełniają wymogi kwalifikowalności materiału dowodowego. Skonsolidowany wzór architektoniczny łączy strumieniowanie zdarzeń, przechowywanie obiektów i rozproszona wyszukiwarka.
# docker-compose.yml per ambiente e-Discovery locale
version: '3.9'
services:
# Message broker per ingestion asincrona
kafka:
image: confluentinc/cp-kafka:7.5.0
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
KAFKA_NUM_PARTITIONS: 12
depends_on: [zookeeper]
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
# Object storage per documenti originali e derivati
minio:
image: minio/minio:RELEASE.2024-01-16T16-07-38Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ediscovery
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
volumes:
- minio_data:/data
# Search engine per full-text e metadata search
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
environment:
- discovery.type=single-node
- xpack.security.enabled=true
- ELASTIC_PASSWORD=${ES_PASSWORD}
- ES_JAVA_OPTS=-Xms2g -Xmx2g
volumes:
- es_data:/usr/share/elasticsearch/data
# Worker per processing documenti
tika:
image: apache/tika:2.9.1-full
ports:
- "9998:9998"
# Database relazionale per metadata e catena custodia
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ediscovery
POSTGRES_USER: ediscovery
POSTGRES_PASSWORD: ${PG_PASSWORD}
volumes:
minio_data:
es_data:
Ogromny potok przyjmowania dokumentów
Przyjmowanie jest najbardziej krytyczną fazą: musi gromadzić dokumenty z heterogenicznych źródeł (serwer poczty e-mail, pliki udostępnianie, przechowywanie w chmurze, aplikacje SaaS) przy jednoczesnym zachowaniu integralności kryminalistycznej. Każdy zdobyty dokument musi mieć weryfikowalny skrót kryptograficzny oraz niezmienny zapis tego, kiedy i jak oraz zostały zebrane.
"""
ediscovery/ingestion/collector.py
Collettore forense con chain of custody
"""
import hashlib
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
import boto3
from kafka import KafkaProducer
import psycopg2
class ForensicCollector:
"""
Raccoglie documenti con hash SHA-256 e registra
la catena di custodia in PostgreSQL.
"""
def __init__(self, config: dict):
self.minio = boto3.client(
's3',
endpoint_url=config['minio_url'],
aws_access_key_id=config['minio_user'],
aws_secret_access_key=config['minio_password']
)
self.producer = KafkaProducer(
bootstrap_servers=config['kafka_brokers'],
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
self.pg_conn = psycopg2.connect(config['postgres_dsn'])
self.bucket = 'ediscovery-originals'
def collect_file(
self,
file_path: Path,
matter_id: str,
custodian_id: str,
collector_id: str
) -> dict:
"""
Raccoglie un file con verifica integrita e registra custody event.
Restituisce il documento event per il topic Kafka.
"""
# 1. Calcola hash SHA-256 prima del trasferimento
sha256 = self._compute_sha256(file_path)
md5 = self._compute_md5(file_path)
file_size = file_path.stat().st_size
# 2. Genera Document ID univoco
doc_id = str(uuid.uuid4())
# 3. Upload su MinIO con metadata
s3_key = f"matters/{matter_id}/originals/{doc_id}/{file_path.name}"
self.minio.upload_file(
str(file_path),
self.bucket,
s3_key,
ExtraArgs={
'Metadata': {
'doc-id': doc_id,
'sha256': sha256,
'custodian-id': custodian_id,
'matter-id': matter_id
}
}
)
# 4. Verifica integrita post-upload
response = self.minio.head_object(Bucket=self.bucket, Key=s3_key)
uploaded_size = response['ContentLength']
if uploaded_size != file_size:
raise ValueError(
f"Integrita compromessa: atteso {file_size} bytes, "
f"caricato {uploaded_size} bytes"
)
# 5. Registra custody event in PostgreSQL
collection_timestamp = datetime.now(timezone.utc).isoformat()
custody_event = {
'event_id': str(uuid.uuid4()),
'doc_id': doc_id,
'matter_id': matter_id,
'custodian_id': custodian_id,
'collector_id': collector_id,
'event_type': 'COLLECTION',
'timestamp': collection_timestamp,
'source_path': str(file_path),
'sha256': sha256,
'md5': md5,
'file_size': file_size,
's3_key': s3_key
}
self._record_custody_event(custody_event)
# 6. Pubblica su Kafka per processing asincrono
document_event = {
'doc_id': doc_id,
'matter_id': matter_id,
'custodian_id': custodian_id,
's3_key': s3_key,
'filename': file_path.name,
'file_size': file_size,
'sha256': sha256,
'collection_timestamp': collection_timestamp,
'status': 'COLLECTED'
}
self.producer.send('ediscovery.documents.collected', document_event)
return document_event
def _compute_sha256(self, file_path: Path) -> str:
h = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(65536), b''):
h.update(chunk)
return h.hexdigest()
def _compute_md5(self, file_path: Path) -> str:
h = hashlib.md5()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(65536), b''):
h.update(chunk)
return h.hexdigest()
def _record_custody_event(self, event: dict) -> None:
with self.pg_conn.cursor() as cur:
cur.execute("""
INSERT INTO custody_events (
event_id, doc_id, matter_id, custodian_id,
collector_id, event_type, timestamp,
source_path, sha256, md5, file_size, s3_key
) VALUES (
%(event_id)s, %(doc_id)s, %(matter_id)s, %(custodian_id)s,
%(collector_id)s, %(event_type)s, %(timestamp)s,
%(source_path)s, %(sha256)s, %(md5)s, %(file_size)s, %(s3_key)s
)
""", event)
self.pg_conn.commit()
Przetwarzanie i ekstrakcja treści za pomocą Apache Tika
Przetwarzanie i serce techniczne rurociągu. Każdy dokument musi zostać przekonwertowany na tekst z możliwością wyszukiwania, ekstrakcją metadanych (autor, daty, wątek wiadomości e-mail, właściwości dokumentu) i znormalizowanymi we wspólnym schemacie. Apache Tika zarządza ponad 1500 różnymi formatami plików, co czyni go de facto standardem ekstrakcji treści w e-Discovery.
"""
ediscovery/processing/processor.py
Worker di processing documenti con Apache Tika
"""
import json
import requests
from kafka import KafkaConsumer, KafkaProducer
from elasticsearch import Elasticsearch
import boto3
class DocumentProcessor:
"""
Consumer Kafka che processa ogni documento raccolto:
estrae testo e metadata con Tika, indicizza su ES.
"""
TIKA_URL = "http://tika:9998"
def __init__(self, config: dict):
self.consumer = KafkaConsumer(
'ediscovery.documents.collected',
bootstrap_servers=config['kafka_brokers'],
group_id='document-processor',
value_deserializer=lambda v: json.loads(v.decode('utf-8'))
)
self.producer = KafkaProducer(
bootstrap_servers=config['kafka_brokers'],
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
self.es = Elasticsearch(
config['elasticsearch_url'],
basic_auth=('elastic', config['es_password'])
)
self.minio = boto3.client('s3', endpoint_url=config['minio_url'])
def process_documents(self) -> None:
for message in self.consumer:
event = message.value
try:
processed = self._process_document(event)
self._index_document(processed)
self.producer.send(
'ediscovery.documents.processed',
{**event, **processed, 'status': 'PROCESSED'}
)
except Exception as exc:
self.producer.send(
'ediscovery.documents.errors',
{**event, 'error': str(exc), 'status': 'ERROR'}
)
def _process_document(self, event: dict) -> dict:
# Scarica documento da MinIO
response = self.minio.get_object(
Bucket='ediscovery-originals',
Key=event['s3_key']
)
file_content = response['Body'].read()
# Estrai testo con Tika (PUT /tika)
tika_response = requests.put(
f"{self.TIKA_URL}/tika",
data=file_content,
headers={
'Accept': 'text/plain',
'Content-Type': 'application/octet-stream'
},
timeout=120
)
extracted_text = tika_response.text
# Estrai metadata con Tika (PUT /meta)
meta_response = requests.put(
f"{self.TIKA_URL}/meta",
data=file_content,
headers={
'Accept': 'application/json',
'Content-Type': 'application/octet-stream'
},
timeout=60
)
metadata = meta_response.json()
return {
'extracted_text': extracted_text,
'text_length': len(extracted_text),
'tika_metadata': metadata,
'author': metadata.get('dc:creator', ''),
'created_date': metadata.get('dcterms:created', ''),
'modified_date': metadata.get('dcterms:modified', ''),
'content_type': metadata.get('Content-Type', ''),
'language': metadata.get('language', ''),
'page_count': metadata.get('xmpTPg:NPages', 0)
}
def _index_document(self, doc: dict) -> None:
"""Indicizza documento su Elasticsearch per ricerca full-text."""
self.es.index(
index=f"ediscovery-{doc['matter_id']}",
id=doc['doc_id'],
document={
'doc_id': doc['doc_id'],
'matter_id': doc['matter_id'],
'custodian_id': doc['custodian_id'],
'filename': doc['filename'],
'content': doc['extracted_text'],
'author': doc.get('author', ''),
'created_date': doc.get('created_date'),
'modified_date': doc.get('modified_date'),
'content_type': doc.get('content_type', ''),
'language': doc.get('language', ''),
'page_count': doc.get('page_count', 0),
'file_size': doc['file_size'],
'sha256': doc['sha256'],
'collection_timestamp': doc['collection_timestamp'],
'status': 'PROCESSED',
'review_status': 'UNREVIEWED',
'relevance_score': None,
'privilege': False,
'tags': []
}
)
Deduplikacja: dokładne i prawie duplikaty wykrywania
W typowym zbiorze e-Discovery 40–70% dokumentów to duplikaty lub prawie duplikaty. Tam deduplikacja istotne jest nie tylko zmniejszenie kosztów przeglądu, ale jest to: wymóg prawny: tworzenie tysięcy identycznych kopii tego samego e-maila narusza zasady wykrycia i zwiększa koszty kontrahenta. Istnieją dwa poziomy deduplikacji:
Poziomy deduplikacji w e-Discovery
- Dokładne deduplikowanie (oparte na skrótach): Dokumenty z identycznym SHA-256 są dokładnymi duplikatami. Zachowujesz „kopię przechowującą” z bardziej odpowiednimi metadanymi, a pozostałe łączysz jako duplikaty.
- Prawie Dedup (MinHash/LSH): Dokumenty o podobnej, ale nie identycznej treści (e-mail z dodanym podpisem, wersje robocze). Algorytmy takie jak MinHash z uwzględnieniem lokalizacji Haszowanie identyfikuje dokumenty o podobieństwie Jaccarda > 0,85.
- Wątki e-maili: Grupowanie e-maili w rozmowy (wątki) dla zmniejsz liczbę recenzji, prezentując tylko najnowszą wiadomość ze wszystkimi kontekstami.
"""
ediscovery/processing/deduplication.py
Near-deduplication con MinHash e LSH
"""
from datasketch import MinHash, MinHashLSH
import hashlib
from typing import Optional
class DeduplicationEngine:
def __init__(self, num_perm: int = 128, threshold: float = 0.85):
self.lsh = MinHashLSH(threshold=threshold, num_perm=num_perm)
self.num_perm = num_perm
self.processed: dict[str, str] = {} # sha256 -> doc_id
def is_exact_duplicate(self, sha256: str) -> Optional[str]:
"""Restituisce doc_id del duplicato esatto o None."""
return self.processed.get(sha256)
def register_document(self, doc_id: str, sha256: str, text: str) -> None:
"""Registra un documento nel sistema di dedup."""
self.processed[sha256] = doc_id
minhash = self._compute_minhash(text)
self.lsh.insert(doc_id, minhash)
def find_near_duplicates(self, text: str, query_doc_id: str) -> list[str]:
"""
Trova near-duplicati del testo con Jaccard sim >= threshold.
Restituisce lista di doc_id simili.
"""
minhash = self._compute_minhash(text)
results = self.lsh.query(minhash)
return [r for r in results if r != query_doc_id]
def _compute_minhash(self, text: str) -> MinHash:
minhash = MinHash(num_perm=self.num_perm)
# Shingling a livello di parola (3-gram)
tokens = text.lower().split()
shingles = [
' '.join(tokens[i:i+3])
for i in range(len(tokens) - 2)
]
for shingle in shingles:
minhash.update(shingle.encode('utf8'))
return minhash
Przegląd wspomagany technologią i kodowanie predykcyjne
Faza przeglądu jest w historii najdroższa: starsi prawnicy czytają każdy dokument zaklasyfikować je jako istotne, nieistotne lub uprzywilejowane (objęte tajemnicą adwokacką). The Przegląd wspomagany technologią (TAR) con Ciągłe aktywne uczenie się (CAL) drastycznie zmniejsza ten koszt: model uczy się na podstawie decyzji i priorytetów recenzentów najbardziej prawdopodobne istotne dokumenty, co pozwala na zatrzymanie przeglądu, gdy stawka oczekiwane wycofanie przekracza próg (zwykle 75% zgodnie ze standardem TREC Legal Track).
"""
ediscovery/review/predictive_coding.py
Predictive Coding con Sentence Transformers e Active Learning
"""
from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import numpy as np
from typing import Optional
class PredictiveCodingEngine:
"""
Implementa TAR 2.0 (Continuous Active Learning).
Il reviewer classifica documenti, il modello re-addestra
e riordina la review queue.
"""
def __init__(self, model_name: str = 'all-mpnet-base-v2'):
self.encoder = SentenceTransformer(model_name)
self.classifier = LogisticRegression(
class_weight='balanced',
max_iter=1000
)
self.scaler = StandardScaler()
self.training_texts: list[str] = []
self.training_labels: list[int] = [] # 1=rilevante, 0=non rilevante
self.is_trained = False
def add_review_decision(
self,
doc_text: str,
is_relevant: bool
) -> None:
"""Aggiunge una decisione di review al training set."""
self.training_texts.append(doc_text)
self.training_labels.append(1 if is_relevant else 0)
# Re-addestra quando ci sono abbastanza esempi (>=10 per classe)
pos_count = sum(self.training_labels)
neg_count = len(self.training_labels) - pos_count
if pos_count >= 5 and neg_count >= 5:
self._retrain()
def _retrain(self) -> None:
embeddings = self.encoder.encode(
self.training_texts,
batch_size=32,
show_progress_bar=False
)
embeddings_scaled = self.scaler.fit_transform(embeddings)
self.classifier.fit(embeddings_scaled, self.training_labels)
self.is_trained = True
def predict_relevance(
self,
texts: list[str]
) -> list[dict]:
"""
Predice la rilevanza di una lista di documenti.
Restituisce lista di {text_index, relevance_prob} ordinata per score desc.
"""
if not self.is_trained:
# Prima del training, restituisce ordine casuale
return [
{'index': i, 'relevance_prob': 0.5}
for i in range(len(texts))
]
embeddings = self.encoder.encode(
texts,
batch_size=32,
show_progress_bar=False
)
embeddings_scaled = self.scaler.transform(embeddings)
probabilities = self.classifier.predict_proba(
embeddings_scaled
)[:, 1] # probabilità classe positiva
results = [
{'index': i, 'relevance_prob': float(p)}
for i, p in enumerate(probabilities)
]
return sorted(results, key=lambda x: x['relevance_prob'], reverse=True)
def estimate_recall(
self,
reviewed_count: int,
total_count: int,
relevant_found: int
) -> float:
"""
Stima il recall atteso usando il metodo seed+sample
secondo TREC Legal Track guidelines.
Semplificazione: usa il tasso di prevalenza osservato.
"""
if reviewed_count == 0:
return 0.0
prevalence = relevant_found / reviewed_count
estimated_total_relevant = prevalence * total_count
if estimated_total_relevant == 0:
return 1.0
return min(relevant_found / estimated_total_relevant, 1.0)
Porównanie platform e-Discovery
Rynek platform e-Discovery jest skonsolidowany, ale szybko ewoluuje pod presją AI. Oto porównanie głównych rozwiązań dostępnych w latach 2025-2026:
| Platforma | Mocne strony | Ograniczenia | Model cenowy | Funkcje sztucznej inteligencji |
|---|---|---|---|---|
| Względność | Standardy korporacyjne, rozległy ekosystem, dojrzałe interfejsy API | Złożoność konfiguracji, wysokie koszty | SaaS + hostowane samodzielnie, za GB | RelevanceAI, wyszukiwanie koncepcyjne, wykrywanie anomalii |
| DYSK | Natywny w chmurze, nowoczesny UX, natywnie zintegrowana sztuczna inteligencja | Mniejszy ekosystem niż teoria względności | Subskrypcja + użytkowanie | DISCO AI: kodowanie predykcyjne, automatyczne tagowanie, pytania i odpowiedzi dotyczące dokumentów |
| Everlaw | Współpraca w czasie rzeczywistym, doskonały UX, gotowa wersja próbna | Mniej funkcji dla przypadków masowych | Za GB/miesiąc | EverAI: kodowanie predykcyjne, podsumowanie, pytania i odpowiedzi dotyczące zeznań |
| Logikcull | Samoobsługa, przejrzyste ceny, szybki onboarding | Mniej nadaje się do bardzo skomplikowanych przypadków | Za GB lub subskrypcję | Automatyczne tagowanie, wyszukiwanie wspomagane, zaawansowane deduplikowanie |
| Ujawnić | Zaawansowana sztuczna inteligencja (Brainspace), autorski NLP | Stroma krzywa uczenia się | Licencjonowanie przedsiębiorstw | Grupowanie tematów, wyszukiwanie koncepcji, wykrywanie anomalii |
Eksport EDRM XML i tworzenie dokumentów
Ostatnim etapem jest dostarczenie kontrahentowi dokumentów zgodnie z ustalonym formatem. Standard XML EDRM definiuje schemat XML do wymiany dokumentów ze wszystkimi metadane, ułatwiające przesyłanie na dowolną platformę. Dokumenty zazwyczaj przychodzą produkty z Numeracja Batesa (unikalna numeracja progresywna) oraz z redakcją stosowane do treści uprzywilejowanych.
"""
ediscovery/production/edrm_exporter.py
Generazione export EDRM XML con Bates numbering
"""
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from typing import Iterator
def generate_edrm_xml(
documents: list[dict],
matter_id: str,
bates_prefix: str = "PROD",
start_bates: int = 1
) -> str:
"""
Genera EDRM XML per un set di documenti prodotti.
Ogni documento riceve un Bates number univoco.
"""
root = ET.Element('Root')
root.set('DataInterchangeType', 'Processed')
root.set('DateCreated', datetime.now(timezone.utc).isoformat())
root.set('Encoding', 'UTF-8')
root.set('MajorVersion', '1')
root.set('MinorVersion', '2')
batch = ET.SubElement(root, 'Batch')
documents_el = ET.SubElement(batch, 'Documents')
for idx, doc in enumerate(documents):
bates_num = f"{bates_prefix}{str(start_bates + idx).zfill(7)}"
doc_el = ET.SubElement(documents_el, 'Document')
doc_el.set('DocID', doc['doc_id'])
# Tags con metadata
tags_el = ET.SubElement(doc_el, 'Tags')
def add_tag(name: str, value: str, data_type: str = 'Text') -> None:
tag = ET.SubElement(tags_el, 'Tag')
tag.set('TagName', name)
tag.set('TagValue', value)
tag.set('TagDataType', data_type)
add_tag('BatesNumber', bates_num)
add_tag('DocID', doc['doc_id'])
add_tag('MatterID', matter_id)
add_tag('Custodian', doc.get('custodian_id', ''))
add_tag('FileName', doc.get('filename', ''))
add_tag('DateCollected', doc.get('collection_timestamp', ''), 'DateTime')
add_tag('DateCreated', doc.get('created_date', ''), 'DateTime')
add_tag('Author', doc.get('author', ''))
add_tag('FileSize', str(doc.get('file_size', 0)), 'LongInteger')
add_tag('SHA256', doc.get('sha256', ''))
add_tag('ContentType', doc.get('content_type', ''))
add_tag('ReviewStatus', doc.get('review_status', ''))
add_tag('IsPrivileged', str(doc.get('privilege', False)), 'Boolean')
add_tag(
'RelevanceScore',
str(round(doc.get('relevance_score', 0), 4)),
'Decimal'
)
return ET.tostring(root, encoding='unicode', xml_declaration=True)
Krytyczne rozważania prawne
- Wywłaszczenie: Późniejsze zniszczenie lub zmiana dowodów w uzasadniony sposób przewidywalne postępowanie sądowe może prowadzić do surowych sankcji, łącznie z niekorzystnymi konsekwencjami instrukcje (instruując ławę przysięgłych, aby przyjęła, że zniszczony materiał dowodowy był niekorzystny).
- Przegląd przywilejów: Dokumenty objęte tajemnicą adwokacką lub pracą doktrynę produktu należy określić i opracować przed rozpoczęciem produkcji. Produkcja przypadkowe użycie dokumentów uprzywilejowanych można zakwestionować za pomocą umów o wycofaniu (FRE 502 (d)).
- Transgraniczna prywatność danych: Zbieraj dane od europejskich pracowników ds odkrycie USA wymaga dogłębnej analizy RODO. Ramy ochrony danych USA–UE (2023) uprościło przekazywanie środków, lecz należyta staranność pozostaje kluczowa.
- Obronność AI: Sądy wymagają przejrzystości w zakresie stosowanych metod TAR. Wybrany protokół i próg wycofania muszą być udokumentowane i możliwe do obrony.
Schemat bazy danych dla łańcucha dostaw
Relacyjna baza danych i szkielet prawny platformy. Każda czynność na każdym dokumencie należy śledzić ze znacznikiem czasu, użytkownikiem i powodem działania. Ta ścieżka audytu musi być niezmienny: żaden rekord nie może zostać usunięty ani zmodyfikowany z mocą wsteczną.
-- Schema PostgreSQL per e-Discovery con audit trail immutabile
-- Matters (cause/procedimenti)
CREATE TABLE matters (
matter_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
matter_name TEXT NOT NULL,
matter_number TEXT UNIQUE NOT NULL,
client_id UUID NOT NULL,
status TEXT NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
closed_at TIMESTAMPTZ
);
-- Custodians (soggetti i cui dati vengono raccolti)
CREATE TABLE custodians (
custodian_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
matter_id UUID REFERENCES matters(matter_id),
full_name TEXT NOT NULL,
email TEXT NOT NULL,
department TEXT,
hold_applied BOOLEAN NOT NULL DEFAULT FALSE,
hold_date TIMESTAMPTZ
);
-- Documents (indice master dei documenti)
CREATE TABLE documents (
doc_id UUID PRIMARY KEY,
matter_id UUID REFERENCES matters(matter_id),
custodian_id UUID REFERENCES custodians(custodian_id),
filename TEXT NOT NULL,
file_size BIGINT NOT NULL,
sha256 CHAR(64) NOT NULL,
md5 CHAR(32) NOT NULL,
s3_key TEXT NOT NULL,
content_type TEXT,
is_duplicate_of UUID REFERENCES documents(doc_id),
is_near_dup_of UUID REFERENCES documents(doc_id),
collection_timestamp TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'COLLECTED',
review_status TEXT NOT NULL DEFAULT 'UNREVIEWED',
relevance_score NUMERIC(5,4),
privilege BOOLEAN NOT NULL DEFAULT FALSE,
bates_number TEXT UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Custody events (audit trail immutabile)
-- Constraint: nessuna UPDATE/DELETE permessa
CREATE TABLE custody_events (
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
doc_id UUID REFERENCES documents(doc_id),
matter_id UUID REFERENCES matters(matter_id),
custodian_id UUID,
collector_id UUID,
event_type TEXT NOT NULL, -- COLLECTION, PROCESSING, REVIEW, PRODUCTION, etc.
timestamp TIMESTAMPTZ NOT NULL,
source_path TEXT,
sha256 CHAR(64),
md5 CHAR(32),
file_size BIGINT,
s3_key TEXT,
user_id UUID,
notes TEXT
);
-- Impedisce UPDATE e DELETE sulla tabella eventi
CREATE RULE no_update_custody_events AS
ON UPDATE TO custody_events DO INSTEAD NOTHING;
CREATE RULE no_delete_custody_events AS
ON DELETE TO custody_events DO INSTEAD NOTHING;
-- Review decisions
CREATE TABLE review_decisions (
decision_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
doc_id UUID REFERENCES documents(doc_id),
reviewer_id UUID NOT NULL,
decision TEXT NOT NULL, -- RELEVANT, NOT_RELEVANT, PRIVILEGED, NEEDS_REDACTION
confidence TEXT, -- HIGH, MEDIUM, LOW
review_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
notes TEXT
);
Wnioski i dalsze kroki
Budowa platformy e-Discovery klasy korporacyjnej wymaga architektury, która łączy trzy elementy imperatywy często sprzeczne ze sobą: wydajność techniczna (przetwarzaj miliony dokumentów w rozsądnym terminie), poprawność prawna (łańcuch dowodowy niezmienne, możliwe do udokumentowania, przegląd uprawnień możliwy do obrony) e użyteczność (recenzenci są prawnikami, a nie analitykami danych).
Kluczowe komponenty, które zbadaliśmy — pobieranie do celów kryminalistycznych za pomocą SHA-256, przetwarzanie wdrożony z Apache Tika, deduplikacja z MinHash/LSH i kodowanie predykcyjne z Active Kształcenie – reprezentowanie aktualnego stanu wiedzy w sektorze w latach 2025–2026. Przyjęcie sztucznej inteligencji przekształcił TAR z technologii eksperymentalnej w standard rynkowy: 79% platform integruje go dzisiaj natywnie.
W następnym artykule z tej serii zobaczymy, jak zbudować Silnik zgodności z dynamicznymi silnikami reguł do automatyzacji monitorowania regulacyjnego w czasie rzeczywistym.
Zasoby i spostrzeżenia
- EDRM (model referencyjny elektronicznego wykrywania): edrm.net
- Dokumentacja Platformy Teorii Względności: relativity.com/sztuczna-inteligencja
- Wytyczne TREC dotyczące wycofania ścieżki prawnej: standard wycofania Measure.it
- FRE 502(d): Umowy o zwrot w przypadku przypadkowo wyprodukowanych dokumentów uprzywilejowanych
- datasketch - biblioteka Pythona dla MinHash/LSH
- Transformatory zdań: osadzanie dla kodowania predykcyjnego







