CobbleDB: Gjenoppbygger AI-søkesøkelagring for lavere forsinkelse og kostnad

En frakoblet arkitektur med separert varig tilstand, satsvise oppdateringer og henting ved spørringstidspunktet.

ForfatterePerplexity Engineering

AI-nativt søk pålegger ulike krav til hvordan webinnhold må prosesseres, lagres og serveres. For å møte disse kravene i skala har vi bygd opp hele stabelen fra bunnen av for å forbedre ytelse og redusere kostnad.

Vi bygde vår egen infrastruktur for gjennomsøkning, indeksering og rangering, innbyggingsmodeller og inferensmotorer for å drive effektiv prosessering og rask servering. Vi utvider nå den tilnærmingen til lagring, bindevevet som prosessering skriver til og servering leser fra.

Administrerte databaser som DynamoDB sørget for pålitelig lagring, men vi hadde begrenset kontroll over hvordan data ble lagret og lesninger ble betjent, og bruksbasert prising vokste med indeksstørrelsen og forespørselsvolumet vårt. Separat koblet vår forrige pipelene dokumentprosessering direkte til servicedatabasen, noe som gjorde det vanskelig å administrere oppdateringer uavhengig av live-lesninger.

Med hjelp fra en ny intern sverm av kodingagenter, bygde vi dette laget rundt CobbleDB, et nøkkel-verdi-varmt lager, støttet av Pillar for varig dokumenttilstand og publisering, og Lorry for satsvise oppdateringer. Redesignet skiller dokumentprosessering fra innhenting til det varme lageret og lar oss finjustere servering til vår arbeidsmengde. Før-og-etter-målinger i produksjon viser omtrent 5× lavere forsinkelse ved satsvis lesing, og vår interne kostnadsmodell anslår minst 20 % besparelser sammenlignet med DynamoDB.

Lagringsinfrastruktur for AI-nativt søk

En klassisk søkestabel er bygget for en menneskelig leser, ikke en språkmodell. Den forskjellen endrer granulariteten som systemet trenger for å klargjøre, lagre og betjene webinnhold med.

For å gjøre rå HTML om til modellklart innhold, renser prosesseringspipelinen vår hver side, deler den inn i semantisk sammenhengende avsnitt, beregner innbyggingene deres og lagrer avsnittene og innbyggigene sammen i databasen. Ved spørringstidspunktet velger serveringspipelinen ut teksten som er mest relevant for spørringen, og sender den videre til modellen.

To forskjellige arbeidsflyt

Lagringslaget må derfor støtte to arbeidsflyt med ulike krav: innhenting av oppdateringer fra prosesseringspipelinen og servering av lesninger med lav forsinkelse.

På skrivesiden ankommer nye sider kontinuerlig, eksisterende sider endres, og endringer i oppdelingsmetoder eller innbyggingsmodeller kan kreve omprosessering av store deler av korpuset. Lagringslaget må innlemme disse oppdateringene i den varige dokumenttilstanden, spore de gjeldende versjonene, publisere de riktige representasjonene til nedstrømssystemer, og støtte store etterfyllinger uten å gjenta hele pipelinen fra rågjennomsøkningen.

I kontrast ber omgangstid-lesninger om satser med sidenøkler og henter de tilsvarende avsnittstekstene og innbyggingene for rangering og avsnittsvalg. Disse lesningene ligger på veien til endelig svar, så de må fullføres raskt.

Disse arbeidsmengdene krever forskjellige design. Prosessering drar nytte av varige køer og muligheten til å levere oppdateringer effektivt, prøve feilede operasjoner på nytt og oppdatere eksisterende poster i skala. Servering drar nytte av forhåndsberegnede poster, varme mellomlagre og raske satsvise lesninger med minimalt arbeid ved spørringstidspunktet. Tetthet mellom de to kan føre til at gjenopprettingsarbeid og store oppdateringer konkurrerer med forsinkelsessensitive lesninger.

Hvor administrert database kommer til kort

DynamoDB ga oss svært tilgjengelig lagring uten byrden ved å drifte en distribuert database. En dedikert tabell for klargjorte sider inneholdt avsnitt og innbygginger (embeddings) for lesing ved spørringstidspunktet, mens fullt sideinnhold og metadata ble lagret separat. Da arbeidsmengden vår vokste, ble imidlertid tre begrensninger viktige.

For det første belaster den bruksbaserte prisingen hver byte som leses eller skrives. En enkelt søke-API-forespørsel inneholder 100-120 nøkler som tilsvarer sider vi må hente. Datainnhentingsprosessen deler en forespørsel inn i mindre satser på 10-20 sidenøkler med en gjennomsnittlig elementstørrelse på omtrent 50 KB. Gjentatt lesing av disse postene forbrukte betydelig kapasitet, mens kontinuerlig gjennomsøkning og endringer i oppdelings- eller innbyggingsmodeller genererte flere skriveoperasjoner. Økonomien var også avhengig av størrelsen og hyppigheten til postene, ikke bare antall API-kall.

For det andre mangler den administrerte tjenesten den fleksibiliteten vi trenger for ytelsesjustering. Operasjonen vi trengte var smal: gitt en sats med sideidentifikatorer, returner deres klargjorte innhold raskt. Vi ønsket kontroll over hvilken maskin som holdt en partisjon, hvor mye minne som ble viet til mellomlageret, og hvilken replika som håndterte en forespørsel. Disse kontrollene betyr noe fordi en ikke-mellomlagret lesing, et hopp på tvers av soner eller en treg replika kan forsinke hele batchen. DynamoDB administrerer sin egen lagrings- og replikaadferd bak API-et. Vi kunne konfigurere tjenesten, men vi kunne ikke finjustere dens interne plassering, lagringsmotormellomlagre eller replikautvalgspolitikk rundt arbeidsmengden vår.

For det tredje var prosessering knyttet direkte til skriving i det varme lageret. Vår forrige prosesseringspipelene skrev hver klargjorte side direkte til DynamoDB. En stor omprosesseringsjobb ble derfor til en bølge av individuelle oppdateringer av det varme lageret, og prosesseringen måtte tilpasse seg det varme lagerets gjennomstrømningsgrenser og forsøk på nytt-adferd (retry). Vi kunne ikke kjøre MapReduce over hele databasen i løpet av 1–2 dager for å prøve en ny oppdelingsmetode, endre innbyggingsmodellen vår eller legge til et nytt felt. Det var ingen uavhengig strøm av lagrede sideoppdateringer som en separat tjeneste kunne samle i satser (batch) og levere til det varme lageret i sitt eget tempo.

Å adressere disse begrensningene krevde mer enn å erstatte databasen. Vi måtte skille varig dokumenttilstand og oppdateringslevering fra lagring ved spørringstidspunktet, og deretter optimalisere det varme lageret spesifikt for raske satsvise lesninger.

Skilj dokumenttilstand, oppdateringslevering og servering

Den nye arkitekturen tildeler hvert ansvarsområde til en annen komponent. Pillar opprettholder en varig dokumenttilstand og bestemmer hva som skal publiseres. Lorry gjør disse eksportene om til partisjonerte satser. CobbleDB henter inn satsene og betjener klargjorte poster etter sidenøkkel.

Før og etter lagringsmigreringen. Pillar og Lorry skiller varig dokumenttilstand og leveranse av oppdateringer fra lesninger ved spørringstid som betjenes av CobbleDB.

CobbleDB: den justerbare lesebanen

CobbleDB er vårt varme lager optimert for servering av klargjorte poster ved spørringstidspunktet. Det er en distribuert nøkkel-verdi-lagring: nøklene er sideidentifikatorer (hashede URL-er), og verdiene er sidens prosesserte representasjon (forhåndsdelte avsnitt og vektorinnbygginger per avsnitt).

Den viktige endringen er vår kontroll over lagringskonfigurasjon.

Data er partisjonert og delt på tvers av datanoder. Dette distribuerte formatet sikrer at databasen kan skalere horisontalt for å imøtekomme et enormt korpus av prosesserte sider. Hver partisjon har tre replikaer på tre forskjellige noder; hvis en kopi blir utilgjengelig, kan de andre fortsette å betjene lesninger.

Hver datanode blir tildelt en delmengde av partisjonene. Noden lagrer dataene sine ved hjelp av RocksDB, en åpen kildekode-basert innebygd nøkkel-verdi-lagringsmotor som passer godt for leseintensive arbeidsmengder matet av satsvis datainnhenting (batch ingestion).

Mellomlagrede data kan serveres fra minne, mens ikke-mellomlagrede lesninger bruker lokal NVMe-lagring. Dette gir oss en direkte måte å finjustere balansen mellom minne og disk for den observerte arbeidsmengden, i stedet for å akseptere en administrert tjenestes interne retningslinjer for mellomlagring.

En tilstandsløs spørringsrouter hasher nøkler til partisjoner og ruter forespørsler til noder parallelt. Arkitekturen inkluderer også en PostgreSQL-basert metadatatjeneste og varig objektlagring.

Dette designet støtter lesemønstrene arbeidsmengden vår krever. Etter at en forespørsel går gjennom henting og rangering for å få et kandidateffektsett med relevante sider, forespørres det varme lageret i bulk etter innholdet i disse sidene. Disse leseforespørslene ankommer ruteren og blir rutet til en tilsvarende datanode. Ruteren prioriterer å sende forespørsler til en node i samme tilgjengelighetssone for å minimere forsinkelse på tvers av soner, om mulig. Hver datanode kan hente sideinnholdet ved hjelp av MultiGet, et RocksDB batch-read API utformet for å hente flere nøkler samtidig med lav forsinkelse. Hvis en node er treg til å svare, kan systemet også spørre en annen partisjonsreplika på en annen node, noe som forbedrer haleforsinkelsen.

Vi kan også unngå å implementere vanlige funksjoner som arbeidsflyt ikke krever. For eksempel tilbyr de fleste databaser konsistensgarantier og transaksjonsstøtte, men det varme lageret vårt krever ikke transaksjoner eller synkroniserte replikaer. Det er akseptabelt at det er et lite tidsrom mellom når et dokument skrives og når det er tilgjengelig for lesing, og det er også greit at en replika henter inn data raskere enn en annen. Å utelate funksjoner vi ikke trenger reduserer overhead og kostnad for systemet vårt.

En stateless ruter grupperer sidenøkler etter partisjon og leser replikaer parallelt. Trege lesninger kan sendes til en annen replika; det første svaret for hver partisjon slås sammen til resultatet.

Pillar og Lorry: den varige skrivebanen

Et separat lagringslag holder oversikt over alt vi henter inn og prosesserer, og slår sammen oppdateringer til en versjonert dokumenttilstand og publiserer de nødvendige representasjonene til CobbleDB og søkeindeksen. Vi bygde Pillar, vårt dokumenttilstands- og publiseringslag, for dette formålet.

Pillar støttes av YTsaurus. Den lagrer sidekomponenter (som metadata, biter og innbygginger) i separate YTsaurus-tabellfamilier. Den sporer også versjonene av biter og innbygginger, slik at flere representasjoner kan eksistere side om side uten å overskrive hverandre, slik at de underliggende modellene kan endre seg. Siden Pillar og YTsaurus bruker billig HDD-lagring, kan vi lagre langt flere dokumenter enn vi tidligere lagret i DynamoDB.

Pillar sporer delmengder, eller policy-definerte grupper av sider som ferske sider eller sider med høy verdi. Medlemskap i delmengde avgir hvilke oppdateringer som må sendes videre til nedstrømskomponenter som CobbleDB. CobbleDB bruker dyrere NVMe-lagring, så vi eksporterer bare dokumentene vi faktisk ønsker å bruke i søkeresultater. Når innholdet i en side oppdateres, eller hvis den legges til eller fjernes fra en delmengde, er Pillar ansvarlig for å sette tilsvarende eksportarbeid i kø.

Atomiske YTsaurus-transaksjoner dekker tilstand, eksportformål og forbrukt inngang for å sikre korrekthet. For at en transaksjon skal fullføre vellykket, må systemet oppdatere den varige tilstanden, markere inngangen som registrert og sette alle nødvendige eksporter i kø; hvis transaksjonen avbrytes, blir det ikke gjort noen endringer på noen av lagene.

CobbleDB forventer at oppdateringer samles i satser i henhold til partisjon. Pillar-eksport konverteres til dette formatet av en tilstandsløs tjeneste vi kaller Lorry. Dens eneste oppgave er å lese eksportposter fra en vedvarende partisjonstilpasset kø, akkumulere dem til per-partisjon-batchfiler og overlevere dem til CobbleDB.

Mellom Lorry og Cobble deler en ekstra protokoll dataplanet (S3) fra kontrollplanet. Lorry skriver batchen til S3 under en unik identifikator og registrerer batchen i Cobble. Cobble-partisjonsreplikaer spør uavhengig etter batchen eller kontrollplanets API for neste S3-batch som skal hentes inn, og bruker asynkront batchoppdateringer i kronologisk rekkefølge. Dette betyr at en treg eller gjenopprettende node kan hente inn det forsmedelige i sitt eget tempo, i stedet for å bremse resten av systemet.

Pillar publiserer varige eksportfiler, og Lorry skriver partisjonerte batcher til S3 og registrerer dem hos kontrollplanet. Hver CobbleDB-replika oppdager og tar uavhengig inn sine batcher i kronologisk rekkefølge.

Den nye banen skiljer den umiddelbare behandlingstransaksjonen fra innhenting til det varme lageret og gir en spillbar bane for inkrementelle oppdateringer og fullstendige gjenoppbygginger. Denne arbeidsdelingen sikrer at oppdateringer til varig dokumenttilstand ikke forstyrrer forsinkelsen på lesebanen.

Validering

5x raskere

Etter migrering av det varme lageret falt medianforsinkelsen for satsvis lesing (batch-read) fra 31,4 ms til 5,60 ms. P90 falt fra 56,7 ms til 9,77 ms, og p99 falt fra 123 ms til 24,2 ms. Forholdstallene varierer fra 5,08× til 5,80×, med forbedringer i både typiske forespørsler og haleytelsen.

Produksjonsbatchleseforsinkelse før og etter migrering: P50 falt fra 31,4 til 5,60 ms, P90 fra 56,7 til 9,77 ms, og P99 fra 123 til 24,2 ms. Dette er leseforsinkelser fra hot store fra live trafikk på ulike tidspunkter, ikke samsvarende forespørsler eller ende-til-ende-svartid.

Disse tallene er avledet fra satsvise lesninger, der hver spørring ber om omtrent 10-15 nøkler. Gjennomsnittlig elementstørrelse for nyttelast er 50 KB. Begge systemene ble forespurt med ca. 200k rps, selv om vi også kjørte belastningstester opp til 500k rps uten forringelse.

For å isolere bidraget fra CobbleDB ble alle andre aspekter ved oppsettet holdt konstante. Dette forblir imidlertid en observasjonsbasert før-og-etter-sammenligning; systemene betjente live produksjonstrafikk på forskjellige tidspunkter i stedet for de samme forespørslene under kontrollerte forhold. Vi målte derfor også ytelsen til begge systemene på en syntetisk testbenk. For å tilnærme oss forespørslene som observeres i produksjon, spurte vi begge med satser på 10-15 nøkler og verdier som varierer fra 100B til 100 KiB.

Syntetisk batchleseforsinkelse for batcher med 10 og 15 nøkler på tvers av verdistørrelser fra 100 B til 100 KiB. Hvert datapunkt er gjennomsnittet av P50-, P90- eller P99-forsinkelse på kørselsnivå fra to kjøringer per backend og konfigurasjon.

DynamoDB sliter i denne sammenhengen fordi den administrerte lesebanen ikke kan justeres for satsvise lesninger av denne størrelsen. I kontrast er CobbleDB optimalisert for nøyaktig denne innstillingen, siden vi administrerer plassering, mellomlagring (caching) og replikakontroller. Den grupperer nøkler etter partisjon, utfører lesninger parallelt mot partisjonsreplikaer, og kan sikre trege lesninger mot andre replikaer. Resultatene våre viser at disse designvalgene oversettes til raskere lesninger i et produksjonsmiljø.

20 % billigere

CobbleDB er også betydelig billigere enn DynamoDB. I tillegg til lagring tar DynamoDB også betalt for hver byte som leses eller skrives. I skalaen til korpuset vårt og serveringstrafikken blir disse bruksbaserte avgiftene en vesentlig del av totalkostnaden. Kontinuerlige oppdateringer forbruker skrivekapasitet, mens hver sats med klargjorte sider hentet for en forespørsel forbruker lesekapasitet.

Vi beregnet kostnadsbesparelsene basert på interne anslag for lagringsstørrelse, skrivekapasitetsenheter og lesekapasitetsenheter. På alle forpliktelsesnivåer er CobbleDB minst 20 % billigere enn DynamoDB. De faktiske besparelsene er sannsynligvis enda høyere, siden disse resultatene ikke tar hensyn til de sannsynlige besparelsene på sikkerhetskopiering fra komprimering.

Hvordan en sverm av AI-agenter hjelper oss med å bygge det

Å bygge CobbleDB og den omliggende infrastrukturen var langt fra trivielt. CobbleDBs kjernedatabase alene er omtrent 40 000 linjer med Rust, og migreringen krevde også omfattende arbeid for å bekrefte korrekthet og feiladferd i produksjonsskala, integrere CobbleDB med både prosesserings- og serveringsbaner, og trygt overføre live-trafikk.

Vi akselererte denne innsatsen ved hjelp av et internt system som drifter en sverm av kodingagenter. Systemet er utformet for å være svært vedvarende; det beholder prosjektets mål, aktive risikoer, repositoriehistorikk og tidligere beslutninger på tvers av økt.

Vi tildelte et kollektiv av slike proaktive, alltid-på-agenter til CobbleDB-prosjektet. Deres første oppgave var å bygge en nøyaktig modell av arbeidet som allerede var i gang. De reviderte prosjektets tråd og aktive tråder, koblet utestående elementer til deres pull request og eiere, og produserte en oppdatert oversikt over systemhelse og neste handlinger. Derfra opererte de på tvers av flere deler av ingeniørløkken:

  • Gjennomgikk kode- og infrastrukturendringer.
  • Fremhevet blokkeringer som var enkle å overse, inkludert usikre gjenopprettingsantagelser, foreldede byggereferanser og konfigurasjoner som ville mislykkes ved kjøring.
  • Klargjorde fokuserte fiksher, tester, overvåkingsendringer og operasjonell dokumentasjon.
  • Sporet CI-, gjennomgangs- og infrastruktursperrer samtidig som produksjonshandlinger ble holdt eksplisitt menneskeeide.
  • Overvåkede utrullinger og øvelser for sikkerhetskopiering eller gjenoppretting, og sammenlignet deretter den observerte systemtilstanden med utrullingens suksesskriterier.
  • Vedlikeholdt prosjektoversikter slik at ingeniører kunne se hva som hadde blitt sendt, hva som forble blokkert, og hvor menneskelig vurdering fortsatt var nødvendig.

Ingeniører setter fortsatt arkitekturen, gjennomgikk konsekvensanalyser for endringer og autoriserte produksjonsoperasjoner. Agentene håndterte mye av den kontinuerlige inspeksjonen og oppfølgingen mellom disse beslutningene. Dette tillot et lite team å drive prosjektet effektivt – kjerneinfrastrukturen til CobbleDB ble bygget av to menneskelige ingeniører og hundrevis av AI-agenter på to måneder.

Konklusjon

CobbleDB er ikke en generell database; den er spesialbygd for vår spesifikke arbeidsmengde med gjentatte satsvise lesninger av klargjorte siderposter, matet av en asynkron prosesseringspipelene. Ved å skille varig tilstand, oppdateringslevering og servering ved spørringstidspunktet, kan vi optimalisere hvert ansvarsområde uavhengig uten å tvinge et enkelt databasegrensesnitt til å imøtekomme alle eller skyve vedlikeholdsarbeid over på den live spørringsbanen. Migrering til CobbleDB forbedret median- og haleforsinkelsen betraktelig samtidig som infrastrukturkostnadene ble redusert.

Ettersom vi fortsetter å utvikle AI-nativ infrastruktur internt, kan vi optimalisere ytelse og kostnad på måter som ikke er mulig når vi er bundet til tredjepartsimplementeringer og prismodeller. Kraftige AI-agenter forskyver kalkylen ytterligere i favør av å bygge fremfor å kjøpe.

Vi planlegger å åpne kildekoden til CobbleDB snart, slik at andre team som bygger AI-native søkesystemer kan dra nytte av det samme lagringslaget som gjør søk raskere og billigere i produktene våre.

Sitat

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