RAG Événementiel : ownCloud, NATS, Qdrant et MCP
/ 11 min de lecture
Mis à jour :Table des matières
Dans l’écosystème de l’intelligence artificielle et des assistants de productivité personnelle, le RAG (Retrieval-Augmented Generation) est devenu la brique indispensable pour connecter des modèles de langage à des bases documentaires privées. Pourtant, la majorité des implémentations actuelles reposent sur des mécanismes d’ingestion obsolètes : scrutation périodique du système de fichiers (cron polling), re-vectorisation complète de dossiers volumineux à l’aveugle, ou déclenchement manuel depuis une interface d’administration.
Ce modèle présente des défauts majeurs : il consomme inutilement du temps CPU et des entrées/sorties disque, introduit une latence de plusieurs dizaines de minutes entre le dépôt d’un document et sa disponibilité pour l’IA, et génère fréquemment des doublons de fragments vectoriels (chunk duplication) lors de simples renommages ou mises à jour de fichiers.
Pour doter mon infrastructure cloud d’une mémoire documentaire immédiate et sécurisée, j’ai conçu un pipeline d’ingestion entièrement piloté par les événements et nativement multi-utilisateur autour d’ownCloud Infinite Scale (oCIS), du courtier de messages distribué NATS JetStream, de la base vectorielle Qdrant, et d’un serveur Model Context Protocol (MCP) servi en Streamable HTTP.
Voici le deep dive technique de cette architecture, depuis l’interception des paquets NATS jusqu’à l’isolation stricte des collections vectorielles par utilisateur.
Vue d’ensemble de l’Architecture
Plutôt que d’exécuter un crawler périodique, l’architecture s’appuie sur le modèle de microservices natif d’ownCloud Infinite Scale. Les services internes d’oCIS communiquent par le bus NATS. Un conteneur sidecar léger (ocis-qdrant-indexer) y est connecté sur le réseau Docker sécurisé et assure l’ingestion, le routage événementiel et l’exposition du protocole MCP.
flowchart TB
subgraph StorageLayer["Couche Stockage oCIS"]
User["Utilisateur / Client WebDAV"] -->|"Upload / Edit"| OCIS["ownCloud Infinite Scale (oCIS)<br>Moteur decomposedfs"]
OCIS -->|"Événement NATS (SpaceId)"| NATS["NATS JetStream Broker<br>(Endpoint interne :9233)"]
end
subgraph SidecarLayer["Conteneur Sidecar (ocis-qdrant-indexer)"]
Consumer["NATS Event Consumer<br>(UploadReady, ItemTrashed, ItemMoved)"]
UserStore[("User Store SQLite Chiffré<br>(AES-256-GCM / PATs & Spaces)")]
UserIndexer["Multi-User Indexer<br>(Routage SpaceId & Sync Arrière-Plan)"]
WebDAVClient["Client WebDAV Authentifié<br>(PAT Dédié par Utilisateur)"]
Chunker["Extraction Tika / PaddleOCR<br>Tokenizer Cache & SHA-256"]
end
subgraph VectorAndAI["Couche Vectorielle & Inférence"]
CloudflareAI["Cloudflare Workers AI<br>Modèle @cf/baai/bge-m3 (1024 dims)"]
Qdrant[("Qdrant Vector DB<br>Collections par utilisateur: user_hash")]
end
subgraph MCPLayer["Surface Agent IA"]
MCPServer["MCP Streamable HTTP Server<br>(HTTP Basic Auth avec ownCloud)"]
Hermes["Agents IA (Hermes, Pi, Claude)<br>search_owncloud_docs / sync_owncloud_docs"]
end
NATS -->|"Événement avec SpaceId"| Consumer
Consumer --> UserIndexer
UserIndexer <-->|"Résolution Utilisateurs & PAT"| UserStore
UserIndexer --> WebDAVClient
WebDAVClient -->|"Download via /dav/spaces/"| OCIS
WebDAVClient --> Chunker
Chunker -->|"Calcul Vecteurs 1024 dims"| CloudflareAI
CloudflareAI -->|"Embeddings"| Chunker
Chunker -->|"Upsert / Purge Filtrée (space_id)"| Qdrant
Hermes <-->|"Session MCP & Basic Auth"| MCPServer
MCPServer <-->|"Validation Graph API /me"| OCIS
MCPServer <-->|"Vector Search Isolé"| Qdrant
MCPServer -->|"Déclenchement Sync Arrière-Plan"| UserIndexer
1. L’Interception Événementielle : NATS JetStream dans oCIS
Contrairement à l’architecture monolithique d’ownCloud 10 en PHP, ownCloud Infinite Scale est un système distribué écrit en Go dont les microservices communiquent via des événements asynchrones sur un bus NATS JetStream intégré.
Par défaut, ce bus écoute strictement sur 127.0.0.1:9233 à l’intérieur du conteneur oCIS. Pour permettre à notre sidecar d’indexation d’intercepter les événements sur le réseau Docker dédié (resources), la directive d’écoute est explicitement bindée sur l’interface réseau interne :
services: owncloud: image: owncloud/ocis:latest environment: - "NATS_NATS_HOST=0.0.0.0" - "NATS_NATS_PORT=9233" networks: - resourcesLe Cycle de Vie des Événements Surveillés
Le sidecar s’abonne aux sujets NATS émis par le composant storage-users d’oCIS :
UploadReady: Déclenché dès qu’un document est entièrement téléversé et stabilisé sur le stockage. Le payload contient leResourceID(SpaceId+OpaqueId) et le chemin relatif (FileRef.Path).ItemTrashed&ItemPurged: Émis lorsqu’un fichier est placé dans la corbeille ou purgé définitivement.ItemMoved: Émis lors d’un déplacement ou renommage de fichier ou dossier au sein de l’arborescence.
Grâce à cette écoute réactive, le traitement démarre dans la milliseconde suivant la confirmation d’écriture du fichier par le moteur de stockage décomposé (decomposedfs).
2. Architecture Multi-Utilisateur & Authentification Zero Trust
L’un des pièges majeurs lors de l’intégration avec oCIS réside dans son modèle de sécurité Zero Trust. Dans oCIS, chaque utilisateur dispose d’un espace personnel strictement isolé (Personal Space).
Le Rejet de l’Administrateur Global et du Service Account
Dans les systèmes traditionnels, on utilise un compte administrateur ou un compte de service global pour tout lire. Avec oCIS, cette approche est à la fois inefficace et dangereuse :
- Absence de droits transversaux : Le compte
adminsystème ne possède aucun droit de lecture par défaut dans les espaces personnels des utilisateurs. Toute requête WebDAV du compte admin vers/remote.php/dav/files/<user>/...échoue avec une erreur HTTP401 Unauthorizedou403 Forbidden. - Principe de moindre privilège : L’impersonation administrative globale brise les frontières d’étanchéité entre utilisateurs et expose l’ensemble des données en cas de compromission du composant d’ingestion.
Le Choix de l’Authentification HTTP Basic Native avec PAT
Pour garantir une isolation sans compromis, le serveur MCP adopte une authentification HTTP Basic où chaque utilisateur présente son propre nom d’utilisateur ownCloud et son jeton d’accès personnel (Personal Access Token / PAT) :
- Validation immédiate sur la Graph API oCIS : À la première connexion MCP, les identifiants sont validés directement auprès de l’endpoint Graph API (
/meet/me/drives). L’identité du tenant est dérivée de l’ID utilisateur vérifié par ownCloud, jamais d’un paramètre arbitraire passé dans la requête. - Stockage persistant chiffré (
users.sqlite) : Les jetons PAT et la liste des espaces autorisés (space_id) sont conservés localement dans une base SQLite protégée par un chiffrement authentifié AES-256-GCM. La clé de chiffrement de 32 octets (USER_STORE_ENCRYPTION_KEY) est fournie par l’environnement du conteneur. - Aucune fuite de secrets : Les jetons en clair ne sont jamais consignés dans les journaux d’application ni exposés aux agents IA.
3. Partitionnement des Collections Qdrant & Isolation par Espace
L’alternative courante consistant à regrouper tous les documents dans une collection vectorielle unique filtrée par métadonnées présente des risques majeurs : une omission de filtre dans une requête ou une collision d’identifiants sur deux chemins identiques pourrait faire fuiter des documents confidentiels.
L’architecture retient un double niveau d’isolation :
flowchart TD
UserQuery["Requête MCP (search_owncloud_docs)"] --> AuthCheck["Validation Authentification Basic<br>& Dérivation ID Tenant"]
AuthCheck --> CollectionSelect["Sélection Collection Qdrant Dédiée<br>user_{hash(user_id)}"]
CollectionSelect --> SpaceFilter["Application Filtre Obligatoire SpaceId<br>(spaces autorisés dans le store)"]
SpaceFilter --> SearchExec["Recherche Vectorielle Qdrant<br>(Cosinus, Top K=5, Seuil=0.65)"]
- Une collection vectorielle par utilisateur : Chaque utilisateur dispose d’une collection Qdrant distincte nommée à partir d’un hash opaque de son identifiant ownCloud (
user_<hash>). La frontière est garantie au niveau du moteur vectoriel. - Filtrage granulaire par
space_id: Au sein de sa collection, les opérations d’interrogation et d’indexation sont restreintes aux seuls espaces auxquels l’utilisateur a légitimement accès (espace personnel, espaces partagés et espaces de projets).
Déduplication et Ingestion Idempotente
Pour éviter la prolifération de fragments orphelins lors de la modification de fichiers volumineux, le pipeline applique une stratégie d’ingestion strictement idempotente :
- Vérification d’Empreinte (SHA-256) : Avant toute étape d’extraction ou d’embedding, le hachage cryptographique du fichier est calculé. Si le hash stocké dans les métadonnées de Qdrant correspond à la nouvelle version, l’ingestion s’interrompt immédiatement (0 calcul, 0 token gaspillé).
- Purge Préalable Atomique : Si le fichier a changé, tous les points vectoriels associés à son
file_pathounode_idsont supprimés dans Qdrant avant l’insertion des nouveaux fragments. - Suppression Événementielle : Lors de la réception de
ItemTrashedouItemPurged, une requête de suppression par filtre (filter: { must: [{ key: "path", match: { value: event.path } }] }) purge instantanément les données de la collection de l’utilisateur concerné.
4. Routage Événementiel Multi-Utilisateur avec NATS
Dans un contexte multi-utilisateur, la réception d’un événement NATS pose une question fondamentale : pour quel utilisateur ce document doit-il être indexé ?
Le sidecar implémente un routage intelligent basé sur le champ SpaceId de l’enveloppe NATS :
sequenceDiagram
autonumber
participant OCIS as ownCloud (NATS)
participant Sidecar as ocis-qdrant-indexer
participant Store as SQLite (AES-256-GCM)
participant Qdrant as Qdrant DB
OCIS->>Sidecar: Événement NATS (UploadReady, SpaceId: "sp-123", Path: "doc.pdf")
Sidecar->>Store: Rechercher utilisateurs autorisés sur "sp-123"
Store-->>Sidecar: Liste des utilisateurs + PATs déchiffrés
loop Pour chaque utilisateur autorisé
Sidecar->>OCIS: Téléchargement WebDAV avec le PAT de l'utilisateur
Sidecar->>Sidecar: Extraction texte & Embeddings BGE-M3
Sidecar->>Qdrant: Upsert dans la collection dédiée user_{hash}
end
- Routage ciblé : Le sidecar interroge le store local pour identifier les utilisateurs dont les droits couvrent le
SpaceIdde l’événement. - Téléchargement authentifié par l’utilisateur : Le fichier est récupéré via l’endpoint
/remote.php/dav/spaces/<spaceId>/<path>en utilisant le PAT spécifique de cet utilisateur. - Réconciliation de repli : Si un événement ancien ou spécifique ne comporte pas de
SpaceIdexploitable, le système déclenche une réconciliation sécurisée utilisateur par utilisateur, sans jamais déduire l’appartenance d’un document à partir de son seul nom de fichier.
5. Vectorisation avec Cloudflare Workers AI & Ingestion Multimodale
Plutôt que de faire tourner un modèle d’embedding lourd (comme Sentence-Transformers ou Ollama) sur le processeur du VPS — ce qui créerait des pics de charge et de latence pour les autres services hébergés —, la vectorisation est déléguée à Cloudflare Workers AI.
Le pipeline s’appuie sur le modèle multilingue @cf/baai/bge-m3 :
- Dimensionnalité : 1024 dimensions avec métrique cosinus.
- Fenêtre de contexte : jusqu’à 8 192 tokens en entrée.
- Cache de tokenizer local : Un cache persistant monté dans
/tokenizer-cachepermet au découpeur Markdown de comptabiliser exactement les tokens du modèle lors du découpage hiérarchique, évitant les troncatures inopinées. - Support multilingue : performances remarquables sur les documents techniques rédigés en français et en anglais.
Analyse de Rentabilité (Free Tier Cloudflare)
L’avantage économique de ce choix est déterminant :
- Cloudflare offre 10 000 Neurons gratuits par jour sur sa plateforme Workers AI.
- Le modèle BGE-M3 consomme environ 1 075 Neurons par million de tokens en entrée.
- Cela représente un quota quotidien gratuit d’environ 9,3 millions de tokens, soit l’équivalent de plus de 18 000 pages de texte indexées chaque jour sans débourser un centime et sans consommer 1 Mo de RAM sur le serveur hôte.
Extraction Multimodale & Repli OCR
Pour traiter l’hétérogénéité des documents d’entreprise :
- Les documents textuels (Markdown, texte brut, PDF avec couche texte native, fichiers Office) sont extraits via Apache Tika.
- Pour les scans de documents ou images dépourvus de couche textuelle, le sidecar sollicite en repli un conteneur dédié PaddleOCR CPU (
ghcr.io/frflo/ocis-paddleocr:latest) déployé sur le réseau interneresources, assurant une extraction optique précise sans recours à une API cloud tierce.
6. Exposition MCP : Streamable HTTP, Synchronisation Arrière-Plan et Recherche
Une fois les documents découpés et stockés dans les collections vectorielles, les agents d’intelligence artificielle (tels que Hermes Agent, Pi ou Claude) interagissent avec le système via le Model Context Protocol (MCP) en Streamable HTTP sur POST /mcp.
Découplage de la Synchronisation (sync_owncloud_docs)
Dans les premières versions, le déclenchement d’une synchronisation manuelle bloquait l’appel MCP pendant toute la durée du scan WebDAV et des téléchargements. Dans des environnements d’exécution stricts (comme le runtime QuickJS d’Executor), ce comportement provoquait des dépassements de délais (timeouts) côté client (Internal tool error [5225f429]).
Le serveur MCP sépare désormais strictement le contrôle et l’exécution :
- Exécution asynchrone non-bloquante : L’outil
sync_owncloud_docsdémarre l’indexation dans un worker en arrière-plan et répond immédiatement{ status: "started" }. Si une synchronisation est déjà active, il retourne{ status: "already_running" }. - Synchronisation initiale automatique : Dès la première authentification réussie d’un utilisateur, une synchronisation de fond est automatiquement planifiée pour indexer l’arborescence initiale sans bloquer la session.
- Recherche sémantique contextualisée : L’outil
search_owncloud_docseffectue la recherche vectorielle cosinus sur la collection dédiée de l’utilisateur avec filtrage par espace, restituant les extraits pertinents, le chemin WebDAV, le score de similarité et les métadonnées de page.
Gestion Robuste du Transport Streamable HTTP
Pour garantir une communication multi-requêtes fiable avec @modelcontextprotocol/sdk, le serveur implémente un gestionnaire de sessions étanche associant chaque mcp-session-id à une instance dédiée :
const sessions = new Map< string, { server: McpServer; transport: WebStandardStreamableHTTPServerTransport }>();
export function handleMcpRequest(req: Request): Promise<Response> { const sessionId = req.headers.get("mcp-session-id") ?? crypto.randomUUID(); let session = sessions.get(sessionId);
if (!session) { const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: () => sessionId, }); const server = createMcpServerInstance(); server.connect(transport); session = { server, transport }; sessions.set(sessionId, session);
transport.onclose = () => { sessions.delete(sessionId); }; }
return session.transport.handlePostMessage(req);}Conclusion
En associant l’écoute événementielle NATS d’ownCloud Infinite Scale à un modèle de sécurité multi-utilisateur strict (collections Qdrant partitionnées, chiffrement AES-256-GCM des PATs et authentification HTTP Basic native), ce pipeline résout l’équation complexe du RAG personnel et collaboratif.
L’infrastructure garantit une étanchéité absolue entre utilisateurs, une réactivité instantanée à la moindre modification documentaire, et une sobriété de ressources maximale grâce au déport des embeddings sur Cloudflare Workers AI et à la synchronisation non-bloquante via MCP.