Comment nous avons indexé 1,14 million de décisions judiciaires en 3 semaines

Un regard en coulisses : comment Mont Virtua a construit la plus grande base de données juridique suisse vérifiée aux sources. Décisions techniques, erreurs et leçons.

Comment nous avons indexé 1,14 million de décisions judiciaires en 3 semaines

On nous demande parfois comment une jeune entreprise dispose d’une base de données plus complète que ce que des fournisseurs établis ont construit en des années. La réponse est peu spectaculaire : nous avons fait la chose évidente que personne ne fait. Nous sommes allés directement à la source.

Cet article n’est pas du marketing. C’est un retour d’expérience : ce que nous avons construit, quelles décisions nous avons prises et ce qui a mal tourné.

Jour 1 : la prise de conscience

Le droit suisse est public. Intégralement. Gratuitement.

Fedlex publie toutes les lois fédérales. Les 26 cantons publient leurs lois. Le Tribunal fédéral publie ses arrêts. Le Tribunal administratif fédéral publie ses arrêts. 115 tribunaux publient en ligne.

Tout est là. Sur les serveurs officiels de l’État. Dans des formats structurés. Pas de paywall. Pas de contrat de licence.

Pourtant, des études et des entreprises paient des milliers de francs par an pour accéder à des bases de données qui font fondamentalement la même chose : collecter ces données publiques, les traiter et les rendre consultables. Le traitement a de la valeur. Le monopole sur les données elles-mêmes n’existe pas.

Nous nous sommes demandé : que se passe-t-il si nous prenons les mêmes sources publiques, mais avec une infrastructure moderne ? Pas la technologie de 2005. Mais : PostgreSQL avec recherche plein texte en quatre langues. Des embeddings vectoriels pour la recherche sémantique. Un graphe de citations avec 1,42 million d’arêtes. Le tout sur un seul serveur.

Semaine 1 : scraping

Les sources

Source Type Volume Format
Fedlex Lois fédérales 4 800+ XML/HTML
26 portails cantonaux Lois cantonales 23 000+ HTML (26 formats différents)
TF Arrêts du Tribunal fédéral ~70 000 HTML
TAF Tribunal administratif fédéral 91 582 HTML
Tribunaux cantonaux Décisions cantonales ~980 000 HTML (113 formats différents)
FOSC Feuille officielle 2 500 000 XML
FINMA Réglementation 27 tables HTML/PDF

Le défi des 26 cantons

Chaque canton a son propre portail. Son propre format HTML. Sa propre structure d’URL. Pas d’API. Pas d’interface unifiée.

Zurich livre du HTML propre avec une structure cohérente. Berne utilise un CMS des années 2000 avec des frames profondément imbriquées. Appenzell Rhodes-Intérieures a un site web statique probablement maintenu à la main.

Nous avons écrit un parser distinct pour chaque canton. 26 parsers. Certains de 50 lignes de code, d’autres de 500. Le travail n’était pas intellectuellement exigeant. Il exigeait de la patience.

Leçon 1 : le goulot d’étranglement dans la capture de données n’est pas la technologie. C’est la volonté de travailler à travers 26 formats HTML différents sans prendre de raccourcis.

Architecture du scraper

Nous avons volontairement construit simplement :

  • Scripts Python. Pas de framework. Pas de Scrapy. Pas de Selenium.
  • requests pour HTTP. BeautifulSoup pour le parsing HTML. psycopg2 pour PostgreSQL.
  • Chaque source son propre script. Pas de tentative de construire une abstraction universelle.
  • Mises à jour incrémentales : chaque scraper se souvient où il s’est arrêté. Au prochain lancement, il ne récupère que les nouvelles entrées.

Leçon 2 : les frameworks de scraping génériques font gagner du temps lorsque les sources se ressemblent. Quand chaque source est un cas particulier, un script simple par source est plus rapide à écrire et plus facile à déboguer.

Fréquence

Tout fonctionne la nuit. Un cron job à 02h00 lance la pipeline. Fedlex d’abord, puis les cantons par ordre alphabétique, puis les tribunaux, puis la FOSC. Durée totale : 2-4 heures selon les temps de réponse des serveurs.

Nous ne surchargeons pas les serveurs. Parallélisme maximal : 2 requêtes simultanées par source. Pauses entre les requêtes. Nous sommes des invités sur des serveurs publics et nous comportons en conséquence.

Semaine 2 : structuration et indexation

Le schéma

Disposer de données brutes est sans valeur si elles ne sont pas structurées. Notre schéma de base de données :

  • laws : 27 795 lois avec métadonnées (numéro RS, titre, date d’entrée en vigueur, canton, langue, statut)
  • law_units : 2,02 millions d’unités législatives (articles, alinéas, chiffres). Chaque unité référence sa loi. Chacune contient le texte intégral en quatre langues maximum.
  • decisions : 1,14 million de décisions avec métadonnées (tribunal, date, numéro de dossier, domaine juridique)
  • citations : 1,42 million d’arêtes de citation. Quelle décision cite quel article ? Quel article est cité par quelles décisions ?

Recherche plein texte

PostgreSQL dispose d’une recherche plein texte intégrée. Pour quatre langues, nous avions besoin de quatre configurations de recherche textuelle différentes :

  • german pour le DE
  • french pour le FR
  • italian pour l’IT
  • english pour l’EN

Chaque unité législative a un index tsvector par langue. La recherche utilise ts_rank pour le tri par pertinence. Pour la plupart des requêtes, c’est suffisamment rapide : moins de 200 ms pour une recherche plein texte sur 2 millions d’entrées.

Leçon 3 : la recherche plein texte de PostgreSQL est sous-estimée. Pour des corpus textuels structurés dans des langues connues, elle offre 90 % de la qualité d’Elasticsearch à 10 % de la complexité opérationnelle.

Embeddings

La recherche plein texte trouve des mots. La recherche sémantique trouve des concepts.

Exemple : une recherche «protection contre le licenciement en cas de maladie» doit aussi trouver des décisions mentionnant «période de protection» et «art. 336c CO» sans contenir le terme «protection contre le licenciement».

Nous avons converti les 2,02 millions d’unités législatives et 1,14 million de décisions en vecteurs 384-dimensionnels avec le modèle all-MiniLM-L6-v2. Le modèle est petit (80 Mo), rapide et multilingue. Pas le meilleur modèle d’embedding du marché, mais parfaitement suffisant pour un premier index.

Les vecteurs résident dans PostgreSQL avec l’extension pgvector. Index HNSW avec m=16 et ef_construction=64. Recherche approximative des plus proches voisins sur 3 millions de vecteurs en moins de 50 ms.

Leçon 4 : commencez avec le modèle le plus simple qui fonctionne. Un mauvais embedding en ligne bat un embedding parfait encore en évaluation.

Le graphe de citations

Les décisions citent des lois. Les lois renvoient à d’autres lois. Les décisions ultérieures confirment, précisent ou infirment les décisions antérieures.

Ces relations sont la valeur réelle. Pas le texte lui-même, qui est public. Mais les connexions.

Nous extrayons les citations automatiquement des textes de décisions. Patterns RegEx pour : «art. 336c CO», «ATF 148 III 25», «TAF A-1234/2025», références de décisions cantonales. 1,42 million d’arêtes. Stockées dans une table PostgreSQL. Interrogeables avec des CTE récursives.

Ce que cela permet :

  • Quelles décisions citent un article donné ?
  • Comment la jurisprudence sur un article a-t-elle évolué dans le temps ?
  • Y a-t-il des contradictions entre tribunaux ?
  • Quels articles sont cités le plus fréquemment (et sont donc les plus contestés) ?

Leçon 5 : le graphe de citations n’est pas une fonctionnalité. C’est le produit. Tout le reste est de l’infrastructure.

Semaine 3 : assurance qualité

Ce qui a mal tourné

Problème 1 : 34 000 nœuds structurels sans texte. Des unités législatives comme «Deuxième titre» ou «Troisième section» sont des éléments de structure, pas du contenu. Nous les avons d’abord indexées, ce qui a pollué les résultats de recherche. Solution : identifier ces nœuds et les exclure de la pipeline d’embedding. 34 000 entrées nettoyées.

Problème 2 : ~12 000 décisions sans texte. Certains tribunaux cantonaux ne publient que les considérants principaux, pas le texte intégral. Les scrapers ont créé ces entrées avec un champ texte vide. Solution : marquées, pas supprimées. Les métadonnées (tribunal, date, citations) conservent leur valeur.

Problème 3 : chaos d’encodage avec les textes français. Deux portails cantonaux livrent du Latin-1 au lieu de l’UTF-8. Les accents devenaient des points d’interrogation. Solution : détection explicite de l’encodage avec chardet avant le parsing.

Problème 4 : doublons dans les lois cantonales. Certains cantons publient la même loi sous différentes URL (version consolidée et loi de modification). Notre dédoublonnage se base sur numéro RS + canton + date d’entrée en vigueur. Cela a capté 98 % des doublons. Les 2 % restants ont été nettoyés manuellement.

Ce que nous avons bien fait

  • Aucune donnée inventée. Chaque entrée de notre base de données a une URL source pointant vers un serveur officiel de l’État. Pas un seul point de données ne provient d’une source tierce.
  • Incrémental, pas monolithique. Chaque scraper peut être relancé individuellement. Si un portail cantonal est hors ligne, le reste continue.
  • Tout dans une seule base de données. PostgreSQL. Pas de cluster Elasticsearch, pas d’instance Redis, pas de vector store séparé. Une base de données qui fait tout. Moins d’infrastructure, moins de points de défaillance.

Les chiffres

Métrique Valeur
Lois 27 795
Unités législatives 2 020 000
Décisions judiciaires 1 140 000
Arêtes de citation 1 420 000
Entrées FOSC 2 500 000
Tables FINMA 27
Taille de la base 41 Go
Temps de développement 3 semaines
Coûts d’infrastructure CHF 0 (serveur local)
Coûts de fonctionnement ~CHF 15/mois (inférence IA pour les alertes)

Ce que cela signifie

Nous ne possédons pas de données exclusives. Nous possédons une pipeline qui exploite les données publiques mieux que tout fournisseur existant. Ce n’est pas un secret. Les sources de données sont ouvertes à tous.

L’avantage réside dans l’exécution : 26 cantons correctement parsés, 115 tribunaux couverts, 1,42 million d’arêtes de citation extraites, quatre langues indexées, le tout consultable en une seule requête. N’importe qui peut reproduire cela, à condition d’être prêt à passer trois semaines à écrire des parsers pour 26 formats HTML différents.

La plupart ne le sont pas.


Cet article est publié à titre informatif et ne constitue pas un conseil juridique.

Retour au blog

Articles similaires