CobbleDB : refonte du stockage pour la recherche par IA (latence et coût)
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.
La recherche native par IA impose des exigences différentes sur 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 modèles d'incorporation et nos moteurs d'inférence pour alimenter un traitement efficace et un service rapide. Nous étendons maintenant cette approche au stockage, le tissu conjonctif vers lequel le traitement écrit et à partir duquel le service lit.
Les bases de données gérées telles que DynamoDB offraient un stockage fiable, mais nous avions un contrôle limité sur la manière dont les données étaient stockées et les lectures servies, 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 couplait 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 essaim d'agents de codage internes, nous avons reconstruit cette couche autour de CobbleDB, un magasin chaud clé-valeur, pris en charge par Pillar pour l'état persistant des documents et la publication, et Lorry pour les mises à jour par lots. La refonte sépare le traitement des documents de l'ingestion du stockage chaud et nous permet d'ajuster le service à notre charge de travail. Les mesures de production avant et 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, et non pour un modèle de langue. 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 incorporations et stocke les passages et les incorporations ensemble dans la base de données. Au moment de la requête, le pipeline de service sélectionne le texte le plus pertinent pour la requête et le transmet au modèle.
Deux flux de travail distincts
La couche de stockage doit donc prendre en charge deux flux de travail aux exigences différentes : l'ingestion des mises à jour 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 les modifications apportées aux méthodes de découpage en blocs ou aux modèles d'incorporation peuvent nécessiter le retraitement de grandes portions du corpus. La couche de stockage doit intégrer ces mises à jour dans l'état persistant des documents, suivre les versions actuelles, publier les représentations correctes vers les systèmes aval et prendre en charge les remplissages massifs 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 incorporations correspondants pour le classement et la sélection des passages. Ces lectures se trouvent sur le chemin menant à la réponse finale, elles doivent donc être exécutées rapidement.
Ces charges de travail nécessitent des conceptions différentes. Le traitement bénéficie de files d'attente persistantes et de la capacité à livrer les mises à jour efficacement, à réitérer en cas d'échec et à mettre à jour les enregistrements existants à grande échelle. Le service bénéficie d'enregistrements précalculés, de caches chauds et de lectures groupées rapides avec un minimum de travail 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 à rivaliser avec les lectures sensibles à la latence.
Là où les bases de données gérées montrent leurs limites
DynamoDB nous a fourni un stockage hautement disponible sans la charge d'exploitation d'une base de données distribuée. Une table dédiée aux pages préparées contenait les passages et les incorporations 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.
Deuxièmement, 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 lots plus petits 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 a consommé une capacité substantielle, tandis que les explorations continues et les modifications apportées aux modèles de découpage en blocs ou d'incorporation ont généré davantage d'écritures. L'aspect économique dépendait également de la taille et de la fréquence des enregistrements, et 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 dont nous avions besoin était simple : étant donné un lot d'identifiants de page, renvoyer rapidement leur contenu préparé. Nous voulions contrôler quelle machine hébergeait une partition, quelle quantité de mémoire était consacrée à son cache et quelle réplique gérait une requête. Ces contrôles importent car une lecture non mise en cache, un saut inter-zones ou une réplique lente peuvent retarder l'ensemble du lot. DynamoDB gère son propre stockage et le comportement de ses répliques derrière l'API. Nous pouvions configurer le service, mais nous ne pouvions pas ajuster son placement interne, ses caches de moteur de stockage ou sa politique de sélection des répliques en fonction de notre charge de travail.
Troisièmement, le traitement était directement lié aux écritures dans le stockage chaud. Notre pipeline de traitement précédent écrivait chaque page préparée directement dans DynamoDB. Un travail de re-traitement important devenait par conséquent une vague de mises à jour individuelles du stockage chaud, et le traitement devait s'adapter aux limites de débit et au comportement de nouvelle tentative du stockage chaud. 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 d'incorporation ou ajouter un nouveau champ. Il n'existait pas de flux indépendant de mises à jour de pages enregistrées qu'un service distinct pouvait grouper et livrer au stockage chaud à son propre rythme.
Répondre à ces contraintes nécessitait plus que le simple remplacement de la base de données. Nous devions séparer l'état persistant des documents et la diffusion des mises à jour du stockage au moment de la requête, puis optimiser spécifiquement le stockage chaud pour des lectures groupées rapides.
Séparer l'état des documents, la diffusion 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 quoi publier. Lorry transforme ces exportations en lots partitionnés. CobbleDB ingère les lots et sert les enregistrements préparés par clé de page.
CobbleDB : le chemin de lecture ajustable
CobbleDB est notre stockage chaud 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 sont la représentation traitée de la page (ses passages pré-découpés en blocs et ses incorporations vectorielles par bloc).
Le changement important réside dans notre contrôle de 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 accueillir un corpus énorme de pages traitées. Chaque partition possède trois répliques 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 de partitions. Le nœud stocke ses données à l'aide de RocksDB, un moteur de stockage clé-valeur intégré open source parfaitement adapté aux charges de travail à forte 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 en partitions et achemine les requêtes vers les nœuds en parallèle. L'architecture comprend également un service de métadonnées s'appuyant sur PostgreSQL et un stockage d'objets persistant.
Cette conception prend en charge les schémas de lecture exigés par notre charge de travail. Une fois qu'une requête a traversé les étapes de recherche et de classement pour obtenir un ensemble candidat de pages pertinentes, le stockage chaud 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 de requêtes à un nœud situé dans la même zone de disponibilité afin de minimiser la latence inter-zones, 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 une autre réplique de partition sur un nœud différent, améliorant ainsi la latence de longue traîne.
Nous pouvons également éviter d'implémenter des fonctionnalités courantes que notre flux de travail ne requiert pas. Par exemple, la plupart des bases de données offrent des garanties de cohérence et la prise en charge des transactions, mais notre stockage chaud ne nécessite ni transactions ni répliques synchronisées. Il est acceptable qu'il y ait un court laps de temps entre le moment où un document est écrit et celui où il est disponible pour la lecture, et il est également acceptable qu'une réplique ingère des données plus rapidement qu'une autre. L'omission de fonctionnalités dont nous n'avons pas besoin réduit la surcharge et le coût de notre système.
Pillar et Lorry : le chemin d'écriture persistant
Une couche de stockage séparée assure le suivi de tout ce que nous extrayons et traitons, en fusionnant les mises à jour dans un état de document versionné et en publiant les représentations nécessaires vers CobbleDB et l'index de recherche. Nous avons conçu Pillar, notre couche de gestion de l'état des documents et de publication, à cette fin.
Pillar s'appuie sur YTsaurus. Il stocke les composants de page (tels que les métadonnées, les blocs et les incorporations) dans des familles de tables YTsaurus séparées. Il suit également les versions des blocs et des incorporations, permettant à de multiples représentations de coexister sans s'écraser mutuellement afin que les modèles sous-jacents puissent changer. Comme Pillar et YTsaurus utilisent un stockage sur disque dur bon marché, nous pouvons stocker beaucoup plus de documents que nous n'en stockions auparavant dans DynamoDB.
Pillar suit les sous-ensembles, ou groupes de pages définis par des politiques, tels que les pages fraîches ou les pages à haute valeur. L'appartenance à un sous-ensemble détermine quelles mises à jour doivent être transmises aux composants aval tels que CobbleDB. CobbleDB utilise un stockage NVMe plus coûteux, nous n'exportons donc 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 YTsaurus atomiques couvrent l'état, les intentions d'exportation et les entrées consommées afin de garantir l'exactitude. Pour qu'une transaction se déroule 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 couche que ce soit.
CobbleDB s'attend à ce que les mises à jour soient regroupées par lots en fonction de la partition. Les exportations de 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 dans Cobble. Les répliques de partition Cobble interrogent indépendamment le lot ou l'API du plan de contrôle 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.
Le nouveau chemin sépare la transaction de traitement immédiat de l'ingestion du stockage chaud 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 des documents n'interfèrent pas avec la latence du chemin de lecture.
Validation
5x plus rapide
Après la migration du stockage chaud, 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 rapports vont de 5,08× à 5,80×, avec des améliorations tant pour les requêtes typiques que pour la longue traîne.
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 aucune 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 benchmark 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.
DynamoDB est mis à mal dans ce contexte car son chemin de lecture géré ne peut pas être optimisé pour des lectures par lots de cette taille. En revanche, CobbleDB est optimisé exactement pour ce scénario, puisque nous gérons le placement, la mise en cache et les contrôles des répliques. Il regroupe les clés par partition, exécute les lectures en parallèle sur les répliques de partition et peut se prémunir contre les lectures lentes en faisant appel à d'autres répliques. 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 de la taille du stockage, des unités de capacité d'écriture et des 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, étant donné que ces résultats ne tiennent pas compte des économies de sauvegarde probables liées à la compression.
Comment une escouade d'agents d'IA nous a aidés à le construire
La construction de CobbleDB et de l'infrastructure environnante a été loin d'être triviale. La base de données principale de CobbleDB compte à elle seule environ 40 000 lignes de code en Rust, et la migration a également nécessité 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 en toute sécurité le trafic en direct.
Nous avons accéléré cet effort en utilisant un système interne qui exploite un essaim 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 permanence 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 fil 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 l'état de santé du système et les actions suivantes. À partir de là, ils ont opéré dans plusieurs parties de la boucle d'ingénierie :
- Examen du code et des modifications de l'infrastructure.
- Mise en évidence de bloqueurs faciles à manquer, notamment des hypothèses de restauration non sécurisées, des références de compilation 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 de l'intégration continue, des révisions et des barrières d'infrastructure tout en maintenant les actions de production explicitement sous contrôle humain.
- Déploiements surveillés et exercices de sauvegarde ou de restauration, puis comparaison de l'état du système observé avec les critères de réussite du déploiement.
- Maintien de rapports de projet actualisés afin que les ingénieurs puissent voir ce qui a été livré, ce qui reste bloqué et où le jugement humain est encore requis.
Les ingénieurs continuent de définir l'architecture, d'examiner les changements importants et d'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 mener à bien le projet de manière efficace : l'infrastructure principale 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ées, alimentée par un pipeline de traitement asynchrone. En séparant l'état persistant, la diffusion 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 à les accommoder toutes ni reporter le travail de maintenance sur le chemin de requête en direct. La migration vers CobbleDB a considérablement amélioré la latence médiane et de longue traîne 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 qui n'est pas possible 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 davantage en faveur de la construction plutôt que de l'achat.
Nous prévoyons de passer CobbleDB en open source prochainement, afin que d'autres équipes construisant des systèmes de recherche natifs par 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
@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}
}