Concevoir RAG pour 100 millions de documents : shardage, recherche hybride, coûts
Quels changements surviennent lorsque un système RAG atteint des milliards de fragments : ingestion asynchrone, routage, shardage, récupération hybride, réclassement, autorisation, mise en cache et contrôle des coûts.
Mettre en marche une démonstration RAG prend une après-midi. On divise un PDF en fragments, on les intégre, on stocke les vecteurs dans une base de données, on récupère les cinq voisins les plus proches, on les colle dans une requête et on lance un modèle.
Cette méthode fonctionne très bien pour quelques milliers de documents. Lorsque le corpus atteint 100 millions de documents, le système ne fonctionne plus correctement, et la question qui se pose change. Ce n’est plus « quelles bases de données vectorielles choisir ? », mais plutôt : comment réduire des milliards de pièces de preuve potentielles aux 10 ou 20 passages importants, sans nuire à la latence, à la qualité de la recherche, à la disponibilité ou au budget ?
C’est un problème d’ingénierie complètement différent. À cette échelle, RAG est un système de données et de recherche distribué auquel est associé un modèle de langage à l’extrémité.
Ce que le système doit faire
Pour garder la discussion concrète, supposons ces exigences :
- Plus de 100 millions de documents, dans des formats tels que PDF, HTML et DOCX
- Des millions d’utilisateurs qui posent des questions en langue naturelle
- Des citations associées à chaque réponse
- Des documents qui sont mis à jour et supprimés au fil du temps
- Un retraitement rapide, environ 1 à 3 secondes avant le début de la génération
- Une haute disponibilité
- Un traitement asynchrone des documents
Tout se résume à une exigence fondamentale : trouver les 10 à 50 informations appropriées parmi potentiellement des milliards de fragments.
Commencer par les calculs
La première erreur dans la conception de grands systèmes RAG est de considérer un document et un vecteur comme étant la même chose. Supposons :
Documents = 100,000,000
Average chunks/document = 30
Embedding dimensions = 768
Storage/dimension = 4 bytes (float32)
Le nombre de vecteurs est alors :
100,000,000 × 30
= 3,000,000,000 vectors
Trois milliards de vecteurs. L’espace de stockage en nombres flottants bruts nécessaire pour eux est d’environ :
3B × 768 × 4 bytes
≈ 9.2 TB
Et ce ne sont que les chiffres eux-mêmes. Un déploiement réel comprend également :
ANN index structures
chunk text
document metadata
ACL metadata
IDs
lexical indexes
replicas
snapshots
WALs
temporary indexes
operational headroom
Avec deux répliques de l’index vectoriel, vous dépassez déjà 18 TB de vecteurs bruts, sans même compter les structures de recherche. Si les documents comprennent en moyenne 50 fragments et non 30 :
100M documents × 50 chunks
= 5 billion vectors
C’est pourquoi l’expression « nous mettrons tous les fragments dans une base de données vectorielle et lancerons une recherche de similarité » ne décrit pas une architecture à grande échelle. Elle décrit simplement où l’on commence.
Deux plans, séparés
La manière la plus claire d’organiser le système consiste à le diviser en deux plans indépendants, l’un pour l’ingestion et l’autre pour la récupération des données. Cette séparation est importante : un client qui télécharge 20 millions de nouveaux documents ne devrait pas faire passer la latence des requêtes de 300 ms à 8 secondes. L’ingestion est optimisée pour le débit, tandis que la récupération l’est pour la latence ; forcer les deux à passer par le même processus est une façon fiable de se créer des problèmes opérationnels plus tard.
La base de données vectorielle n’est pas votre source de vérité
Conservez les documents originaux dans des services de stockage d’objets peu coûteux tels que S3, GCS ou Azure Blob. La base de données vectorielle est un index, et les indices peuvent être reconstruits. Si vous passez du modèle d’incorporation E1 à E2 dans six mois, vous voulez régénérer les vecteurs à partir des versions originales stockées, plutôt que de tenter de reconstituer le contenu à partir de la base de données vectorielle elle-même.
La versionning des objets est également utile dans ce cas, car une mise à jour peut être enregistrée sous forme de nouvelle version au lieu de détruire l’ancienne. Amazon S3, par exemple, permet de conserver plusieurs versions d’un objet, et les politiques de cycle de vie peuvent déplacer les versions plus anciennes vers des niveaux de stockage moins chers ou les faire expirer.
L’ingestion doit être asynchrone
Supposons qu’un client télécharge 500 000 PDF. Vous ne souhaitez pas une requête POST /documents qui analyse, exécute du OCR, nettoie les fichiers, les divise en parties, les intégre, les indexe avant de renvoyer un 200. Une telle opération pourrait durer des heures. Au lieu de cela, l’endpoint ne devrait faire que très peu de choses :
POST /documents
│
▼
Store document
│
▼
Create ingestion event
│
▼
Return 202 Accepted
Tout le reste se déroule en parallèle, poussé par des événements. Kafka constitue une option intéressante pour cette structure, car les sujets sont divisés en partitions que les consommateurs peuvent traiter simultanément, et l’ordre est préservé au sein de chaque partition, ce qui est pratique lorsque des événements partageant une clé de document ou de locataire nécessitent une séquence prévisible.
L’outil spécifique importe moins que la fonctionnalité qu’il offre : l’ingestion requiert un tampon. Si votre service d’incorporation traite 30 000 fragments par seconde et qu’il doit soudainement gérer 100 000 fragments par seconde, le système doit pouvoir accumuler les tâches en attente sans tomber en panne.
Rendre chaque étape d’ingestion idempotente
À cette échelle, les tâches s’exécutent deux fois. Les messages sont réexpédiés. Les travailleurs meurent après avoir écrit des données mais avant d’avoir confirmé la tâche. Les requêtes expirent même si le côté distant a terminé le travail. Prévoyez tout cela dès le début en dérivant des identifiants déterministes à partir d’entrées stables :
document_id
document_version
chunk_index
embedding_model_version
Un identifiant de bloc peut alors être un hash des éléments qui le définissent :
hash(
tenant_id +
document_id +
document_version +
chunk_index
)
Le fait de recevoir deux fois le même événement d’indexation ne génère plus de contenu dupliqué, car
upsert(chunk_id, ...)
Il est sûr de réessayer. Il est difficile d’assurer une exécution exactement une seule fois. Une exécution idempotente est généralement l’objectif plus réaliste.
Le découpage en blocs est une décision d’infrastructure
Sur un petit corpus, changer la taille des blocs semble inoffensif. Avec des milliards de vecteurs, cela coûte réellement de l’argent. Prenons 100 millions de documents : une stratégie qui génère 20 blocs par document donne 2 milliards de vecteurs, tandis qu’une autre qui en génère 60 en donne 6 milliards. Un changement qui semble anodin a simplement triplé la quantité totale :
embedding compute
vector storage
indexing work
retrieval fan-out
reindexing time
replication traffic
Ainsi, le découpage n’est pas simplement un réglage en NLP. C’est un paramètre de planification des capacités.
Chaque morceau doit également contenir suffisamment de métadonnées pour permettre de le retracer jusqu’à son origine :
{
"chunk_id": "c_982...",
"document_id": "doc_51...",
"document_version": 8,
"tenant_id": "tenant_17",
"page": 43,
"section": "Risk Factors",
"language": "en",
"created_at": "...",
"acl_groups": ["finance", "executives"],
"embedding_version": "embed_v4"
}
Le texte lui-même peut être stocké ailleurs, en fonction de votre stratégie de stockage, mais la traçabilité doit être préservée lors du retrait des données. Sinon, les citations deviendront problématiques tôt ou tard.
Ne recherchez pas l’ensemble du corpus sauf si c’est absolument nécessaire
Ce pourrait être l’idée la plus importante de toute cette conception. Prenons la question : « Qu’a changé dans notre politique de remboursement en Europe ce trimestre ? » Une configuration naïve consiste simplement à rechercher dans les 3 000 000 000 de vecteurs. Mais la question nous en dit déjà beaucoup :
organization = current tenant
region = Europe
topic = refund policy
time = current quarter
permissions = documents user may access
Une architecture de récupération de sons utilise ces contraintes de manière agressive, réduisant des milliards de fragments à une petite partie avant même que toute recherche de similarité ne commence. Les chiffres exacts dépendent de la charge de travail, il convient donc de les considérer comme des exemples illustratifs. Le principe reste cependant le même : router en premier, récupérer ensuite.
Le shardage rend la couche vectorielle distribuée
À un certain stade, un seul nœud ne peut plus contenir l’ensemble du corpus pouvant être recherché, et l’index doit être partitionné. Les moteurs vectoriels distribués tels que Qdrant s’échelonnent horizontalement en répartissant les points sur plusieurs shards, avec une réplication qui place des copies de ces shards sur plusieurs nœuds afin d’assurer la disponibilité et une capacité de lecture supplémentaire.
Cependant, il y a un piège. Si chaque requête doit être distribuée à chaque shard, l’ajout de shards entraîne finalement plus de travail de coordination au lieu de le réduire. Une meilleure conception permet à autant de requêtes que possible d’être acheminées. Des attributs comme ceux-ci peuvent vous indiquer quels shards doivent réellement participer :
tenant_id
region
document namespace
language
time partition
Cela est particulièrement avantageux dans les systèmes multi-locataires. Au lieu de
Query
↓
128 shards
une requête pourrait être traitée
tenant_id = 429
↓
Shard group 18
↓
4 shards
Le design distribué actuel de Qdrant, à titre d’exemple, prend en charge le shardage défini par l’utilisateur, ce qui permet aux opérations de cibler des clés de shard spécifiques et d’éviter d’affecter toute la collection. Cette leçon s’applique quel que soit le fournisseur : une bonne partitionnement réduit la diffusion des requêtes.
La réplication répond à une question différente
Les gens confondent le shardage et la réplication, mais ils résolvent des problèmes distincts. Le shardage permet de diviser un ensemble de données trop volumineux pour une seule machine. La réplication répond à la question de ce qui se passe lorsque l’une de ces machines disparaît. Imaginez cette architecture :
Shard A → Node 1
Shard B → Node 2
Shard C → Node 3
Si le nœud 2 tombe en panne, une partie du corpus disparaît avec lui. Avec la réplication :
Shard A → Node 1 + Node 4
Shard B → Node 2 + Node 5
Shard C → Node 3 + Node 6
Le système continue de répondre aux requêtes même en cas de défaillance de certains nœuds. La redondance entraîne des coûts en termes d’espace de stockage, de bande passante pour les écritures et d’efforts opérationnels, mais un cluster de recherche en production sans elle présente des modes de défaillance problématiques.
La recherche vectorielle seule ne suffit pas
Imaginez que quelqu’un demande : « Qu’est-ce qui est indiqué dans la politique HR-48291 ? » La recherche sémantique pourrait retourner des informations générales sur les politiques RH, tandis que la recherche lexicale est excellente pour identifier précisément le terme HR-48291. Inversez maintenant la question : « Les employés peuvent-ils prendre des congés supplémentaires après avoir adopté un enfant ? » Ici, la recherche sémantique se révèle utile. Les deux approches se recoupent mais couvrent des domaines différents, c’est pourquoi une étape de recherche hybride, combinant les deux méthodes, est généralement choisie.
Les directives actuelles d’Elastic concernant la recherche hybride suivent la même approche, en combinant la récupération lexicale et vectorielle et en proposant la Fusion de rangs réciproques comme méthode pour fusionner les classements. Une propriété intéressante de la RRF est qu’il n’est pas nécessaire de prétendre que un score BM25 et une similarité cosinus sont des valeurs comparables ; on combine plutôt les positions dans le classement. En version simplifiée :
RRF(d) = Σ 1 / (k + rank_i(d))
où rank_i(d) est la position du document d dans le système de recherche i.
Faites attention au préfiltrage séquentiel
Il existe un raccourci tentant :
3B documents
↓
BM25 returns 10,000
↓
vector search only those
Cela peut fonctionner pour certains types de charges de travail, mais il peut également détruire la capacité de rappel sémantique. Imaginez un utilisateur qui tape « Quelles règles s’appliquent aux employés qui adoptent ? » et un document indiquant : « Les employés qui accueillent un enfant par placement hors naissance ont droit à seize semaines de congé familial. » La correspondance entre les mots-clés de ce document et la formulation de l’utilisateur est faible. Si BM25 le rejette avant même que la récupération sémantique ne puisse l’examiner, aucun modèle d’embedding ne pourra le sauver.
Ainsi, en raison des contraintes de routage strictes, il est préférable de générer en parallèle des candidats à partir des deux systèmes de récupération, puis de réclasser l’ensemble ainsi obtenu. Ces contraintes strictes forment une catégorie à part entière :
tenant
permissions
document status
Ces exigences devraient normalement être respectées avant le début du récupération des données.
L’autorisation est requise avant même que le LLM ne voie quoi que ce soit
Imaginez un corpus d’entreprise contenant :
Engineering documents
HR documents
Board documents
Legal documents
Executive compensation
Customer contracts
La pertinence sémantique ne constitue pas une autorisation. Un document d’un directeur financier peut être mathématiquement le plus proche de la question d’un employé, mais cela ne lui donne pas pour autant le droit de le lire. Une conception erronée se présente ainsi :
Vector Search
↓
Retrieve confidential chunk
↓
Send chunk to LLM
↓
Check permission
Lorsque la vérification des permissions est effectuée, le texte confidentiel a déjà été transféré vers une composante ultérieure. Le flux correct commence par déterminer qui est l’utilisateur :
User
↓
Identity
↓
Groups / roles / ACL
↓
Authorized search space
↓
Retrieval
Cela implique que les métadonnées de contrôle d’accès doivent participer efficacement à la récupération des données. À très grande échelle, un schéma ACL qui doit scanner des millions d’enregistrements après la récupération devient à la fois un problème de sécurité et un problème de performance.
La récupération produit des candidats, pas du contexte
Supposons qu’une méthode de récupération hybride génère 400 candidats. Ne placez pas ces 400 éléments dans la requête. Classez-les d’abord par ordre de pertinence :
Vector candidates: 200
Lexical candidates: 200
│
▼
Fusion
│
▼
~250 unique chunks
│
▼
Reranker
│
▼
top 20–40
La première étape de recherche est conçue pour trouver rapidement des éléments plausiblement pertinents. Le réclassificateur, quant à lui, a une fonction différente : il doit évaluer la pertinence avec plus de précision à partir d’un ensemble bien plus restreint. Ce schéma en deux étapes permet à un modèle de pertinence coûteux de fonctionner sur des centaines d’éléments plutôt que sur des milliards.
Puis éliminer la redondance
Même un bon réclassificateur peut vous fournir quelque chose comme ceci :
Chunk 1 → page 14
Chunk 2 → page 14
Chunk 3 → page 15
Chunk 4 → page 14
Chunk 5 → page 15
Tous les cinq pourraient essentiellement exprimer la même idée, et l’envoi de chacun d’eux gaspille des tokens de contexte. Avant de créer la prompt finale, appliquez une combinaison de ces étapes :
deduplication
document diversity
section diversity
near-duplicate detection
adjacent-chunk merging
token budgeting
Parfois, une réponse nécessite également ses voisins. Si le bloc 47 contient la réponse mais commence au milieu d’une explication, l’outil de construction de contexte peut les récupérer.
chunk 46
chunk 47
chunk 48
au lieu d’espérer que la recherche vectorielle récupérera de manière indépendante chaque segment contigu.
La construction du contexte mérite sa propre couche
Une implémentation surprenamment courante considère le contexte comme rien de plus que ceci :
context = "\n\n".join(retrieved_chunks)
Les systèmes de production fonctionnent mieux avec un constructeur de contexte dédié. Sa tâche est d’analyser des éléments de preuve classés et de générer un en-tête de requête efficace sans perdre les informations de citation. Conceptuellement :
Retrieved Evidence
↓
Deduplicate
↓
Merge related chunks
↓
Enforce token budget
↓
Preserve source IDs
↓
Order evidence
↓
Generate context
Le modèle pourrait alors recevoir quelque chose de structurellement similaire à ceci :
SOURCE S1
Document: Employee Handbook
Page: 42
Version: 18
Text: ...
SOURCE S2
Document: European Leave Addendum
Page: 7
Version: 4
Text: ...
QUESTION
...
On nous demande alors de citer S1, S2 et ainsi de suite. C’est bien plus sûr que de demander à un modèle de recréer des URL ou des identifiants de documents à partir de sa mémoire.
La compréhension des requêtes doit être rapide
Certaines requêtes profitent d’une normalisation avant la récupération des informations. Un utilisateur tape « De combien la chiffre d’affaires a-t-il augmenté l’année dernière en Europe ? » Compte tenu de la date actuelle et de la terminologie propre à l’entreprise, la récupération peut être plus efficace avec des champs structurés extraits de cette phrase :
metric = revenue
region = Europe
year = 2025
intent = financial comparison
Dans une conversation, une question comme « Et l’Asie ? » doit être réécrite en tenant compte du contexte précédent :
How much did revenue grow in Asia in 2025?
L’idée principale est que ces étapes n’ont que rarement besoin de votre modèle de raisonnement le plus coûteux. Utilisez le composant le moins cher qui remplit la tâche de manière fiable. Une pile d’IA mature ressemble généralement à ceci :
small model
↓
classification
small model
↓
query rewriting
embedding model
↓
retrieval
reranker
↓
relevance
large model
↓
final reasoning
et pas comme ça :
largest model everywhere
Décomposition pour les questions multi-étapes
Certaines questions ne peuvent pas être répondues de manière fiable à partir d’un seul passage. Une question telle que « quel produit européen a connu la plus forte baisse de chiffre d’affaires, et comment la direction l’a-t-elle expliquée ? » se divise en deux recherches :
Question 1:
Which European product had the largest decline?
Question 2:
What explanation did management provide for that product?
La récupération peut alors collecter des preuves pour chaque sous-question séparément, et les résultats sont fusionnés avant la génération.
Complex Query
│
▼
Query Decomposer
/ \
/ \
▼ ▼
Subquery A Subquery B
│ │
▼ ▼
Retrieval Retrieval
\ /
\ /
▼ ▼
Evidence Join
│
▼
LLM
À ce stade, le RAG à grande échelle commence à ressembler davantage à un système de planification de requêtes qu’à une simple recherche du voisin le plus proche.
La fraîcheur modifie la manière d’indexer
Supposons que le corpus contienne 100 millions de documents et que 2 millions d’entre eux changent chaque jour. Réindexer chaque nuit n’est probablement pas ce que vous souhaitez, il vous faut donc un indexage incrémental :
Document updated
│
▼
new document version
│
▼
parse changed document
│
▼
generate new chunks
│
▼
embed
│
▼
write new index entries
│
▼
mark previous version inactive
La versionning logique est préférable à un remplacement destructif immédiat. Par exemple, pour un document :
document_id = D17
version 41 → status=inactive
version 42 → status=active
Filtre des requêtes
status = active
Les anciennes entrées peuvent être collectées automatiquement en arrière-plan. Du point de vue de l’application, la mise à jour s’effectue de manière atomique, sans qu’il soit nécessaire que tous les systèmes de stockage changent en même temps.
Versionnez également les modèles
Tôt ou tard, vous changerez de modèles d’incorporation, et vous ne devez jamais stocker des vecteurs sans savoir ce qui les a générés. Au minimum, assurez-vous que ceux-ci soient traçables :
embedding_model
embedding_version
embedding_dimensions
chunking_version
parser_version
Sinon, six mois plus tard, vous pourriez constater que 15 % d’un index a été créé à l’aide d’une méthode plus ancienne, sans moyen simple de déterminer quels sont ces 15 %. Une migration peut être effectuée sur des index en parallèle :
Documents
│
┌─────────┴─────────┐
▼ ▼
embedding model V3 embedding model V4
│ │
▼ ▼
Index V3 Index V4
│
▼
shadow traffic
│
▼
evaluate
│
▼
switch alias
Ne transférez pas des milliards de vecteurs en espérant simplement que la nouvelle méthode de recherche sera meilleure. Mesurez les résultats avant de faire le changement.
Caché à plusieurs niveaux
Toute requête ne mérite pas d’être traitée selon l’ensemble du processus. Si des milliers d’employés demandent "Combien de jours de congé avons-nous ?", il n’est pas nécessaire d’exécuter
embedding
→ distributed retrieval
→ reranking
→ large LLM
Chaque utilisation est une perte de temps. Une série de caches en couches est plus judicieuse :
User Query
│
▼
Exact Response Cache
│ miss
▼
Semantic Cache
│ miss
▼
Retrieval Cache
│ miss
▼
Search
Un cache d’incrustation est l’accompagnement évident :
hash(normalized_query)
↓
query embedding
Les choses deviennent compliquées lorsque le contenu change. Une réponse mémorisée en fonction de
Policy version 17
peut se tromper une fois
Policy version 18
Il passe en mode actif. La clé de cache ou le schéma d’invalidation doit donc connaître les limites du corpus et des versions. Stocker les résultats du modèle en cache sans règles de fraîcheur est dangereux.
Prévoir les échecs, pas seulement le scénario idéal
Les grands systèmes échouent progressivement. S’attendre à des situations telles que :
embedding provider unavailable
one vector shard unavailable
search cluster degraded
reranker timeout
LLM rate limited
LLM provider outage
queue backlog growing
parser crashing on malformed PDF
OCR worker running out of memory
Chaque échec nécessite une politique explicite. Par exemple :
Vector search fails
↓
Can lexical search answer?
↓
yes
↓
return degraded retrieval mode
ou :
Primary LLM unavailable
↓
Fallback model
↓
Lower quality but service continues
ou :
Document parser fails 5 times
↓
Dead-letter queue
↓
record failure reason
↓
operator / automated remediation
Tenter sans cesse n’est pas de la résilience. Parfois, il s’agit simplement d’une boucle infinie accompagnée d’une facture cloud.
Backpressure
Imaginez que l’ingestion reçoive normalement
20M chunks/hour
et la capacité d’incorporation peut gérer
25M chunks/hour
Tout va bien. Puis un client effectue une migration et génère
200M chunks/hour
Sans contrainte de retour, l’échec se produit de la manière suivante :
embedding service overloaded
↓
timeouts
↓
retries
↓
more load
↓
more failures
↓
retry storm
Avec un tampon devant les travailleurs :
incoming workload
↓
durable queue
↓
workers consume at safe rate
↓
backlog temporarily grows
La capacité peut désormais être ajustée indépendamment du taux d’arrivée. C’est une autre raison pour laquelle le plan de traitement mérite sa propre architecture.
L’observabilité doit couvrir la qualité de récupération
Les rapports de surveillance ordinaires indiquent des éléments tels que :
HTTP 200 rate
CPU
memory
disk
p95 latency
error rate
requests/second
Tout cela est utile, mais un service RAG peut renvoyer un HTTP 200 en 800 ms tout en donnant une réponse complètement fausse. Vous avez également besoin de telemétrie spécifique à l’IA. Un suivi utile par requête pourrait ressembler à ceci :
request_id: rq_912
query rewrite:
"parental policy?" → "parental leave policy"
vector retrieval:
200 candidates
145 ms
lexical retrieval:
200 candidates
91 ms
fusion:
278 unique candidates
reranking:
278 → 20
212 ms
context:
13 chunks
8,420 tokens
LLM:
input 9,104 tokens
output 681 tokens
1.8 sec
citations:
3 documents
total:
2.4 sec
L’évaluation hors ligne nécessite donc des métriques permettant de répondre à des questions telles que : les preuves appropriées ont-elles été retrouvées ? Ont-elles obtenu un rang suffisamment élevé ? La réponse est-elle étayée par ces preuves ? Le réclassement a-t-il suffisamment amélioré les requêtes pour justifier son temps de traitement ? Quelles catégories de requêtes échouent le plus souvent ? Cela offre des pistes d’action plus concrètes que de se contenter de suivre le taux global d’hallucinations.
Évaluer la récupération de données séparément de la génération
Lorsqu’une réponse finale est incorrecte, il existe au moins deux catégories d’échec distinctes. Dans un échec de récupération, les preuves nécessaires n’ont jamais été intégrées dans le contexte :
Correct document exists
↓
retriever misses it
↓
LLM never sees it
↓
wrong answer
Aucune modification rapide du texte ne peut y remédier. Dans le cas d’une échec de génération, les preuves nécessaires sont bien arrivées :
correct evidence
↓
context
↓
LLM
↓
incorrect interpretation
Désormais, la conception des prompts, le choix du modèle, l’organisation du contexte ou la stratégie de raisonnement peuvent être importants. Si vous ne jugez que les réponses finales, ces deux aspects semblent identiques, ce qui oblige un processus d’évaluation rigoureux à examiner les étapes intermédiaires.
Allouez un budget à la latence
« Le système doit être rapide » n’est pas une exigence réelle. Allouez plutôt un budget à chaque étape. Par exemple, si l’objectif est d’obtenir une première réponse en environ 2,5 secondes, on peut répartir le temps comme suit :
API + auth 50 ms
query understanding 100 ms
retrieval 300 ms
fusion 20 ms
reranking 250 ms
context construction 30 ms
LLM time-to-first-token 1,200 ms
network / safety margin 550 ms
-----------------------------------
target ~2,500 ms
Ces chiffres ne sont pas universels. L’exercice est important car il montre où passe réellement le temps. Si le temps de récupération représente 1,8 seconde, changer de fournisseur d’LLM n’aura aucun impact sur le goulot d’étranglement principal. Si l’LLM est responsable de 90 % du retard, passer un mois à réduire de 40 ms la recherche vectorielle risque fort de ne pas être l’utilisation la plus judicieuse des efforts.
Les coûts exigent la même discipline
Un grand système RAG comporte de nombreux centres de coûts distincts :
object storage
document parsing
OCR
embedding generation
vector storage
vector replicas
lexical search
metadata database
network transfer
reranking
LLM input tokens
LLM output tokens
observability
backups
Mesurez donc le coût par unité utile, par exemple :
cost / 1,000 indexed documents
cost / million chunks
cost / query
cost / successful answer
cost / tenant
Le dernier de ces points est le plus important dans le SaaS. Vous pourriez découvrir :
Tenant A
5% of revenue
42% of LLM spend
Sans un comptage par locataire, tout ce que l’on voit, c’est une facture d’infrastructure mensuelle inattendue et élevée.
Réservez le stockage coûteux à ceux qui en ont besoin
Une image mentale utile est celle d’un gradient allant du coûteux et rapide au bon marché et volumineux :
expensive / fast
▲
│
vector indexes
search indexes
Redis caches
│
metadata DB
│
object storage
│
▼
cheap / large
Le PDF complet n’a aucune raison d’être stocké dans l’index vectoriel. Réservez un espace de stockage coûteux uniquement pour ce qui nécessite une recherche, laissez le stockage d’objets conserver les documents originaux, et laissez l’infrastructure de recherche gérer les représentations consultables. Lorsque le corpus atteint des centaines de téraoctets, cette distinction devient très importante.
Données chaudes et froides
Tous les 100 millions de documents ne sont pas consultés de manière égale. Peut-être :
Last 30 days → 70% of queries
Last 12 months → 25%
Older archive → 5%
Traiter tout de la même manière gaspille les infrastructures. Une option consiste en une séparation comme celle-ci :
Query
│
├────→ Hot index
│
└────→ Archive index when necessary
Le routage basé sur le temps empêche également une question concernant les événements de la semaine de remonter à dix ans d’éléments historiques. Les grands systèmes deviennent efficaces en évitant le travail, et non seulement en effectuant plus rapidement des tâches inutiles.
Le multi-tenancy change tout
Disons que votre plateforme sert
10,000 organizations
Vous ne voulez certainement pas
10,000 completely independent vector clusters
Cependant, regrouper tous les locataires dans un seul espace de noms entraîne des problèmes de perturbations entre voisins et de gestion des autorisations. Une approche viable consiste à regrouper les locataires en fonction de leur taille :
Small tenants
↓
shared shard groups
Medium tenants
↓
partitioned shared infrastructure
Very large tenants
↓
dedicated shard groups
Cela empêche un client d’importer 500 millions de blocs, ce qui éviterait de submerger des milliers de blocs plus petits, et cela offre une solution d’isolement pour les clients entreprises qui en ont besoin. Aucune stratégie de partitionnement par locataire ne convient à tout le monde.
L’LLM arrive vers la fin
Regardez combien de aspects techniques nous avons abordés avant d’arriver au modèle. C’est intentionnel. Une requête RAG mature se déroule de cette manière :
User
↓
API Gateway
↓
Authentication
↓
Authorization
↓
Rate Limiter
↓
Query Understanding
↓
Shard Routing
↓
Hybrid Candidate Retrieval
↓
Rank Fusion
↓
Reranker
↓
Deduplication
↓
Context Builder
↓
LLM
↓
Citation Validation
↓
Response
Le LLM est important, mais avec 100 millions de documents, il n’est qu’un des nombreux composants.
La vue d’ensemble de la production
En résumant, voici le parcours de la requête, puis le parcours d’ingestion, ainsi que les systèmes qui les soutiennent tous deux :
CLIENTS
│
▼
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│
┌──────▼───────┐
│ Auth / ACL │
│ Rate Limits │
└──────┬───────┘
│
▼
┌────────────────────┐
│ Query Orchestrator │
└─────────┬──────────┘
│
┌─────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Cache Query Rewrite Metadata
Filters
│ │ │
└─────────────┼──────────────┘
▼
Shard Router
│
┌────────────┴────────────┐
▼ ▼
Lexical Search Vector Search
│ │
└───────────┬─────────────┘
▼
Rank Fusion
│
▼
Reranker
│
▼
Deduplicate
│
▼
Context Builder
│
▼
LLM
│
▼
Citation Validation
│
▼
RESPONSE
================================================================
INGESTION SYSTEM
Documents
│
▼
Object Store
│
▼
Event Queue
│
┌────────────┼────────────┐
▼ ▼ ▼
Parser Parser Parser
│ │ │
└────────────┼────────────┘
▼
Normalization
│
▼
Chunker
│
┌──────────────┴──────────────┐
▼ ▼
Embedding Queue Text Index Queue
│ │
┌─────┼─────┐ ▼
▼ ▼ ▼ Lexical Index
GPU GPU GPU
│ │ │
└─────┼─────┘
▼
Vector Index
================================================================
SUPPORTING SYSTEMS
Metadata DB → document state, versions, ownership
Object Storage → original source documents
Vector Cluster → semantic retrieval
Search Cluster → lexical retrieval
Redis → caches and rate limiting
Message Queue → asynchronous ingestion
Observability → traces, metrics, evaluation
Secrets / IAM → service authorization
Evaluation System → retrieval + answer-quality tests
Anti-modèles à éviter
Lors de l’examen d’une telle architecture, les éléments suivants suscitent immédiatement des inquiétudes :
One giant vector collection with no routing strategy
Synchronous PDF ingestion
Only vector retrieval, no lexical search
ACL filtering after retrieval
No document versioning
No embedding model version
No reranking
Sending top-100 chunks directly to the LLM
Using the LLM for every trivial classification task
No dead-letter queue
No retrieval-level evaluation
No tenant-level cost attribution
Treating the vector DB as permanent document storage
Re-embedding the entire corpus for every small content change
Benchmarking only average latency instead of tail latency
N’importe lequel d’eux peut fournir un prototype. Ensemble, à l’échelle de milliards de vecteurs, ils deviennent extrêmement problématiques.
Le principe fondamental
La manière la plus utile de concevoir un grand RAG n’est pas celle-ci :
question
→ vector search
→ LLM
C’est ça :
billions of possible evidence units
↓
routing constraints
↓
relevant partitions
↓
cheap candidate search
↓
hundreds of items
↓
expensive reranking
↓
tens of passages
↓
context optimization
↓
LLM
↓
grounded answer
Le système fonctionne comme un entonnoir. À chaque étape, nous consacrons plus de ressources de calcul à un nombre réduit de candidats :
Cheap operation → huge search space
Moderate operation → smaller candidate set
Expensive operation → tiny candidate set
Most expensive model → final context only
C’est cette idée qui permet à l’architecture de rester économiquement viable.
Points clés
Un système RAG basé sur quelques milliers de documents relève principalement d’un exercice d’ingénierie logicielle. Lorsqu’il s’agit de 100 millions de documents, il devient une infrastructure de recherche, impliquant des systèmes distribués, de l’ingénierie des données, des mesures de sécurité et un contrôle des coûts, tous nécessitant une attention simultanée. La base de données vectorielle est importante, tout comme le modèle d’embedding et les LLM, mais l’architecture qui les entoure est plus cruciale que la plupart des tutoriels ne le reconnaissent.
Le modèle mental qui fonctionne à grande échelle : stocker les données à bas coût, traiter de manière asynchrone, gérer des versions pour tout, partitionner délibérément, appliquer des autorisations dès le début, récupérer les informations à l’aide de plusieurs signaux, réévaluer rigoureusement, ne fournir au modèle que des données de haute qualité, et mesurer chaque étape séparément. Lorsque le corpus atteint des milliards de fragments, la meilleure optimisation n’est plus d’accélérer la recherche, mais d’organiser les choses de façon à ce que la majeure partie du système n’ait jamais affaire à la majeure partie du corpus.