CobbleDB: AI-zoekopslag herbouwen voor lagere latentie en kosten

Een ontkoppelde architectuur met gescheiden duurzame status, gebatchte updates en query-time-retrieval.

AuteursPerplexity Engineering

AI-native zoeken stelt andere eisen aan hoe webinhoud moet worden verwerkt, opgeslagen en geserveerd. Om aan die eisen te voldoen op schaal, hebben we de hele stack vanaf de grond af herbouwd om de prestaties te verbeteren en de kosten te verlagen.

We hebben onze eigen crawling-, indexerings- en rankinginfrastructuur, embeddingmodellen en inferentie-engines gebouwd om efficiënte verwerking en snel serveren mogelijk te maken. We breiden die benadering nu uit naar opslag, het bindweefsel waarnaar de verwerking schrijft en waarvan de serving leest.

Managed databases zoals DynamoDB zorgden voor betrouwbare opslag, maar we hadden beperkte controle over hoe gegevens werden opgeslagen en reads werden bediend, en op gebruik gebaseerde prijzen groeiden mee met onze indexgrootte en query-volume. Bovendien koppelde onze vorige pipeline de documentverwerking rechtstreeks aan de serving database, waardoor het moeilijk was om updates onafhankelijk van live reads te beheren.

Met hulp van een nieuwe interne zwerm van codeer-agents hebben we deze laag herbouwd rond CobbleDB, een key-value hot store, ondersteund door Pillar voor duurzame documentstatus en publicatie, en Lorry voor gebatchte updates. Het herontwerp scheidt documentverwerking van hot-store-ingest en stelt ons in staat om serving af te stemmen op onze workload. Metingen voor en na in de productie tonen een ruwweg 5× lagere latentie voor batch-lezen, en ons interne kostenmodel schat minstens 20% besparing ten opzichte van DynamoDB.

Opslaginfrastructuur voor AI-native zoeken

Een klassieke zoekstack is gebouwd voor een menselijke lezer, niet voor een taalmodel. Dat verschil verandert de granulariteit waarmee het systeem webinhoud moet voorbereiden, opslaan en serveren.

Om ruwe HTML om te zetten in inhoud die klaar is voor modellen, schoont onze verwerkingspipeline elke pagina op, splitst deze in semantisch samenhangende passages, berekent hun embeddings en slaat de passages en embeddings samen op in de database. Op query-time selecteert de serving-pipeline de tekst die het meest relevant is voor de query en geeft deze door aan het model.

Twee verschillende workflows

De opslaglaag moet daarom twee workflows met verschillende eisen ondersteunen: het ingestten van updates uit de verwerkingspipeline en het bedienen van reads met lage latentie.

Aan de schrijfzijde arriveren continu nieuwe pagina\'s, veranderen bestaande pagina\'s en kunnen wijzigingen in chunkingmethoden of embeddingmodellen vereisen dat grote delen van het corpus opnieuw worden verwerkt. De opslaglaag moet deze updates opnemen in een duurzame documentstatus, de huidige versies bijhouden, de juiste representaties publiceren naar downstream systemen en grote backfills ondersteunen zonder de volledige pipeline vanaf de ruwe crawl te herhalen.

Daarentegen vragen query-time-reads om batches van paginakeys en halen ze de bijbehorende passagetekst en embeddings op voor ranking en passageselectie. Deze reads bevinden zich op het pad naar het uiteindelijke antwoord, dus moeten ze snel worden voltooid.

Deze workloads vragen om verschillende ontwerpen. Verwerking profiteert van duurzame wachtrijen en de mogelijkheid om updates efficiënt te leveren, fouten opnieuw te proberen en bestaande records op schaal bij te werken. Serving profiteert van vooraf berekende records, warme caches en snelle batch-reads met minimaal werk op query-time. Een strakke koppeling tussen de twee kan ervoor zorgen dat herstelwerk en grote updates concurreren met latentiegevoelige reads.

Waar managed databases tekortschieten

DynamoDB bood ons zeer beschikbare opslag zonder de last van het beheren van een gedistribueerde database. Een toegewijde tabel met voorbereide pagina\'s bevatte passages en embeddings voor query-time-reads, terwijl de volledige paginainhoud en metadata apart werden opgeslagen. Naarmate onze workload groeide, werden drie beperkingen echter belangrijk.

Ten eerste brengt de op gebruik gebaseerde prijsstructuur elke gelezen of geschreven byte in rekening. Een enkel Search API-verzoek bevat 100-120 keys die overeenkomen met pagina\'s die we moeten ophalen. Het ophalen van gegevens splitst een verzoek op in kleinere batches van 10-20 paginakeys, waarbij de gemiddelde itemgrootte ongeveer 50 KB is. Het herhaaldelijk lezen van die records verbruikte aanzienlijke capaciteit, terwijl continue crawls en wijzigingen in chunking- of embeddingmodellen meer writes genereerden. De economie hing ook af van de grootte en frequentie van de records, niet alleen van het aantal API-aanroepen.

Ten tweede mist de managed service de flexibiliteit die we nodig hebben voor prestatie-optimalisatie. De operatie die we nodig hadden was beperkt: geef op basis van een batch paginakeys snel de voorbereide inhoud terug. We wilden controle over welke machine een partitie bevatte, hoeveel geheugen werd toegewezen aan de cache en welke replica een verzoek afhandelde. Die bedieningselementen zijn belangrijk omdat een ongecachette read, een cross-zone hop of een trage replica de hele batch kan vertragen. DynamoDB beheert zijn eigen opslag- en replicagedrag achter de API. We konden de service configureren, maar we konden de interne plaatsing, opslagengine-caches of het replica-selectiebeleid niet afstemmen op onze workload.

Ten derde was de verwerking direct gekoppeld aan hot-store-writes. Onze vorige verwerkingspipeline schreef elke voorbereide pagina rechtstreeks naar DynamoDB. Een grote herverwerkingstaak werd daardoor een golf van individuele hot-store-updates, en de verwerking moest rekening houden met de doorvoerlimieten en het retry-gedrag van de hot store. We konden geen MapReduce uitvoeren over de hele database in 1-2 dagen om een nieuwe chunkingmethode te proberen, ons embeddingmodel te wijzigen of een nieuw veld toe te voegen. Er was geen onafhankelijke stream van opgeslagen pagina-updates die een afzonderlijke service kon batchen en in zijn eigen tempo aan de hot store kunne leveren.

Het aanpakken van deze beperkingen vergde meer dan alleen het vervangen van de database. We moesten de duurzame documentstatus en update-levering scheiden van de query-time-opslag, en vervolgens de hot store specifiek optimaliseren voor snelle batch-reads.

Scheid documentstatus, update-levering en serving

De nieuwe architectuur wijst elke verantwoordelijkheid toe aan een ander component. Pillar beheert een duurzame documentstatus en beslist wat er moet worden gepubliceerd. Lorry zet die exports om in gepartitioneerde batches. CobbleDB ingests de batches en serveert voorbereide records per paginakey.

Voor en na de opslagmigratie. Pillar en Lorry scheiden duurzame documentstatus en afleveren van updates van query-time reads die door CobbleDB worden bediend.

CobbleDB: het instelbare leespad

CobbleDB is onze hot store die is geoptimaliseerd voor het serveren van voorbereide records op query-time. Het is een gedistribueerde key-value store: de keys zijn pagina-identificatoren (gehashte URL\'s) en de values zijn de verwerkte representatie van de pagina (hun vooraf gechunckde passages en per-chunk vector-embeddings).

De belangrijke verandering is onze controle over de opslagconfiguratie.

Gegevens worden gepartitioneerd en verdeeld over datanodes. Dit gedistribueerde formaat zorgt ervoor dat de database horisontaal kan schalen om een enorm corpus van verwerkte pagina\'s op te slaan. Elke partitie heeft drie replica\'s op drie verschillende nodes; als de ene kopie onbeschikbaar wordt, kunnen de anderen doorgaan met het bedienen van reads.

Elke datanode krijgt een subset van de partities toegewezen. De node slaat zijn gegevens op met behulp van RocksDB, een open-source ingebouwde key-value-opslagengine die zeer geschikt is voor leesintensieve workloads gevoed door batch-ingest.

Gecachedde gegevens kunnen vanuit het geheugen worden geserveerd, terwijl ongecachette reads lokale NVMe-opslag gebruiken. Dit geeft ons een directe manier om de balans tussen geheugen en schijf af te stemmen op de geobserveerde workload, in plaats van het interne cachebeleid van een managed service te accepteren.

Een stateless query router hasht keys naar partities en stuurt verzoeken parallel naar nodes. De architectuur bevat ook een met PostgreSQL ondersteunde metadataservice en duurzame objectopslag.

Dit ontwerp ondersteunt de leespatronen die onze workload vereist. Nadat een query door ophalen en ranking is gegaan om een kandidaatset van relevante pagina\'s te verkrijgen, wordt de hot store in bulk bevraagd naar de inhoud van die pagina\'s. Deze leesverzoeken arriveren bij de router en worden doorgestuurd naar een bijbehorende datanode. De router prioriteert het verzenden van verzoeken naar een node in dezelfde beschikbaarheidszone om de cross-zone-latentie indien mogelijk te minimaliseren. Elke datanode kan de paginainhoud ophalen met MultiGet, een RocksDB batch-read-API die is ontworpen om meerdere keys gelijktijdig op te halen met een lage latentie. Als een node traag reageert, kan het systeem ook een andere partitiereplica op een andere node bevragen, wat de staartlatentie verbetert.

We kunnen ook vermijden dat we algemene functies implementeren die onze workflow niet vereist. De meeste databases bieden bijvoorbeeld consistentiegaranties en transactieondersteuning, maar onze hot store vereist geen transacties of gesynchroniseerde replica\'s. Het is acceptabel als er een korte pauze zit tussen het moment dat een document wordt geschreven en het moment dat het beschikbaar is voor reads, en het is ook prima als de ene replica gegevens sneller ingeste dan de andere. Het weglaten van functies die we niet nodig hebben, vermindert de overhead en kosten van ons systeem.

Een stateless router groepeert paginasleutels per partitie en leest replica’s parallel. Trage reads kunnen naar een andere replica worden gestuurd; de eerste respons voor elke partitie wordt samengevoegd in het resultaat.

Pillar en Lorry: het duurzame schrijfpad

Een afzonderlijke opslaglaag houdt alles bij wat we crawlen en verwerken, waarbij updates worden samengevoegd tot een versiebeheerde documentstatus en de noodzakelijke representaties worden gepubliceerd naar CobbleDB en de zoekindex. Hiervoor hebben we Pillar gebouwd, onze documentstatus- en publicatielaag.

Pillar wordt ondersteund door YTsaurus. Het slaat paginacomponenten (zoals metadata, chunks en embeddings) op in afzonderlijke YTsaurus-tabelfamilies. Het houdt ook de versies van chunks en embeddings bij, waardoor meerdere representaties naast elkaar kunnen bestaan zonder elkaar te overschrijven, zodat de onderliggende modellen kunnen veranderen. Omdat Pillar en YTsaurus goedkope HDD-opslag gebruiken, kunnen we veel meer documenten opslaan dan we voorheen in DynamoDB opsloegen.

Pillar houdt subsets bij, of op beleid gedefinieerde groepen pagina\'s zoals verse pagina\'s of pagina\'s met een hoge waarde. Subsets lidmaatschap bepaalt welke updates moeten worden doorgegeven aan downstream componenten zoals CobbleDB. CobbleDB gebruikt duurdere NVMe-opslag, dus we exporteren alleen de documenten die we daadwerkelijk in zoekresultaten willen gebruiken. Wanneer de inhoud van een pagina wordt bijgewerkt, of als deze wordt toegevoegd aan of verwijderd van een subset, is Pillar verantwoordelijk voor het in de wachtrij plaatsen van het bijbehorende exportwerk.

Atomische YTsaurus-transacties omvatten de status, exportintenties en verwerkte invoer om de correctheid te waarborgen. Om een transactie succesvol te voltooien, moet het systeem de duurzame status bijwerken, de invoer markeren als verwerkt en alle vereiste exports in de wachtrij plaatsen; als de transactie wordt afgebroken, wordt er op geen enkele laag een wijziging doorgevoerd.

CobbleDB verwacht dat updates worden gebatcht op basis van partitie. Pillar-exports worden naar dit formaat omgezet door een stateless service die we Lorry noemen. Zijn enige taak is het lezen van exportrecords uit een persistente, op partities afgestemde wachtrij, deze te verzamelen in batchbestanden per partitie en ze te overhandigen aan CobbleDB.

Tussen Lorry en Cobble scheidt een extra protocol het datavlak (S3) van het besturingsvlak. Lorry schrijft de batch naar S3 onder een unieke identificator en registreert de batch binnen Cobble. Cobble-partitiereplica\'s bevragen de batch- of control-plane-API onafhankelijk naar de volgende te ingestten S3-batch en passen batch-updates asynchroon toe in chronologische volgorde. Dit betekent dat een trage of herstellende node in zijn eigen tempo kan bijdragen, in plaats van de rest van het systeem te vertragen.

Pillar publiceert duurzame exports, en Lorry schrijft gepartitioneerde batches naar S3 en registreert ze bij het control plane. Elke CobbleDB-replica ontdekt en verwerkt zijn batches onafhankelijk in chronologische volgorde.

Het nieuwe pad scheidt de onmiddellijke verwerkingstransactie van hot-store-ingest en biedt een afspeelbaar pad voor incrementele updates en bulkreconstructies. Deze taakverdeling zorgt ervoor dat updates van de duurzame documentstatus de latentie van het leespad niet verstoren.

Validatie

5x sneller

Na het migreren van de hot store daalde de mediane latentie voor batch-lezen van 31,4 ms naar 5,60 ms. De p90 daalde van 56,7 ms naar 9,77 ms, en de p99 daalde van 123 ms naar 24,2 ms. De verhoudingen variëren van 5,08× tot 5,80×, met verbeteringen in zowel typische verzoeken als de staart.

Productie-batch-leeslatentie vóór en na migratie: P50 daalde van 31.4 naar 5.60 ms, P90 van 56.7 naar 9.77 ms, en P99 van 123 naar 24.2 ms. Dit zijn leeslatenties van de hot store uit liveverkeer op verschillende momenten, niet gematchte verzoeken of end-to-end antwoordlatentie.

Deze cijfers zijn afgeleid van gebatchte reads, waarbij elke query ongeveer 10-15 keys opvraagt. De gemiddelde grootte van een payload-item is 50KB. Beide systemen werden bevraagd op ongeveer 200k rps, hoewel we ook load-tests tot 500k rps uitvoerden zonder degradatie.

Om de bijdrage van CobbleDB te isoleren, werden alle andere aspecten van de opstelling constant gehouden. Dit blijft echter een observationele voor-en-na-vergelijking; de systemen bedienden live productieverkeer op verschillende tijdstippen in plaats van dezelfde verzoeken onder gecontroleerde omstandigheden. We hebben daarom ook de prestaties van beide systemen gemeten aan de hand van een synthetische benchmark. Om de verzoeken uit de productie te benaderen, hebben we beide bevraagd met batches van 10-15 keys en values variërend van 100B tot 100 KiB.

Gesynthetiseerde latency voor batch-leesacties voor batches van 10 en 15 sleutels over waarden van 100 B tot 100 KiB. Elk punt is het gemiddelde van de run-niveau P50-, P90- of P99-latentie uit twee runs per backend en configuratie.

DynamoDB heeft het moeilijk in deze context omdat het managed leespad niet kan worden afgesteld op gebatchte reads van deze grootte. Daarentegen is CobbleDB geoptimaliseerd voor exact deze instelling, aangezien wij de plaatsing, caching en replicabediening beheren. Het groepeert keys per partitie, voert reads parallel uit tegen partitiereplica\'s en kan trage reads indekken tegen andere replica\'s. Onze resultaten tonen aan dat deze ontwerppunten zich vertalen in snellere reads in een productieomgeving.

20% goedkoper

CobbleDB is bovendien aanzienlijk goedkoper dan DynamoDB. Naast opslag brengt DynamoDB ook elke gelezen of geschreven byte in rekening. Op de schaal van ons corpus en serving-verkeer worden deze op gebruik gebaseerde kosten een substantieel deel van de totale kosten. Continue updates verbruiken schrijfcapaciteit, terwijl elke batch voorbereide pagina\'s die voor een query wordt opgehaald leescapaciteit verbruikt.

We hebben de kostenbesparingen berekend op basis van interne schattingen van opslaggrootte, write capacity units en read capacity units. Bij elke commitment tier is CobbleDB minstens 20% goedkoper dan DynamoDB. De echte besparingen zijn waarschijnlijk zelfs hoger, aangezien deze resultaten geen rekening houden met de waarschijnlijke back-upbesparingen door compressie.

Hoe een zwerm AI-agenten ons hielp het te bouwen

Het bouwen van CobbleDB en de omliggende infrastructuur was alles behalve triviaal. Alleen al de kerndatabase van CobbleDB bestaat uit ongeveer 40.000 regels Rust, en de migratie vereiste ook uitgebreid werk om de correctheid en het storingsgedrag op productschaal te verifiëren, CobbleDB te integreren met zowel verwerkings- als serving-paden, en live verkeer veilig over te zetten.

We versnelden deze inspanning met behulp van een intern systeem dat een zwerm codeer-agents aanstuurt. Het systeem is ontworpen om zeer persistent te zijn; het behoudt de doelen van het project, actieve risico\'s, repositorygeschiedenis en eerdere beslissingen over sessies heen.

We hebben een verzameling van dergelijke proactieve, altijd actieve agents toegewezen aan het CobbleDB-project. Hun eerste taak was het bouwen van een nauwkeurig model van het werk dat al onderweg was. Ze auditeerden het projectkanaal en actieve draden, koppelden uitstaande items aan hun pull requests en eigenaren, en produceerden een actuele weergave van de systeemgezondheid en volgende acties. Van daaruit opereerden ze in verschillende onderdelen van de engineeringloop:

  • Code- en infrastructuurwijzigingen beoordeeld.
  • Bracht blokkades aan het licht die gemakkelijk over het hoofd te zien waren, waaronder onveilige herstelaannames, verouderde build-referenties en configuraties die tijdens runtime zouden falen.
  • Bereidde gerichte fixes, tests, monitoringswijzigingen en operationele documentatie voor.
  • Hield CI-, review- en infrastructuurpoorten bij en zorgde er tegelijkertijd voor dat productieacties expliciet door mensen werden beheerd.
  • Gemonitorde rollouts en back-up- of hersteloefeningen vergeleken vervolgens de geobserveerde systeemstatus met de succescriteria van de rollout.
  • Onderhield projectweergaven zodat engineers konden zien wat er was verzonden, wat geblokkeerd bleef en waar menselijk oordeel nog steeds vereist was.

Engineers bepalen nog steeds de architectuur, beoordeelden consequente wijzigingen en autoriseerden productieoperaties. De agents handelden een groot deel van de continue inspectie en opvolging tussen die beslissingen af. Hierdoor kon een klein team het project efficiënt aansturen: de kerninfrastructuur van CobbleDB werd in twee maanden gebouwd door twee menselijke engineers en honderden AI-agents.

Conclusie

CobbleDB is geen database voor algemeen gebruik; het is speciaal gebouwd voor onze specifieke workload van herhaalde batch-reads van voorbereide paginarecords, gevoed door een asynchrone verwerkingspipeline. Door de duurzame status, update-levering en query-time-serving te scheiden, kunnen we elke verantwoordelijkheid onafhankelijk optimaliseren zonder dat een enkele database-interface al deze taken hoeft te ondersteunen of onderhoudswerk naar het live query-pad hoeft te verplaatsen. Migreren naar CobbleDB verbeterde de mediane en staartlatentie aanzienlijk en verlaagde tegelijkertijd de infrastructuurkosten.

Naarmate we doorgaan met het in-house ontwikkelen van AI-native infrastructuur, kunnen we prestaties en kosten optimaliseren op manieren die niet mogelijk zijn wanneer we gebonden zijn aan implementaties en prijsmodellen van derden. Krachtige AI-agents verschuiven de berekening nog verder in het voordeel van zelf bouwen boven kopen.

We zijn van plan om CobbleDB binnenkort als open source vrij te geven, zodat andere teams die AI-native zoeksystemen bouwen kunnen profiteren van dezelfde opslaglaag die het zoeken binnen onze producten sneller en goedkoper maakt.

Citatie

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}
}