GuideAvancé

RAG Multi-Tenant : Architecture SaaS pour Servir 1000 Clients avec un Seul Système

22 juillet 2026
22 min de lecture
Équipe Ailog

Concevez une architecture RAG multi-tenant scalable. Isolation des données, performance, sécurité et optimisation des coûts pour un SaaS RAG servant des centaines de clients.

RAG Multi-Tenant : Architecture SaaS pour Servir 1000 Clients avec un Seul Système

Construire un chatbot RAG pour un client, c'est simple. Le faire tourner pour 1000 clients simultanément sur la même infrastructure tout en garantissant une isolation totale des données ? C'est un tout autre défi. Ce guide détaille les architectures, patterns et pièges du RAG multi-tenant, basé sur l'expérience d'Ailog qui sert des centaines de clients depuis une infrastructure partagée.

TL;DR

  • Multi-tenancy = un seul système RAG servant plusieurs clients avec isolation des données
  • Trois stratégies d'isolation : namespace/partition, collection dédiée, base dédiée
  • Le choix critique : compromis entre isolation, coût et complexité opérationnelle
  • Qdrant : payload filtering (le plus courant), collections séparées, ou instances séparées
  • Coût : de $0.50/tenant/mois (shared) à $50/tenant/mois (dédié)
  • Sécurité : la fuite de données entre tenants est le risque n.1

Pourquoi le multi-tenancy ?

Sans multi-tenancy, chaque nouveau client nécessite une infrastructure dédiée. C'est un cauchemar opérationnel et financier.

Approche10 clients100 clients1000 clients
Infrastructure dédiée$500/mois$5 000/mois$50 000/mois
Multi-tenant partagé$200/mois$400/mois$2 000/mois
Économie60%92%96%
Infrastructure dédiée (1 système par client) :
┌──────────┐ ┌──────────┐ ┌──────────┐     ┌──────────┐
│ Client A │ │ Client B │ │ Client C │ ... │Client 999│
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │     │ ┌──────┐ │
│ │Vector│ │ │ │Vector│ │ │ │Vector│ │     │ │Vector│ │
│ │  DB  │ │ │ │  DB  │ │ │ │  DB  │ │     │ │  DB  │ │
│ ├──────┤ │ │ ├──────┤ │ │ ├──────┤ │     │ ├──────┤ │
│ │ LLM  │ │ │ │ LLM  │ │ │ │ LLM  │ │     │ │ LLM  │ │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │     │ └──────┘ │
└──────────┘ └──────────┘ └──────────┘     └──────────┘
     ❌ Ne scale pas. Coût linéaire.

Multi-tenant (1 système, N clients) :
┌─────────────────────────────────────────────────────┐
│                  Système RAG partagé                  │
│                                                       │
│  ┌─────────────────────────────────────────────────┐ │
│  │              Vector Database                     │ │
│  │  ┌─────┐ ┌─────┐ ┌─────┐         ┌─────┐      │ │
│  │  │  A  │ │  B  │ │  C  │  ...    │ 999 │      │ │
│  │  └─────┘ └─────┘ └─────┘         └─────┘      │ │
│  └─────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────┐ │
│  │              LLM Gateway (partagé)               │ │
│  └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
     ✅ Scale. Coût sub-linéaire.

Les trois stratégies d'isolation

Stratégie 1 : Namespace / Payload Filtering

Les données de tous les tenants sont dans la même collection, différenciées par un champ tenant_id.

DEVELOPERpython
from qdrant_client import QdrantClient from qdrant_client.models import ( Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue ) client = QdrantClient(url="http://localhost:6333") # 1. Créer une collection partagée client.create_collection( collection_name="shared_knowledge", vectors_config=VectorParams( size=1536, distance=Distance.COSINE ) ) # 2. Indexer les documents avec tenant_id def index_document(tenant_id: str, doc_id: str, embedding: list, content: str, metadata: dict): """Indexe un document avec le tenant_id comme payload.""" client.upsert( collection_name="shared_knowledge", points=[ PointStruct( id=doc_id, vector=embedding, payload={ "tenant_id": tenant_id, "content": content, **metadata } ) ] ) # 3. Rechercher avec filtre tenant def search_tenant(tenant_id: str, query_embedding: list, top_k: int = 5) -> list: """Recherche limitée aux documents du tenant.""" results = client.search( collection_name="shared_knowledge", query_vector=query_embedding, query_filter=Filter( must=[ FieldCondition( key="tenant_id", match=MatchValue(value=tenant_id) ) ] ), limit=top_k ) return results # Usage results = search_tenant("tenant_abc123", query_embedding) # Retourne UNIQUEMENT les documents de tenant_abc123

Avantages : Simple, économique, pas de gestion de multiples collections. Inconvénients : Isolation logique seulement, performances dégradées si index très large, risque de fuite de données si bug dans le filtrage.

Stratégie 2 : Collection Dédiée par Tenant

Chaque tenant a sa propre collection dans la même instance de base vectorielle.

DEVELOPERpython
class CollectionPerTenantStrategy: """Une collection Qdrant par tenant.""" def __init__(self, qdrant_url: str): self.client = QdrantClient(url=qdrant_url) def create_tenant(self, tenant_id: str, vector_size: int = 1536): """Crée une collection dédiée pour un nouveau tenant.""" collection_name = f"tenant_{tenant_id}" self.client.create_collection( collection_name=collection_name, vectors_config=VectorParams( size=vector_size, distance=Distance.COSINE ) ) return collection_name def delete_tenant(self, tenant_id: str): """Supprime toutes les données d'un tenant.""" collection_name = f"tenant_{tenant_id}" self.client.delete_collection(collection_name) def index_document(self, tenant_id: str, doc_id: str, embedding: list, content: str): """Indexe un document dans la collection du tenant.""" collection_name = f"tenant_{tenant_id}" self.client.upsert( collection_name=collection_name, points=[ PointStruct( id=doc_id, vector=embedding, payload={"content": content} ) ] ) def search(self, tenant_id: str, query_embedding: list, top_k: int = 5): """Recherche dans la collection du tenant.""" collection_name = f"tenant_{tenant_id}" return self.client.search( collection_name=collection_name, query_vector=query_embedding, limit=top_k )

Avantages : Bonne isolation, performances prévisibles par tenant, suppression facile des données. Inconvénients : Plus de collections à gérer, surcharge mémoire, limite du nombre de collections.

Stratégie 3 : Base de Données Dédiée par Tenant

Chaque tenant a sa propre instance de base vectorielle. Réservé aux grands comptes.

DEVELOPERpython
class DedicatedInstanceStrategy: """Une instance Qdrant dédiée par tenant (ou pool).""" def __init__(self): self.instances = {} def provision_tenant(self, tenant_id: str, tier: str = "standard") -> str: """Provisionne une instance dédiée.""" config = self._get_tier_config(tier) # Kubernetes / Docker provisioning instance_url = self._deploy_instance( tenant_id=tenant_id, cpu=config["cpu"], memory=config["memory"], storage=config["storage"] ) self.instances[tenant_id] = { "url": instance_url, "client": QdrantClient(url=instance_url), "tier": tier } return instance_url def _get_tier_config(self, tier: str) -> dict: """Configuration par niveau de service.""" tiers = { "starter": {"cpu": "0.5", "memory": "512Mi", "storage": "5Gi"}, "standard": {"cpu": "1", "memory": "2Gi", "storage": "20Gi"}, "enterprise": {"cpu": "4", "memory": "8Gi", "storage": "100Gi"}, } return tiers[tier]

Avantages : Isolation totale, performances garanties, conformité réglementaire. Inconvénients : Coût élevé, complexité opérationnelle, ressources sous-utilisées.

Comparaison des stratégies

CritèreNamespace/FilteringCollection DédiéeInstance Dédiée
Isolation des donnéesLogiqueForteTotale
Risque de fuiteMoyenFaibleQuasi-nul
Coût par tenant (100 docs)~$0.50/mois~$2/mois~$20/mois
Coût par tenant (10k docs)~$5/mois~$8/mois~$50/mois
PerformanceDégradée à l'échelleBonneExcellente
Max tenants10 000+1 000-5 000100-500
Suppression donnéesComplexeSimpleTrès simple
Conformité RGPDDifficileBonneExcellente
Complexité opsFaibleMoyenneÉlevée
Cas d'usageSaaS self-serviceSaaS ProGrands comptes

Matrice de décision

Nombre de tenants ?
├── > 5 000 → Namespace/Filtering (obligatoire)
├── 500 - 5 000 → Collection Dédiée
├── < 500
│   ├── RGPD strict / données sensibles → Instance Dédiée
│   ├── Budget limité → Namespace/Filtering
│   └── Standard → Collection Dédiée
└── < 10 (grands comptes) → Instance Dédiée

Architecture complète multi-tenant

DEVELOPERpython
from enum import Enum from typing import Optional import hashlib class IsolationLevel(Enum): SHARED = "shared" # Namespace/Filtering COLLECTION = "collection" # Collection par tenant DEDICATED = "dedicated" # Instance dédiée class MultiTenantRAGService: """Service RAG multi-tenant avec routing automatique.""" def __init__(self): self.shared_client = QdrantClient(url="http://qdrant-shared:6333") self.tenant_registry = {} # tenant_id -> config def register_tenant(self, tenant_id: str, plan: str, isolation: IsolationLevel = None): """Enregistre un nouveau tenant.""" if isolation is None: isolation = self._determine_isolation(plan) config = { "plan": plan, "isolation": isolation, "created_at": datetime.utcnow(), } if isolation == IsolationLevel.COLLECTION: collection_name = f"tenant_{tenant_id}" self.shared_client.create_collection( collection_name=collection_name, vectors_config=VectorParams( size=1536, distance=Distance.COSINE ) ) config["collection"] = collection_name elif isolation == IsolationLevel.DEDICATED: instance_url = self._provision_dedicated(tenant_id, plan) config["instance_url"] = instance_url config["client"] = QdrantClient(url=instance_url) self.tenant_registry[tenant_id] = config def _determine_isolation(self, plan: str) -> IsolationLevel: """Détermine le niveau d'isolation selon le plan.""" plan_mapping = { "free": IsolationLevel.SHARED, "starter": IsolationLevel.SHARED, "pro": IsolationLevel.COLLECTION, "business": IsolationLevel.COLLECTION, "enterprise": IsolationLevel.DEDICATED, } return plan_mapping.get(plan, IsolationLevel.SHARED) async def query(self, tenant_id: str, question: str, query_embedding: list) -> list: """Recherche des documents pour un tenant spécifique.""" config = self.tenant_registry[tenant_id] isolation = config["isolation"] if isolation == IsolationLevel.SHARED: return self.shared_client.search( collection_name="shared_knowledge", query_vector=query_embedding, query_filter=Filter(must=[ FieldCondition( key="tenant_id", match=MatchValue(value=tenant_id) ) ]), limit=5 ) elif isolation == IsolationLevel.COLLECTION: return self.shared_client.search( collection_name=config["collection"], query_vector=query_embedding, limit=5 ) elif isolation == IsolationLevel.DEDICATED: return config["client"].search( collection_name="knowledge", query_vector=query_embedding, limit=5 ) async def delete_tenant_data(self, tenant_id: str): """Supprime toutes les données d'un tenant (RGPD).""" config = self.tenant_registry[tenant_id] isolation = config["isolation"] if isolation == IsolationLevel.SHARED: # Suppression par filtre (plus lent) self.shared_client.delete( collection_name="shared_knowledge", points_selector=Filter(must=[ FieldCondition( key="tenant_id", match=MatchValue(value=tenant_id) ) ]) ) elif isolation == IsolationLevel.COLLECTION: self.shared_client.delete_collection(config["collection"]) elif isolation == IsolationLevel.DEDICATED: self._deprovision_dedicated(tenant_id) del self.tenant_registry[tenant_id]

Isolation des performances

L'isolation des données ne suffit pas. Un tenant gourmand ne doit pas dégrader les performances des autres.

Rate Limiting par Tenant

DEVELOPERpython
from datetime import datetime, timedelta from collections import defaultdict import asyncio class TenantRateLimiter: """Rate limiter par tenant.""" def __init__(self): self.limits = { "free": {"rpm": 10, "rpd": 100, "tokens_per_day": 50000}, "starter": {"rpm": 30, "rpd": 1000, "tokens_per_day": 500000}, "pro": {"rpm": 100, "rpd": 10000, "tokens_per_day": 5000000}, "enterprise": {"rpm": 500, "rpd": 100000, "tokens_per_day": 50000000}, } self.usage = defaultdict(lambda: { "minute": [], "day": [], "tokens_today": 0 }) async def check_and_consume(self, tenant_id: str, plan: str, tokens: int = 0) -> bool: """Vérifie et consomme le quota.""" limits = self.limits[plan] usage = self.usage[tenant_id] now = datetime.utcnow() # Nettoyer les anciens compteurs usage["minute"] = [ t for t in usage["minute"] if now - t < timedelta(minutes=1) ] usage["day"] = [ t for t in usage["day"] if now - t < timedelta(days=1) ] # Vérifier les limites if len(usage["minute"]) >= limits["rpm"]: return False # Rate limit minute if len(usage["day"]) >= limits["rpd"]: return False # Rate limit jour if usage["tokens_today"] + tokens > limits["tokens_per_day"]: return False # Token limit # Consommer usage["minute"].append(now) usage["day"].append(now) usage["tokens_today"] += tokens return True

Tableau des limites par plan

PlanRequêtes/minRequêtes/jourTokens/jourDocuments maxTaille max/doc
Free1010050K501 MB
Starter301 000500K5005 MB
Pro10010 0005M5 00020 MB
Business30050 00020M50 00050 MB
Enterprise500+100 000+50M+Illimité100 MB

Optimisation des coûts

Modèle de coût par tenant

Coût total par tenant = Stockage + Compute + LLM + Infra

Stockage :
├── Vecteurs : $0.10 / 1M vecteurs / mois
├── Payloads : $0.05 / GB / mois
└── Backup : $0.02 / GB / mois

Compute :
├── Recherche : $0.0001 / requête
├── Indexation : $0.001 / document
└── Reranking : $0.0005 / requête

LLM :
├── Embedding : $0.02 / 1M tokens (text-embedding-3-small)
├── Génération : $0.60 / 1M tokens en sortie (GPT-4o-mini)
└── Évaluation CRAG : $0.001 / requête

Infra partagée (répartie) :
├── Qdrant : $200/mois / 100 tenants = $2/tenant
├── API Gateway : $100/mois / 100 tenants = $1/tenant
└── Monitoring : $50/mois / 100 tenants = $0.50/tenant

Coût estimé par profil de tenant

ProfilDocumentsRequêtes/moisCoût estimé/mois
Micro (vitrine)20500$0.80
Petit (PME)2005 000$3.50
Moyen (e-commerce)2 00050 000$25
Grand (enterprise)20 000500 000$180
Très grand100 000+2M+$800+

Sécurité : prévention des fuites de données

Le risque n.1 : la fuite inter-tenant

DEVELOPERpython
class TenantSecurityMiddleware: """Middleware de sécurité pour prévenir les fuites inter-tenant.""" def __init__(self): self.audit_logger = AuditLogger() async def validate_request(self, request, tenant_id: str): """Valide que la requête est légitime pour ce tenant.""" # 1. Vérifier l'authentification auth_tenant = self._extract_tenant_from_auth(request) if auth_tenant != tenant_id: self.audit_logger.log_security_event( "TENANT_MISMATCH", f"Auth tenant {auth_tenant} != request tenant {tenant_id}" ) raise SecurityError("Tenant mismatch") # 2. Vérifier que le tenant existe et est actif tenant = await self._get_tenant(tenant_id) if not tenant or tenant.status != "active": raise SecurityError("Tenant not found or inactive") # 3. Logger l'accès self.audit_logger.log_access(tenant_id, request.path) return True async def validate_response(self, response, tenant_id: str): """Vérifie qu'aucune donnée d'un autre tenant ne fuite.""" # Vérifier les metadata des documents retournés if hasattr(response, "documents"): for doc in response.documents: doc_tenant = doc.metadata.get("tenant_id") if doc_tenant and doc_tenant != tenant_id: self.audit_logger.log_security_event( "DATA_LEAK_PREVENTED", f"Doc from {doc_tenant} nearly served to {tenant_id}" ) response.documents.remove(doc) return response

Checklist sécurité multi-tenant

ContrôlePrioritéImplémenté ?
Authentification par API key liée au tenantCritique
Filtre tenant_id sur TOUTES les requêtes DBCritique
Validation response (pas de fuite)Critique
Rate limiting par tenantHaute
Audit log de tous les accèsHaute
Chiffrement des données au reposHaute
Tests d'intrusion inter-tenantHaute
Backup isolé par tenantMoyenne
Suppression certifiée (RGPD)Moyenne
Monitoring des anomalies d'accèsMoyenne

FAQ

Comment gérer la migration d'un tenant d'un plan à un autre ?

La migration de plan (ex: shared vers collection dédiée) nécessite de copier les données d'un espace de stockage à l'autre. Procédez en trois étapes : (1) créer la nouvelle collection, (2) copier les données en batch, (3) basculer le routing. Pendant la migration, les deux espaces coexistent. Vérifiez que toutes les données sont correctement copiées avant de supprimer l'ancien espace.

Comment faire le scaling horizontal quand un seul serveur Qdrant ne suffit plus ?

Qdrant supporte le sharding et la réplication native. Configurez un cluster Qdrant avec N noeuds, et les collections seront automatiquement distribuées. Pour le multi-tenant, le sharding par tenant_id est idéal : les données d'un tenant sont sur le même shard, ce qui optimise les performances de filtrage. Pinecone et Weaviate offrent des solutions similaires en managed.

Comment garantir la suppression complète des données d'un tenant (RGPD) ?

Avec la stratégie "collection dédiée", c'est simple : supprimez la collection. Avec "namespace/filtering", c'est plus complexe : il faut supprimer tous les points ayant le tenant_id, puis vérifier qu'aucun résidu ne reste dans les caches ou les backups. Conservez un certificat de suppression avec la date et le nombre de documents supprimés pour la conformité RGPD.

Quel est l'impact du nombre de tenants sur les performances de recherche ?

Avec la stratégie namespace/filtering, les performances de recherche se dégradent quand l'index dépasse 10M de vecteurs (tous tenants confondus). La recherche doit filtrer parmi tous les vecteurs avant de retourner les résultats. Avec des collections dédiées, chaque tenant a un petit index indépendant, donc les performances sont constantes quelle que soit le nombre de tenants.

Comment facturer les tenants de manière précise ?

Tracez trois métriques par tenant : (1) le nombre de documents indexés (stockage), (2) le nombre de requêtes (compute), (3) le nombre de tokens LLM consommés (génération). Stockez ces métriques dans une table de facturation mise à jour en temps réel. La facturation peut être au forfait (limites par plan) ou à l'usage (pay-per-query).

Conclusion

L'architecture multi-tenant est le fondement de tout SaaS RAG viable. Le choix de la stratégie d'isolation impacte directement les coûts, les performances et la sécurité de votre plateforme.

Points clés :

  1. Commencez par namespace/filtering pour les plans gratuits et starter, c'est le plus économique
  2. Collections dédiées pour les plans pro : bon compromis isolation/coût
  3. Instances dédiées uniquement pour les grands comptes avec exigences réglementaires
  4. Le rate limiting est indispensable pour protéger la plateforme des abus
  5. L'audit de sécurité inter-tenant doit être automatisé et continu

Ailog est architecturé nativement en multi-tenant, avec une isolation stricte des données et des performances garanties pour chaque client. Créez votre compte et déployez votre chatbot RAG en quelques minutes sur une infrastructure partagée et sécurisée.

Ressources

Tags

RAGmulti-tenantSaaSarchitectureQdrantPineconesécuritéscalabilité

Articles connexes

Ailog Assistant

Ici pour vous aider

Salut ! Pose-moi des questions sur Ailog et comment intégrer votre RAG dans vos projets !