Plateforme locale d'exploration de corpus multimodal — alternative open-source à Palantir & Maltego
Les outils d'analyse de corpus documentaire de référence (Palantir, Maltego, i2 Analyst's Notebook) sont onéreux, propriétaires, et nécessitent l'envoi de données vers des serveurs tiers — incompatible avec des corpus sensibles (judiciaire, journalisme d'investigation, renseignement open-source).
L'objectif de Nexus est de reproduire ces capacités dans un environnement 100 % local, open-source, tournant sur une machine personnelle avec GPU grand public (RTX 3070, 8 Go VRAM). Le projet couvre l'intégralité de la chaîne : ingestion de documents bruts → extraction automatique → analyse sémantique et graphique → interrogation en langage naturel.
| Couche | Technologie |
|---|---|
| Backend API | FastAPI 0.115 + Uvicorn, Python 3.11 |
| Workers asynchrones | Celery 5.4 + Redis 7 |
| Recherche | Elasticsearch 8.12 (BM25 + kNN dense_vector) |
| Graphe | Neo4j 5 Community + GDS Plugin |
| Stockage objets | MinIO (S3-compatible) |
| Frontend | React 18 + MUI 6 (dark theme) + vis-network 9 |
| LLM | LlamaIndex 0.12 + Ollama (local, modèle configurable) |
| Monitoring | Prometheus + Grafana |
| Orchestration | Docker Compose (10 conteneurs) |
| Modèle | Rôle |
|---|---|
| BAAI/bge-m3 | Embeddings multilingues (1024 dim) |
| facebook/bart-large-mnli | Classification zero-shot |
| fr_core_news_lg / en_core_web_trf | NER statistique + transformer |
| GLiNER | NER généraliste contextuel |
| OpenAI Whisper (base) | Transcription audio/vidéo |
| PaddleOCR | OCR multilingue |
| cross-encoder/ms-marco-MiniLM | Re-ranking sémantique |
Détection du format, extraction du contenu brut (PDF, DOCX, audio, vidéo, email…)
Hash SHA-256 — rejet immédiat des doublons binaires
PyMuPDF → Tesseract → PaddleOCR, sélection automatique selon la qualité
Whisper pour les fichiers audio et vidéo
MinHash LSH (seuil 0.9) — élimination des quasi-doublons
Identification automatique de la langue du document
BART-large-MNLI — 12 catégories thématiques
spaCy + GLiNER + regex — extraction de 8 types d'entités
Fusion des mentions, normalisation des noms canoniques
Découpage avec préfixe de contexte pour la RAG
BAAI/bge-m3 — vecteurs de 1024 dimensions par chunk
BM25 + index kNN dense_vector
Nœuds entités + relations MENTIONED_IN, APPEARS_WITH, liens structurels
Chaque étape est une tâche Celery indépendante avec gestion des erreurs transitoires (retry) et permanentes. Le pipeline est repris depuis n'importe quelle étape.
CorpusExplorer/
├── backend/
│ ├── app/
│ │ ├── api/routes/ # 15 modules de routes (auth, documents, search, graph, llm…)
│ │ ├── models/ # Pydantic v2 (Document, Entity, Person, User…)
│ │ ├── services/
│ │ │ ├── ingestion/ # IngestService, extracteurs par format
│ │ │ ├── processing/ # NER, OCR, embeddings, chunker, dedup, classification
│ │ │ ├── search/ # ElasticsearchService, SemanticSearch, cache
│ │ │ ├── graph/ # GraphBuilder (Neo4j), PathFinder
│ │ │ ├── llm/ # RAGService, LlamaIndex adapters, LLMService (Ollama)
│ │ │ ├── profile/ # PersonProfileService, ProfileGenerator
│ │ │ └── auth/ # JWT, RBAC, audit
│ │ └── workers/
│ │ └── tasks.py # Pipeline Celery (~1 000 lignes)
├── frontend/
│ └── src/
│ ├── components/ # 27 pages et composants React
│ ├── services/api.js # Client Axios centralisé
│ └── locales/ # i18n fr/en
└── docker-compose.yml # 10 services orchestrés
FastAPI tourne sur une boucle asyncio principale. Les tâches LlamaIndex (synchrones) sont déléguées à un ThreadPoolExecutor. Le problème : les composants appelaient asyncio.run() depuis ce thread, créant des boucles imbriquées et corrompant le client Elasticsearch async partagé.
Solution : réécriture intégrale de NexusElasticsearchVectorStore et NexusGraphRetriever en mode fully-synchronous, avec des clients Elasticsearch sync dédiés — zéro asyncio dans les threads.
Le DeduplicationService maintient un index MinHash LSH en mémoire. Un purge de la base vidait Redis mais laissait la LSH stale → faux quasi-doublons sur les ré-imports.
Solution : tâche Celery dédiée reset_dedup_state_task() dispatchée après purge Redis pour reconstruire l'index depuis un état vide.
Le build webpack consomme plus de 1 Go de heap Node.js, provoquant des OOM kills en conteneur.
Solution : mem_limit: 1536m + NODE_OPTIONS=--max-old-space-size=1024 sur le conteneur frontend.
Le merge de deux nœuds Neo4j devait rediriger toutes leurs relations, mais les types (MENTIONED_IN, APPEARS_WITH, SPOUSE_OF…) ne sont pas connus à l'avance.
Solution : réécriture du merge en Cypher générique avec CALL {} subqueries dynamiques couvrant tous les types de relations via fallback APOC-free.
Ce projet illustre ma capacité à concevoir et construire un système IA complet de bout en bout — de l'ingestion documentaire à l'assistant conversationnel, en passant par la structuration, la recherche hybride et le graphe de connaissances. C'est le projet qui correspond le mieux aux problématiques que je traite au quotidien chez ChapsVision, transposées dans un contexte 100 % open-source et local, sans aucune contrainte externe.
Il démontre à la fois la profondeur technique (NLP, RAG, vector search, graph, orchestration) et la capacité à livrer un produit utilisable avec une vraie interface, un vrai pipeline et une vraie architecture — pas un notebook ou un POC.