Avancé 35 minJuridique

RAG sur la jurisprudence Légifrance

Légifrance expose les décisions de justice françaises sous licence ouverte via DILA. Un fonds de 500 000+ décisions pesant une quinzaine de Go, parfait pour un RAG juridique 100 % local. Ce guide montre comment télécharger les archives officielles, les parser, indexer avec bge-m3, et poser des questions par recherche sémantique. Tout tient dans un script Python et tourne sur un PC avec 32 Go de RAM et un SSD.

Par Mohamed Meguedmi·Màj 2026-02-12·Testé sur Windows, macOS, Linux

#Ce que vise ce guide

Construire un moteur local qui répond à "trouve-moi les arrêts récents de la Cour de cassation sur la rupture abusive d'un CDD" en quelques secondes, en citant les décisions, leur juridiction, leur date, leur numéro RG. Sans envoyer la question à un serveur distant.

i
Qu'est-ce que DILA
La Direction de l'Information Légale et Administrative publie en open data les décisions sous forme d'archives XML. Le fonds est mis à jour quotidiennement. Légifrance n'est qu'une interface de consultation par-dessus.

#Sources de données

data.gouv.fr — CASS
Arrêts de la Cour de cassation, depuis 1960. ~600 000 décisions. Format XML DILA.
data.gouv.fr — CAPP
Arrêts des Cours d'appel, depuis 2017. ~400 000 décisions.
data.gouv.fr — JURI
Jurisprudence administrative (Conseil d'État, CAA, TA).
Licence
Licence Ouverte 2.0 (compatible Etalab). Usage commercial autorisé, attribution requise.

#1. Télécharger le fonds

Archives quotidiennes CASS
# Les archives sont disponibles sur echanges.dila.gouv.fr
wget -r -np -nH --cut-dirs=3 -R "index.html*" \
  https://echanges.dila.gouv.fr/OPENDATA/CASS/

# Décompression (plusieurs Go)
find CASS -name "*.tar.gz" -exec tar -xzf {} \;
!
Volumétrie
Le dump complet CASS fait environ 8 Go compressé, 35 Go décompressé (centaines de milliers de petits fichiers XML). Prévoyez un SSD et patience pour le premier téléchargement.

#2. Parser le XML DILA

parse_dila.py
from lxml import etree
from pathlib import Path

NS = {"ns": "http://disi.sg.pm.gouv.fr/legi/1"}  # ajuster selon version

def parse_decision(path):
    tree = etree.parse(str(path))
    root = tree.getroot()

    def get(xpath):
        el = root.find(xpath)
        return el.text.strip() if el is not None and el.text else None

    return {
        "id": get(".//META_COMMUN/ID"),
        "juridiction": get(".//META_SPEC/META_JURI/JURIDICTION"),
        "date": get(".//META_SPEC/META_JURI/DATE_DEC"),
        "numero": get(".//META_SPEC/META_JURI/NUMERO"),
        "formation": get(".//META_SPEC/META_JURI/FORMATION"),
        "solution": get(".//META_SPEC/META_JURI/SOLUTION"),
        "texte": " ".join(t for t in root.itertext() if t and t.strip()),
    }

def iter_decisions(folder):
    for path in Path(folder).rglob("*.xml"):
        try:
            yield parse_decision(path)
        except Exception as e:
            print(f"skip {path}: {e}")

#3. Chunking juridique

Une décision de cassation a une structure précise : ENTÊTE / EXPOSÉ DES FAITS / MOYEN / MOTIFS / DISPOSITIF. Chunker naïvement par 700 tokens massacre la structure. Préservez les sections.

Chunking structure-aware
import re

SECTIONS = ["ATTENDU", "CONSIDÉRANT", "SUR LE MOYEN", "PAR CES MOTIFS"]

def split_decision(texte, max_chars=2000):
    # Split par sections juridiques connues
    pattern = "|".join(re.escape(s) for s in SECTIONS)
    parts = re.split(f"(?={pattern})", texte)

    # Re-fusionner les tout petits, couper les trop gros
    chunks = []
    buf = ""
    for p in parts:
        if len(buf) + len(p) < max_chars:
            buf += p
        else:
            if buf: chunks.append(buf)
            buf = p
    if buf: chunks.append(buf)
    return chunks

#4. Indexation

Indexation Qdrant
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams, PointStruct
from sentence_transformers import SentenceTransformer

emb = SentenceTransformer("BAAI/bge-m3", device="cuda")
client = QdrantClient(path="./qdrant_jurispr")
client.recreate_collection(
    "cass",
    vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)

batch = []
for i, d in enumerate(iter_decisions("CASS/")):
    for chunk in split_decision(d["texte"]):
        vec = emb.encode(chunk).tolist()
        batch.append(PointStruct(
            id=i,
            vector=vec,
            payload={
                "juridiction": d["juridiction"],
                "date": d["date"],
                "numero": d["numero"],
                "chunk": chunk[:500],  # extrait pour affichage
            },
        ))
        if len(batch) >= 128:
            client.upsert("cass", points=batch)
            batch = []
Qdrant plutôt que Chroma
Sur 500 000 points et plus, Qdrant est significativement plus rapide que ChromaDB, supporte les filtres typés (date, juridiction), et a une API simple. Sur 1-5k points, Chroma suffit.
Recherche avec filtre
from qdrant_client.http.models import Filter, FieldCondition, MatchValue

def search(q, juridiction=None, after=None, k=10):
    q_vec = emb.encode(q).tolist()
    conditions = []
    if juridiction:
        conditions.append(FieldCondition(key="juridiction", match=MatchValue(value=juridiction)))
    if after:
        conditions.append(FieldCondition(key="date", range={"gte": after}))

    return client.search(
        "cass", query_vector=q_vec, limit=k,
        query_filter=Filter(must=conditions) if conditions else None,
    )

# Exemple
for r in search("rupture abusive CDD", juridiction="COUR DE CASSATION", after="2020-01-01"):
    print(r.payload["date"], r.payload["numero"], r.payload["chunk"][:200])

#Vers la production

Mise à jour quotidienne
Un cron qui télécharge les nouveaux XML de la veille et upsert les nouveaux chunks. Qdrant gère l'update sans reconstruire l'index.
Reranker
Sur juridique, un bge-reranker-v2-m3 post-retrieval fait grimper le recall de ~10 %.
Génération de réponse
Top-5 passages + question → Mistral 7B ou Qwen 14B avec un system prompt "Tu cites systématiquement le numéro RG et la date."
UI
Open WebUI avec un custom tool qui tape sur votre API de recherche. Les juristes ont une interface chat familière.
Ce guide vous a aidé ?

Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.