CobbleDB : Reconstruire le stockage de recherche IA pour réduire la latence et les coûts

Une architecture découplée avec un état persistant séparé, des mises à jour par lots et une récupération au moment de la requête.

AuteursPerplexity Engineering

La recherche native par IA impose des exigences différentes quant à la manière dont le contenu Web doit être traité, stocké et servi. Pour répondre à ces exigences à grande échelle, nous avons reconstruit l'ensemble de la pile à partir de zéro afin d'améliorer les performances et de réduire les coûts.

Nous avons construit notre propre infrastructure d'exploration, d'indexation et de classement, nos propres modèles de représentation vectorielle et nos propres moteurs d'inférence pour alimenter un traitement efficace et une desserte rapide. Nous étendons désormais cette approche au stockage, le tissu conjonctif vers lequel le traitement écrit et à partir duquel la service lit.

Les bases de données gérées telles que DynamoDB offraient un stockage fiable, mais notre contrôle sur la manière dont les données étaient stockées et les lectures servies était limité, et la tarification basée sur l'utilisation augmentait avec la taille de notre index et le volume de nos requêtes. Par ailleurs, notre pipeline précédent associait directement le traitement des documents à la base de données de service, ce qui rendait difficile la gestion des mises à jour indépendamment des lectures en direct.

Avec l'aide d'un nouvel escadron interne d'agents de codage, nous avons reconstruit cette couche autour de CobbleDB, un magasin à accès rapide clé-valeur, pris en charge par Pillar pour l'état persistant des documents et la publication, et par Lorry pour les mises à jour par lots. La reconception sépare le traitement des documents de l'ingestion dans le magasin à accès rapide et nous permet d'ajuster le service à notre charge de travail. Les mesures de production avant-après montrent une latence de lecture par lots environ 5 fois inférieure, et notre modèle de coût interne estime des économies d'au moins 20 % par rapport à DynamoDB.

Infrastructure de stockage pour la recherche native par IA

Une pile de recherche classique est conçue pour un lecteur humain, non pour un modèle de langage. Cette différence modifie la granularité à laquelle le système doit préparer, stocker et desservir le contenu Web.

Pour transformer du code HTML brut en contenu prêt pour les modèles, notre pipeline de traitement nettoie chaque page, la divise en passages sémantiquement cohérents, calcule leurs représentations vectorielles et stocke ensemble les passages et les représentations vectorielles dans la base de données. Au moment de la requête, le pipeline de service sélectionne le texte le plus pertinent par rapport à la requête et le transmet au modèle.

Deux flux de travail distincts

La couche de stockage doit par conséquent prendre en charge deux flux de travail aux exigences différentes : l'ingestion des mises à jour provenant du pipeline de traitement et la desserte de lectures à faible latence.

Du côté de l'écriture, de nouvelles pages arrivent en continu, les pages existantes changent, et des modifications apportées aux méthodes de découpage en blocs ou aux modèles de représentation vectorielle peuvent nécessiter le retraitement de grandes portions du corpus. La couche de stockage doit intégrer ces mises à jour dans l'état persistant du document, suivre les versions actuelles, publier les bonnes représentations vers les systèmes en aval et prendre en charge des remplissages volumineux sans répéter l'intégralité du pipeline à partir de l'exploration brute.

En revanche, les lectures au moment de la requête demandent des lots de clés de page et récupèrent le texte de passage et les représentations vectorielles correspondants pour le classement et la sélection des passages. Ces lectures se situent sur le chemin menant à la réponse finale, et doivent donc être exécutées rapidement.

Ces charges de travail nécessitent des conceptions différentes. Le traitement tire parti de files d'attente persistantes et de la capacité à distribuer efficacement les mises à jour, à relancer les tentatives en cas d'échec et à actualiser les enregistrements existants à grande échelle. Le service tire parti d'enregistrements précalculés, de caches chauds et de lectures par lots rapides avec un travail minimal au moment de la requête. Un couplage étroit entre les deux peut amener le travail de récupération et les mises à jour importantes à entrer en compétition avec les lectures sensibles à la latence.

Là où la base de données gérée fait défaut

DynamoDB nous a fourni un stockage hautement disponible sans la charge d'exploiter une base de données distribuée. Une table dédiée aux pages préparées contenait les passages et les représentations vectorielles (embeddings) pour les lectures au moment de la requête, tandis que le contenu complet des pages et les métadonnées étaient stockés séparément. Cependant, à mesure que notre charge de travail augmentait, trois contraintes sont devenues importantes.

Premièrement, la tarification basée sur l'utilisation facture chaque octet lu ou écrit. Une seule requête d'API de recherche contient 100 à 120 clés correspondant aux pages que nous devons récupérer. Le processus de récupération des données divise une requête en plus petits lots de 10 à 20 clés de page, la taille moyenne des éléments étant d'environ 50 Ko. La lecture répétée de ces enregistrements consommait une capacité substantielle, tandis que les explorations continues et les modifications apportées aux modèles de découpage en blocs ou de représentation vectorielle généraient davantage d'écritures. L'aspect économique dépendait également de la taille et de la fréquence des enregistrements, et non pas seulement du nombre d'appels d'API.

Deuxièmement, le service géré manque de la flexibilité dont nous avons besoin pour l'optimisation des performances. L'opération requise était simple : étant donné un lot d'identifiants de page, renvoyer rapidement leur contenu préparé. Nous souhaitions exercer un contrôle sur la machine hébergeant une partition, sur la quantité de mémoire allouée à son cache et sur le réplica traitant une requête. Ces contrôles importent, car une lecture non mise en cache, un saut interzone ou un réplica lent peuvent retarder l'ensemble du lot. DynamoDB gère son propre stockage et le comportement de ses réplicas en arrière-plan de l'API. Nous pouvions configurer le service, mais nous ne pouvions pas ajuster son placement interne, les caches de son moteur de stockage ou sa politique de sélection des réplicas en fonction de notre charge de travail.

Troisièmement, le traitement était directement lié aux écritures dans le magasin à accès rapide. Notre pipeline de traitement précédent écrivait chaque page préparée directement dans DynamoDB. Une tâche de retraitement importante se transformait donc en une vague de mises à jour individuelles du magasin à accès rapide, et le traitement devait s'adapter aux limites de débit et au comportement de nouvelle tentative du magasin à accès rapide. Nous ne pouvions pas exécuter MapReduce sur l'ensemble de la base de données en 1 ou 2 jours pour tester une nouvelle méthode de découpage en blocs, modifier notre modèle de représentation vectorielle ou ajouter un nouveau champ. Il n'existait aucun flux indépendant de mises à jour de pages enregistrées qu'un service distinct pouvait regrouper par lots et livrer au magasin à accès rapide à son propre rythme.

Répondre à ces contraintes exigeait bien plus que le simple remplacement de la base de données. Nous devions séparer l'état persistant du document et la distribution des mises à jour du stockage au moment de la requête, puis optimiser spécifiquement le magasin à accès rapide pour des lectures par lots rapides.

Séparer l'état du document, la distribution des mises à jour et la desserte

La nouvelle architecture attribue chaque responsabilité à un composant différent. Pillar maintient un état de document persistant et décide de ce qu'il faut publier. Lorry transforme ces exportations en lots partitionnés. CobbleDB ingère les lots et dessert les enregistrements préparés par clé de page.

Avant et après la migration du stockage. Pillar et Lorry séparent l’état durable des documents et la livraison des mises à jour des lectures effectuées au moment de la requête par CobbleDB.

CobbleDB : le chemin de lecture ajustable

CobbleDB est notre magasin à accès rapide optimisé pour la desserte d'enregistrements préparés au moment de la requête. Il s'agit d'un magasin clé-valeur distribué : les clés sont des identifiants de page (URL hachées) et les valeurs correspondent à la représentation traitée de la page (ses passages préalablement découpés en blocs et ses représentations vectorielles par bloc).

Le changement important réside dans notre contrôle sur la configuration du stockage.

Les données sont partitionnées et réparties entre les nœuds de données. Ce format distribué garantit que la base de données peut évoluer horizontalement pour s'adapter à un corpus énorme de pages traitées. Chaque partition possède trois réplicas sur trois nœuds différents ; si une copie devient indisponible, les autres peuvent continuer à servir les lectures.

Chaque nœud de données se voit attribuer un sous-ensemble des partitions. Le nœud stocke ses données à l'aide de RocksDB, un moteur de stockage clé-valeur intégré open source bien adapté aux charges de travail à forte intensité de lecture alimentées par l'ingestion par lots.

Les données mises en cache peuvent être servies à partir de la mémoire, tandis que les lectures non mises en cache utilisent le stockage NVMe local. Cela nous offre un moyen direct d'ajuster l'équilibre entre la mémoire et le disque pour la charge de travail observée, plutôt que d'accepter la politique de cache interne d'un service géré.

Un routeur de requêtes sans état hache les clés pour les répartir dans les partitions et achemine les requêtes vers les nœuds en parallèle. L'architecture comprend également un service de métadonnées adossé à PostgreSQL et un stockage d'objets persistant.

Cette conception prend en charge les schémas de lecture requis par notre charge de travail. Après qu'une requête a traversé les étapes de recherche et de classement pour obtenir un ensemble de pages candidates pertinentes, le magasin à accès rapide est interrogé en bloc pour obtenir le contenu de ces pages. Ces requêtes de lecture arrivent au routeur et sont acheminées vers un nœud de données correspondant. Le routeur donne la priorité à l'envoi des requêtes à un nœud situé dans la même zone de disponibilité afin de minimiser la latence interzone, si possible. Chaque nœud de données peut récupérer le contenu des pages à l'aide de MultiGet, une API de lecture par lots de RocksDB conçue pour récupérer plusieurs clés simultanément avec une faible latence. Si un nœud met du temps à répondre, le système peut également interroger un autre réplica de partition sur un nœud différent, ce qui améliore la latence extrême.

Nous pouvons également éviter d'implémenter des fonctionnalités courantes dont notre flux de travail n'a pas besoin. Par exemple, la plupart des bases de données offrent des garanties de cohérence et une prise en charge des transactions, mais notre magasin à accès rapide ne requiert ni transactions ni réplicas synchronisés. Il est acceptable qu'il y ait un court décalage entre le moment où un document est écrit et celui où il est disponible pour la lecture, et il est également tout à fait acceptable qu'un réplica ingère des données plus rapidement qu'un autre. Omettre des fonctionnalités superflues réduit les frais généraux et le coût de notre système.

Un routeur sans état regroupe les clés de page par partition et lit les répliques en parallèle. Les lectures lentes peuvent être envoyées à une autre réplique; la première réponse pour chaque partition est fusionnée dans le résultat.

Pillar et Lorry : le chemin d'écriture persistant

Une couche de stockage séparée garde la trace de tout ce que nous explorons et traitons, fusionnant les mises à jour dans un état de document versionné et publiant les représentations nécessaires vers CobbleDB et l'index de recherche. Nous avons conçu Pillar, notre couche d'état de document et de publication, à cette fin.

Pillar s'appuie sur YTsaurus. Il stocke les composants des pages (tels que les métadonnées, les blocs et les représentations vectorielles) dans des familles de tables YTsaurus distinctes. Il effectue également le suivi des versions des blocs et des représentations vectorielles, ce qui permet à de multiples représentations de coexister sans s'écraser mutuellement pour permettre l'évolution des modèles sous-jacents. Étant donné que Pillar et YTsaurus utilisent un stockage sur disque dur économique, nous pouvons stocker un nombre de documents bien supérieur à ce que nous stockions auparavant dans DynamoDB.

Pillar effectue le suivi des sous-ensembles, c'est-à-dire des groupes de pages définis par des règles, telles que les pages récentes ou les pages à haute valeur. L'appartenance à un sous-ensemble détermine quelles mises à jour doivent être transmises aux composants en aval comme CobbleDB. CobbleDB utilise un stockage NVMe plus onéreux, de sorte que nous n'exportons que les documents que nous souhaitons réellement utiliser dans les résultats de recherche. Lorsque le contenu d'une page est mis à jour, ou si elle est ajoutée ou supprimée d'un sous-ensemble, Pillar est responsable de la mise en file d'attente du travail d'exportation correspondant.

Les transactions atomiques YTsaurus englobent l'état, les intentions d'exportation et les données d'entrée consommées afin de garantir l'exactitude. Pour qu'une transaction se termine avec succès, le système doit mettre à jour l'état persistant, marquer l'entrée comme prise en compte et mettre en file d'attente toutes les exportations requises ; si la transaction est abandonnée, aucune modification n'est apportée à quelque niveau que ce soit.

CobbleDB s'attend à ce que les mises à jour soient regroupées par lots en fonction de la partition. Les exportations Pillar sont converties dans ce format par un service sans état que nous appelons Lorry. Son seul rôle est de lire les enregistrements d'exportation à partir d'une file d'attente persistante alignée sur les partitions, de les accumuler dans des fichiers de lots par partition et de les transmettre à CobbleDB.

Entre Lorry et Cobble, un protocole supplémentaire sépare le plan de données (S3) du plan de contrôle. Lorry écrit le lot sur S3 sous un identifiant unique et enregistre le lot au sein de Cobble. Les réplicas de partition Cobble interrogent indépendamment l'API du plan de contrôle ou de données pour obtenir le prochain lot S3 à ingérer et appliquent de manière asynchrone les mises à jour par lots dans l'ordre chronologique. Cela signifie qu'un nœud lent ou en cours de récupération peut rattraper son retard à son propre rythme, plutôt de ralentir le reste du système.

Pillar publie des exports durables, et Lorry écrit des lots partitionnés dans S3 et les enregistre auprès du plan de contrôle. Chaque réplique CobbleDB découvre et ingère de façon indépendante ses lots en ordre chronologique.

Le nouveau chemin sépare la transaction de traitement immédiat de l'ingestion dans le magasin à accès rapide et offre un chemin rejouable pour les mises à jour incrémentielles et les reconstructions en bloc. Cette division du travail garantit que les mises à jour de l'état persistant du document n'interfèrent pas avec la latence du chemin de lecture.

Validation

5x plus rapide

Après la migration du magasin à accès rapide, la latence médiane de lecture par lots est passée de 31,4 ms à 5,60 ms. Le p90 est passé de 56,7 ms à 9,77 ms, et le p99 est passé de 123 ms à 24,2 ms. Les ratios vont de 5,08× à 5,80×, avec des améliorations tant pour les requêtes typiques que pour les valeurs extrêmes.

Latence de lecture par lot en production avant et après la migration : la P50 est passée de 31,4 à 5,60 ms, la P90 de 56,7 à 9,77 ms, et la P99 de 123 à 24,2 ms. Il s’agit des latences de lecture du magasin chaud provenant du trafic réel à différents moments, et non de requêtes appariées ni de la latence de réponse de bout en bout.

Ces chiffres sont dérivés de lectures par lots, où chaque requête demande environ 10 à 15 clés. La taille moyenne des éléments de la charge utile est de 50 Ko. Les deux systèmes ont été interrogés à environ 200 000 rps, bien que nous ayons également exécuté des tests de charge jusqu'à 500 000 rps sans dégradation.

Pour isoler la contribution de CobbleDB, tous les autres aspects de la configuration ont été maintenus constants. Il s'agit toutefois d'une comparaison observationnelle avant-après ; les systèmes ont servi du trafic de production réel à des moments différents plutôt que les mêmes requêtes dans des conditions contrôlées. Nous avons par conséquent également mesuré les performances des deux systèmes sur un banc d'essai synthétique. Pour simuler les requêtes observées en production, nous les avons interrogés avec des lots de 10 à 15 clés et des valeurs allant de 100 o à 100 Kio.

Latence synthétique de lecture par lot pour des lots de 10 et 15 clés sur des tailles de valeurs de 100 B à 100 KiB. Chaque point est la moyenne des latences P50, P90 ou P99 au niveau de l’exécution de deux runs par backend et configuration.

DynamoDB est en difficulté dans ce contexte, car son chemin de lecture géré ne peut pas être ajusté pour des lectures par lots de cette taille. En revanche, CobbleDB est optimisé précisément pour ce cas de figure, puisque nous gérons le placement, la mise en cache et les contrôles des réplicas. Il groupe les clés par partition, exécute les lectures en parallèle sur les réplicas de partition et peut se prémunir contre les lectures lentes en sollicitant d'autres réplicas. Nos résultats démontrent que ces choix de conception se traduisent par des lectures plus rapides dans un environnement de production.

20 % moins cher

CobbleDB est également nettement moins cher que DynamoDB. Outre le stockage, DynamoDB facture également chaque octet lu ou écrit. À l'échelle de notre corpus et de notre trafic de service, ces frais basés sur l'utilisation représentent une part substantielle du coût total. Les mises à jour continues consomment de la capacité d'écriture, tandis que chaque lot de pages préparées récupéré pour une requête consomme de la capacité de lecture.

Nous avons calculé les économies de coûts sur la base d'estimations internes concernant la taille du stockage, les unités de capacité d'écriture et les unités de capacité de lecture. À chaque niveau d'engagement, CobbleDB est au moins 20 % moins cher que DynamoDB. Les économies réelles sont probablement encore plus élevées, dans la mesure où ces résultats ne tiennent pas compte des économies de sauvegarde probables découlant de la compression.

Comment une nuée d'agents IA nous a aidés à le construire

La construction de CobbleDB et de l'infrastructure environnante n'a rien eu de trivial. La base de données principale de CobbleDB représente à elle seule environ 40 000 lignes de code en Rust, et la migration a également exigé un travail considérable pour vérifier l'exactitude et le comportement en cas de défaillance à l'échelle de la production, intégrer CobbleDB aux chemins de traitement et de service, et transitionner le trafic en direct en toute sécurité.

Nous avons accéléré cet effort à l'aide d'un système interne qui fait fonctionner un escadron d'agents de codage. Le système est conçu pour être hautement persistant ; il conserve les objectifs du projet, les risques actifs, l'historique du dépôt et les décisions antérieures d'une Session à l'autre.

Nous avons affecté un collectif de tels agents proactifs et actifs en continu au projet CobbleDB. Leur première tâche a été de construire un modèle précis du travail déjà en cours. Ils ont audité le canal du projet et les fils de discussion actifs, relié les éléments en suspens à leurs pull requests et à leurs propriétaires, et produit un rapport actualisé sur la santé du système et les prochaines actions. À partir de là, ils ont opéré à plusieurs niveaux de la boucle d'ingénierie :

  • Examen du code et des modifications de l'infrastructure.
  • Mise en évidence d'éléments bloquants faciles à manquer, notamment des hypothèses de restauration non sécurisées, des références de build obsolètes et des configurations qui échoueraient à l'exécution.
  • Préparation de correctifs ciblés, de tests, de modifications de surveillance et de documentation opérationnelle.
  • Suivi des portes d'intégration continue, de révision et d'infrastructure tout en veillant à ce que les actions de production relèvent explicitement de la responsabilité humaine.
  • Déploiements surveillés et exercices de sauvegarde ou de restauration, puis comparaison de l'état observé du système avec les critères de réussite du déploiement.
  • Maintien de rapports d'état de projet actualisés afin que les ingénieurs puissent voir ce qui a été livré, ce qui reste bloqué et là où le jugement humain est encore requis.

Les ingénieurs ont continué à définir l'architecture, à examiner les changements importants et à autoriser les opérations de production. Les agents se sont chargés d'une grande partie de l'inspection continue et du suivi entre ces décisions. Cela a permis à une petite équipe de piloter le projet efficacement : l'infrastructure de base de CobbleDB a été construite par deux ingénieurs humains et des centaines d'agents d'IA en deux mois.

Conclusion

CobbleDB n'est pas une base de données polyvalente ; elle est conçue sur mesure pour notre charge de travail spécifique de lectures par lots répétées d'enregistrements de pages préparés, alimentée par un pipeline de traitement asynchrone. En séparant l'état persistant, la distribution des mises à jour et la desserte au moment de la requête, nous pouvons optimiser chaque responsabilité de manière indépendante sans forcer une interface de base de données unique à toutes les accommoder ni reporter les tâches de maintenance sur le chemin de requête en direct. La migration vers CobbleDB a considérablement amélioré à la fois la latence médiane et la latence extrême tout en réduisant les coûts d'infrastructure.

Alors que nous continuons à développer en interne une infrastructure native pour l'IA, nous pouvons optimiser les performances et les coûts d'une manière impossible lorsque nous sommes liés à des implémentations et à des modèles de tarification tiers. De puissants agents d'IA font pencher la balance encore un peu plus en faveur de la construction plutôt que de l'achat.

Nous prévoyons de rendre CobbleDB open source prochainement, afin que d'autres équipes concevant des systèmes de recherche natifs pour l'IA puissent bénéficier de la même couche de stockage qui rend la recherche plus rapide et moins coûteuse au sein de nos produits.

Citation

BibTeX
@online{perplexity2026Cobbledb,
  author = {{Perplexity} and Alexandr Yarats and Marco Montagna and Aleksei Bykov and Alexey Gusev and Ali Abdullaev and Amir Mokhammed-Ali and Andrey Zosimov and Denis Kremko and Gleb Vishnevsky and Micah Zirn and Phi Hoang and Claire Beamer and Jeremy Yang and Jerry Ma and Denis Yarats},
  title = {{CobbleDB: Rebuilding AI search storage for lower latency and cost}},
  date = {2026-09-14},
  url = {https://www.perplexity.ai/hub/blog/cobbledb}
}