e-Discovery Platformarchitectuur: opname, verwerking en AI-beoordeling
In 2024 de adoptie van kunstmatige intelligentie op platforms e-ontdekking legaal en steeg in slechts één jaar van 19% naar 79%. Deze gegevens zijn geen statistische curiositeit: ze weerspiegelen er één structurele transformatie in de manier waarop advocatenkantoren, bedrijven en gerechtelijke autoriteiten de zaken beheren bewijsstukken in civiele en strafrechtelijke procedures. Wanneer een rechtszaak miljoenen e-mails en berichten betreft Slack, SharePoint-documenten en logbestanden, het traditionele model van handmatige beoordeling door paralegal is economisch en logistiek niet langer duurzaam.
L'e-ontdekking (Electronic Discovery) en het proces waarmee de partijen bij een procedure betrokken zijn juridisch bewijsmateriaal identificeren, verzamelen, bewaren, verwerken, beoordelen en produceren elektronisch formaat. In de Verenigde Staten en gereguleerd door de Federal Rules of Civil Procedure (FRCP), in Europa vanuit vergelijkbare kaders op nationaal niveau. Moderne platforms moeten petabytes aan data verwerken, respecteer de juridisch geldige bewakingsketens en verminder het aantal documenten dat moet worden beoordeeld het handhaven van een zeer hoge herinnering: geen enkel relevant document kan verloren gaan.
In dit artikel bouwen we de volledige architectuur van een e-Discovery-platform op ondernemingsniveau: van massale inname van heterogene documenten gedistribueerde verwerking, van de ontdubbeling al voorspellende codering met AI-modellen, omhoog aanexporteren in EDRM XML-formaat. Allemaal met echte Python-codevoorbeelden en analyses van de toonaangevende platforms op de markt.
Wat u in dit artikel leert
- Het EDRM-model (Electronic Discovery Reference Model) en zijn fasen
- Microservices-architectuur voor massale opname: Kafka, Elasticsearch, MinIO
- Verwerkingspijplijn: tekstextractie, metadata, MD5/SHA-deduplicatie en bijna-dedup
- Technology Assisted Review (TAR) en Continu Actief Leren (CAL)
- Voorspellende codering met scikit-learn en zinstransformatoren
- Chain of Custody-beheer en onveranderlijk audittraject
- Platformvergelijking: Relativiteit, DISCO, Everlaw, Logikcull
- EDRM XML-export en integratie met casemanagementsystemen
Positie in de LegalTech- en AI-serie
| # | Item | Focus |
|---|---|---|
| 1 | NLP voor contractanalyse | OCR, NER, clausuleclassificatie |
| 2 | U bent hier — e-Discovery Architectuur | Inname, verwerking, AI-beoordeling |
| 3 | Automatisering van naleving | Regelengine en RegTech |
| 4 | Slimme contracten | Soliditeit, Vyper, afdwingbaarheid |
| 5 | Samenvattend met generatieve AI | LLM, RAG, outputvalidatie |
| 6 | Jurisprudentiële zoekmachine | Vector-inbedding en semantisch zoeken |
| 7 | Digitale handtekening en eIDAS 2.0 | PKI, tijdstempels, workflow |
| 8 | AVG-nalevingssystemen | Privacy by design, DSR, datamapping |
| 9 | Juridische AI-copiloot | RAG over juridisch corpus, vangrails |
| 10 | Gegevensintegratie LegalTech | ECLI, API-rechtssystemen, XBRL |
Het EDRM-model: referentiekader voor e-Discovery
L'Elektronisch ontdekkingsreferentiemodel (EDRM) en de de facto standaard die de fasen van het elektronische ontdekkingsproces. Het model is ontwikkeld in 2005 en voortdurend bijgewerkt definieert negen opeenvolgende fasen die elk bedrijfsplatform moet ondersteunen.
| EDRM-fase | Beschrijving | Technische component |
|---|---|---|
| 1. Informatiebeheer | Bewaarbeleid, gegevensclassificatie, datakaart | MDM, beleidsengine, CMDB |
| 2. Identificatie | Lokaliseer potentiële beheerders en relevante gegevensbronnen | Crawler, directoryscan, LDAP-query |
| 3. Behoud | Juridische bewaarplicht: gegevens bevriezen om plundering te voorkomen | Hold-beheer, onveranderlijke opslag, notificatieworkflow |
| 4. Verzameling | Forensische collectie met Chain of Custody | Forensische verzamelaar, hash-verificatie, voogdijlogboek |
| 5. Verwerking | Tekstextractie, metadata, ontdubbeling, NIST-filtering | Apache Tika, Elasticsearch neemt pijplijn op |
| 6.Beoordeling | Relevantie/privilegeclassificatie, TAR/CAL | Voorspellende codering, actief leren, beoordelingsplatform |
| 7. Analyse | Patroon, tijdlijn, entiteitsnetwerk, onderwerpmodellering | Grafiekanalyse, NLP, LDA/BERTopic |
| 8. Productie | Exporteren in afgesproken formaat (TIFF, native, PDF) | EDRM XML-export, Bates-nummering, redactie-engine |
| 9. Presentatie | Proefpresentaties, verklaringen, visuele tijdlijnen | Proefdirecteur, tentoonstellingsmanagement |
Microservices-architectuur voor Enterprise e-Discovery
Een modern e-Discovery-platform kan geen monoliet zijn. Datavolumes variëren van weinig gigabytes tot honderden terabytes voor grote doelen. Architectuur moet dat zijn elastisch schaalbaar, fouttolerant en zorgen voor onveranderlijke audittrails voor voldoen aan de eisen om in aanmerking te komen voor bewijs. Het geconsolideerde architecturale patroon combineert gebeurtenisstreaming, objectopslag en gedistribueerde zoekmachine.
# 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:
Grote pijplijn voor documentopname
Inname is de meest kritische fase: er moeten documenten worden verzameld uit heterogene bronnen (e-mailserver, bestanden delen, cloudopslag, SaaS-applicaties) met behoud van de forensische integriteit. Elk verworven document moet hebben een verifieerbare cryptografische hash en een onveranderlijk verslag van wanneer en hoe en verzameld.
"""
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()
Verwerking en inhoudextractie met Apache Tika
De verwerking en het technische hart van de pijpleiding. Elk document moet naar tekst worden omgezet doorzoekbaar, metagegevens geëxtraheerd (auteur, datums, e-mailthread, documenteigenschappen) en genormaliseerd in een gemeenschappelijk schema. Apache Tika beheert meer dan 1.500 verschillende bestandsformaten, waardoor het de de facto standaard is voor inhoudsextractie in 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': []
}
)
Deduplicatie: exacte en bijna-duplicaatdetectie
In een typische e-Discovery-collectie zijn 40-70% van de documenten duplicaten of bijna-duplicaten. Daar ontdubbeling Het is niet alleen van essentieel belang om de beoordelingskosten te verlagen, maar het is ook een wettelijke vereiste: het produceren van duizenden identieke kopieën van dezelfde e-mail is in strijd met de regels van ontdekking en verhoogt de kosten voor de tegenpartij. Er zijn twee niveaus van deduplicatie:
Ontdubbelingsniveaus in e-Discovery
- Exacte ontdubbeling (gebaseerd op hash): Documenten met identieke SHA-256 zijn exacte duplicaten. U bewaart de "bewaarkopie" met relevantere metadata en koppelt de andere als duplicaten.
- Bijna ontdaan (MinHash/LSH): Documenten met vergelijkbare maar niet identieke inhoud (e-mail met toegevoegde handtekening, conceptversies). Algoritmen zoals MinHash met Locality-Sensitive Hashing identificeert documenten met een Jaccard-gelijkenis > 0,85.
- E-mailthreading: E-mails groeperen in gesprekken (threads) voor verminder het recensievolume door alleen het meest recente bericht met alle context te presenteren.
"""
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
Technologieondersteunde beoordeling en voorspellende codering
The review phase is historically the most expensive: senior lawyers read every document for classify it as relevant, irrelevant, or privileged (covered by attorney-client privilege). De Technologieondersteunde beoordeling (TAR) met Continu actief leren (CAL) verlaagt deze kosten drastisch: het model leert van de beslissingen en prioriteiten van de reviewers de meest waarschijnlijke relevante documenten, zodat u de beoordeling kunt stoppen wanneer het tarief is bereikt de verwachte terugroepactie overschrijdt een drempel (doorgaans 75% volgens de TREC Legal Track-standaard).
"""
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)
Vergelijking van e-Discovery-platforms
De markt voor e-Discovery-platforms is geconsolideerd, maar evolueert snel onder druk van van de AI. Hier is een vergelijking van de belangrijkste oplossingen die beschikbaar zijn in 2025-2026:
| Platform | Sterke punten | Beperkingen | Prijsmodel | AI-functies |
|---|---|---|---|---|
| Relativiteit | Bedrijfsstandaarden, uitgebreid ecosysteem, volwassen API's | Complexiteit van de installatie, hoge kosten | SaaS + zelf-gehost, per GB | RelevanceAI, conceptueel zoeken, detectie van afwijkingen |
| SCHIJF | Cloud-native, moderne UX, native geïntegreerde AI | Kleiner ecosysteem dan Relativiteit | Abonnement + gebruik | DISCO AI: voorspellende codering, autotagging, vragen en antwoorden over documenten |
| Everlaw | Realtime samenwerking, uitstekende UX, klaar voor een proefperiode | Minder functies voor massagevallen | Per GB/maand | EverAI: voorspellende codering, samenvatting, Q&A voor depositie |
| Logistiek | Selfservice, transparante prijzen, snelle onboarding | Minder geschikt voor zeer complexe gevallen | Per GB of abonnement | Autotagging, geassisteerd zoeken, geavanceerde ontdubbeling |
| Onthullen | Geavanceerde AI (Brainspace), eigen NLP | Steile leercurve | Enterprise-licenties | Onderwerpclustering, zoeken naar concepten, detectie van afwijkingen |
EDRM XML-export en documentproductie
De laatste fase is het aanleveren van documenten aan de wederpartij volgens het afgesproken format. De EDRM XML-standaard definieert een XML-schema voor het uitwisselen van documenten met al hun metadata, waardoor het uploaden naar elk platform wordt vergemakkelijkt. Documenten komen meestal producten met Bates-nummering (unique progressive numbering) and with editorial staff toegepast op bevoorrechte inhoud.
"""
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)
Kritieke juridische overwegingen
- Onteigening: De vernietiging of wijziging van bewijsmateriaal daarna en redelijkerwijs voorzienbare gerechtelijke procedures kunnen leiden tot strenge sancties, waaronder negatieve gevolgtrekkingen instructies (de jury instrueren om aan te nemen dat het vernietigde bewijsmateriaal ongunstig was).
- Privilege-beoordeling: Documents covered by attorney-client privilege or work product doctrine must be identified and drafted before production. Een productie accidental use of privileged documents can be challenged with claw-back agreements (FRE 502(d)).
- Grensoverschrijdende gegevensprivacy: Verzamel gegevens van Europese werknemers voor Discovery USA vereist een diepgaande AVG-analyse. Het Amerikaans-EU-kader voor gegevensprivacy (2023) has simplified transfers, but due diligence remains essential.
- AI-weerbaarheid: Rechtbanken eisen transparantie over de gebruikte TAR-methoden. Het gekozen protocol en de terugroepdrempel moeten gedocumenteerd en verdedigbaar zijn.
Databaseschema voor Chain of Custody
De relationele database en de juridische ruggengraat van het platform. Elke actie op elk document moet worden bijgehouden met tijdstempel, gebruiker en reden voor actie. Dit audittraject moet onveranderlijk zijn: geen enkel record kan met terugwerkende kracht worden verwijderd of gewijzigd.
-- 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
);
Conclusies en volgende stappen
Het bouwen van een e-Discovery-platform op bedrijfsniveau vereist een architectuur die drie in evenwicht brengt imperatieven die vaak met elkaar op gespannen voet staan: technische prestaties (miljoenen verwerken van documenten binnen een redelijke termijn), juridische correctheid (keten van bewaring onveranderlijk, terugroepbaar documenteerbaar, privilege review verdedigbaar) e bruikbaarheid (recensenten zijn juristen, geen datawetenschappers).
De belangrijkste componenten die we hebben onderzocht: forensische opname met SHA-256, verwerking geïmplementeerd met Apache Tika, deduplicatie met MinHash/LSH en Predictive Coding met Active Leren – vertegenwoordigt de stand van zaken van de sector in 2025-2026. De adoptie van AI transformeerde TAR van experimentele technologie naar marktstandaard: 79% van de platforms integreert het vandaag de dag native.
In het volgende artikel in de serie zullen we zien hoe je een Compliance-engine met dynamische regelmotoren om toezicht op de regelgeving in realtime te automatiseren.
Hulpbronnen en inzichten
- EDRM (elektronisch ontdekkingsreferentiemodel): edrm.net
- Relativity Platform-documentatie: relativity.com/kunstmatige-intelligence
- TREC Legal Track Recall-richtlijnen: meet.it-recallstandaard
- FRE 502(d): Terugvorderingsovereenkomsten voor per ongeluk geproduceerde bevoorrechte documenten
- datasketch - Python-bibliotheek voor MinHash/LSH
- zinstransformatoren: inbedding voor voorspellende codering







