CobbleDB: Bygger om AI-söklagring för lägre latens och kostnad

En frikopplad arkitektur med separat beständigt tillstånd, batchade uppdateringar och hämtning vid frågetillfället.

FörfattarePerplexity Engineering

AI-inbyggd sökning ställer olika krav på hur webbinnehåll behöver bearbetas, lagras och betjänas. För att möta dessa krav i skala har vi byggt om hela stacken från grunden för att förbättra prestanda och minska kostnader.

Vi byggde vår egen infrastruktur för sökning, indexering och rangordning, inbäddningsmodeller och inferensmotorer för att driva effektiv bearbetning och snabb betjäning. Vi utökar nu den strategin till lagring, den bindväv som bearbetningen skriver till och betjäningen läser från.

Hanterade databaser som DynamoDB tillhandahöll tillförlitlig lagring, men vi hade begränsad kontroll över hur data lagrades och läsningar betjänades, och den användningsbaserade prissättningen växte med vår indexstorlek och frågvolym. Separat kopplade vår tidigare pipeline dokumentbearbetning direkt till databasen, vilket gjorde det svårt att hantera uppdateringar oberoende av liveresultat.

Med hjälp av en ny intern svärm av kodningsagenter byggde vi om detta lager kring CobbleDB, ett nyckel-värde-hetlager, med stöd av Pillar för beständigt dokumenttillstånd och publicering, och Lorry för batchade uppdateringar. Redesignen skiljer dokumentbearbetning från inmatning i det heta lagret och låter oss finjustera betjäningen efter vår arbetsbelastning. Mätningar före och efter i produktionen visar ungefär 5× lägre latens för batchläsning, och vår interna kostnadsmodell uppskattar minst 20 % besparingar i förhållande till DynamoDB.

Lagringsinfrastruktur för AI-inbyggd sökning

En klassisk sökstack är byggd för en mänsklig läsare, inte en språkmodell. Den skillnaden förändrar den granularitet som systemet behöver för att förbereda, lagra och tillhandahålla webbinnehåll.

För att förvandla rå HTML till modellfärdigt innehåll rensar vår bearbetningspipeline varje sida, delar upp den i semantiskt sammanhängande stycken, beräknar deras inbäddningar och lagrar styckena och inbäddningarna tillsammans i databasen. Vid frågetillfället väljer betjäningspipelinen ut den text som är mest relevant för frågan och skickar den till modellen.

Två skilda arbetsflöden

Lagringslagret måste därför stöder två arbetsflöden med olika krav: att mata in uppdateringar från bearbetningspipelinen och att tillhandahålla läsningar med låg latens.

På skrivsidan anländer nya sidor kontinuerligt, befintliga sidor ändras och ändringar av uppdelningsmetoder eller inbäddningsmodeller kan kräva ombearbetning av stora delar av korpusen. Lagringslagret måste integrera dessa uppdateringar i det beständiga dokumenttillståndet, spåra de aktuella versionerna, publicera rätt representationer till nedströmsystem och stödja stora bakfyllnader utan att upprepa hela pipelinen från den råa sökningen.

Däremot begär läsningar vid frågetillfället batchar med sidnycklar och hämtar motsvarande stycketext och inbäddningar för rangordning och styckeval. Dessa läsningar ligger på vägen till det slutliga svaret, så de måste slutföras snabbt.

Dessa arbetsbelastningar kräver olika design. Bearbetning drar nytta av beständiga köer och möjligheten att leverera uppdateringar effektivt, göra nya försök vid fel och uppdatera befintliga poster i skala. Betjäning drar nytta av förberäknade poster, varma cacher och snabba batchläsningar med minimalt arbete vid frågetillfället. Tät koppling mellan de två kan göra att återställningsarbete och stora uppdateringar konkurrerar med latenskänsliga läsningar.

Där hanterad databas kommer till kort

DynamoDB gav oss högt tillgänglig lagring utan bördan av att köra en distribuerad databas. En dedikerad tabell med förberedda sidor innehöll stycken och inbäddningar för läsningar vid frågetillfället, medan fullständigt sidinnehåll och metadata lagrades separat. Allt eftersom vår arbetsbelastning växte blev dock tre begränsningar viktiga.

För det första debiterar den användningsbaserade prissättningen för varje byte som läses eller skrivs. En enskild sök-API-förfrågan innehåller 100-120 nycklar som motsvarar sidor vi behöver hämta. Datahämtningsprocessen delar upp en förfrågan i mindre batchar om 10-20 sidnycklar där den genomsnittliga objektstorleken är ungefär 50 KB. Att upprepade gånger läsa dessa poster förbrukade betydande kapacitet, medan kontinuerliga sökningar och ändringar av uppdelnings- eller inbäddningsmodeller genererade fler skrivningar. Ekonomin berodde också på storleken och frekvensen på posterna, inte bara antalet API-anrop.

För det andra saknar den hanterade tjänsten den flexibilitet vi behöver för prestandajustering. Den åtgärd vi behövde var begränsad: med tanke på en batch med sididentifierare, returnera deras förberedda innehåll snabbt. Vi ville ha kontroll över vilken maskin som hade hand om en partition, hur mycket minne som avsattes för dess cache och vilken replik som hanterade en begäran. De kontrollerna spelar roll eftersom en ocachad läsning, ett hopp över zoner eller en långsam replik kan fördröja hela batchen. DynamoDB hanterar sin egen lagring och sitt replikbeteende bakom API:et. Vi kunde konfigurera tjänsten, men vi kunde inte finjustera dess interna placering, lagringsmotorns cacher eller policyn för replikval kring vår arbetsbelastning.

För det tredje var bearbetningen bunden direkt till skrivningar i det heta lagret. Vår föregående bearbetningspipeline skrev varje förberedd sida direkt till DynamoDB. Ett stort ombearbetningsjobb blev därför en våg av enskilda uppdateringar av det heta lagret, och bearbetningen var tvungen att anpassa sig till det heta lagrets genomflödesgränser och återförsöksbeteende. Vi kunde inte köra MapReduce över hela databasen på 1–2 dagar för att prova en ny uppdelningsmetod, ändra vår inbäddningsmodell eller lägga till ett nytt fält. Det fanns ingen oberoende ström av sparade siduppdateringar som en separat tjänst kunde samla i batchar och leverera till det heta lagret i sin egen takt.

Att åtgärda dessa begränsningar krävde mer än att bara byta ut databasen. Vi behövde separera det beständiga dokumenttillståndet och uppdateringsleveransen från lagringen vid frågetillfället, för att sedan optimera det heta lagret specifikt för snabba batchlästa läsningar.

Separera dokumenttillstånd, uppdateringsleverans och tillhandahållande

Den nya arkitekturen tilldelar varje ansvarsområde till en annan komponent. Pillar upprätthåller ett beständigt dokumenttillstånd och avgör vad som ska publiceras. Lorry förvandlar dessa exporter till partitionerade batchar. CobbleDB matar in batcharna och tillhandahåller förberedda poster per sidnyckel.

Före och efter lagringsmigreringen. Pillar och Lorry separerar hållbart dokumenttillstånd och leverans av uppdateringar från läsningar vid frågetillfället som tillhandahålls av CobbleDB.

CobbleDB: den justerbara lässökvägen

CobbleDB är vårt heta lager optimerat för att betjäna förberedda poster vid frågetillfället. Det är en distribuerad nyckel-värde-lagring: nycklarna är sididentifierare (hashade webbadresser), och värdena är sidans bearbetade representation (deras fördelade stycken och vektorinbäddningar per stycke).

Den viktiga förändringen är vår kontroll över lagringskonfigurationen.

Data partitioneras och delas upp över datanoder. Detta distribuerade format säkerställer att databasen kan skalas horisontellt för att rymma ett enormt korpus av bearbetade sidor. Varje partition har tre repliker på tre olika noder; om en kopia blir otillgänglig kan de andra fortsätta att betjäna läsningar.

Varje datanod tilldelas en delmängd av partitionerna. Noden lagrar sin data med hjälp av RocksDB, en inbäddad nyckel-värde-lagringsmotor med öppen källkod som är väl lämpad för lästung arbetsbelastning som matas av batchinmatning.

Cachad data kan betjänas från minnet, medan ocachade läsningar använder lokal NVMe-lagring. Detta ger oss ett direkt sätt att finjustera balansen mellan minne och disk för den observerade arbetsbelastningen, i stället för att acceptera en hanterad tjäns internerade cachepolicy.

En statslös frågerouter hashar nycklar till partitioner och dirigerar förfrågningar till noder parallellt. Arkitekturen inkluderar även en PostgreSQL-baserad metadatatjänst och beständig objektlagring.

Denna design stöder de läsmönster som vår arbetsbelastning kräver. Efter att en fråga har gått igenom hämtning och rangordning för att erhålla en kandidatuppsättning av relevanta sidor, frågas det heta lagret i bulk efter innehållet på dessa sidor. Dessa läsbegäranden anländer till routern och dirigeras till en motsvarande datanod. Routern prioriterar att skicka begäranden till en nod i samma tillgänglighetszon för att minimera latensen över zoner, om möjligt. Varje datanod kan hämta sidinnehållet med MultiGet, ett RocksDB-batchläsnings-API som är utformat för att hämta flera nycklar samtidigt med låg latens. Om en nod är långsam att svara kan systemet också fråga en annan partitionsreplik på en annan nod, vilket förbättrar svanslatensen.

Vi kan också undvika att implementera vanliga funktioner som vårt arbetsflöde inte kräver. Till exempel erbjuder de flesta databaser konsistensgarantier och transaktionsstöd, men vårt heta lager kräver inte transaktioner eller synkroniserade repliker. Det är acceptabelt att det finns en kort tidslucka mellan när ett dokument skrivs och när det är tillgängligt för läsning, och det är också okej att en replik matar in data snabbare än en annan. Att utelämna funktioner vi inte behöver minskar omkostnaderna och kostnaden för vårt system.

En tillståndslös router grupperar sidnycklar efter partition och läser repliker parallellt. Långsamma läsningar kan skickas till en annan replik; det första svaret för varje partition slås samman till resultatet.

Pillar och Lorry: den beständiga skrivsökvägen

Ett separat lagringslager håller reda på allt vi söker igenom och bearbetar, sammanfogar uppdateringar till ett versionshanterat dokumenttillstånd och publicerar de nödvändiga representationerna till CobbleDB och sökindexet. Vi byggde Pillar, vårt dokumenttillstånds- och publiceringslager, för detta ändamål.

Pillar backas upp av YTsaurus. Den lagrar sidkomponenter (såsom metadata, stycken och inbäddningar) i separata YTsaurus-tabellfamiljer. Den spårar också versionerna av stycken och inbäddningar, vilket gör att flera representationer kan samexistera utan att skriva över varandra så att de underliggande modellerna kan förändras. Eftersom Pillar och YTsaurus använder billig HDD-lagring kan vi lagra långt fler dokument än vi tidigare lagrade i DynamoDB.

Pillar spårar delmängder, eller policydefinierade grupper av sidor som färska sidor eller högvärdiga sidor. Delmängdsmedlemskap avgör vilka uppdateringar som behöver skickas vidare till nedströmskomponenter som CobbleDB. CobbleDB använder dyrare NVMe-lagring, så vi exporterar bara de dokument som vi faktiskt vill använda i sökresultaten. När en sidas innehåll uppdateras, eller om den läggs till eller tas bort från en delmängd, ansvarar Pillar för att köa motsvarande exportarbete.

Atomiska YTsaurus-transaktioner omfattar tillstånd, exportavsikter och konsumerad indata för att säkerställa korrekthet. För att en transaktion ska slutföras framgångsrikt måste systemet uppdatera det beständiga tillståndet, markera indata som bokförd och köa alla nödvändiga exporter; om transaktionen avbryts görs ingen ändring i något lager.

CobbleDB förväntar sig att uppdateringar samlas i batchar efter partition. Pillar-exporter konverteras till detta format av en statslös tjänst som vi kallar Lorry. Dess enda uppgift är att läsa exportposter från en beständig partitionsjusterad kö, samla dem i batchfiler per partition och lämna över dem till CobbleDB.

Mellan Lorry och Cobble skiljer ett ytterligare protokoll dataplanet (S3) från kontrollplanet. Lorry skriver batchen till S3 under en unik identifierare och registrerar batchen i Cobble. Cobble-partitionsrepliker frågar oberoende av varandra efter nästa S3-batch att mata in via batch- eller kontrollplans-API:et och tillämpar asynkront batchuppdateringar i kronologisk ordning. Det innebär att en långsam eller återhämtande nod kan komma ikapp i sin egen takt, i stället för att sakta ner resten av systemet.

Pillar publicerar hållbara exporter, och Lorry skriver partitionerade batcher till S3 och registrerar dem med kontrollplanet. Varje CobbleDB-replika upptäcker och läser oberoende in sina batcher i kronologisk ordning.

Den nya sökvägen skiljer den omedelbara bearbetningstransaktionen från inmatning i det heta lagret och tillhandahåller en reproducerbar sökväg för inkrementella uppdateringar och massåteruppbyggnader. Denna arbetsfördelning säkerställer att uppdateringar av det beständiga dokumenttillståndet inte stör lässökvägens latens.

Validering

5x snabbare

Efter migreringen av det heta lagret sjönk medianfördröjningen för batchläsning från 31,4 ms till 5,60 ms. P90 sjönk från 56,7 ms till 9,77 ms, och P99 sjönk från 123 ms till 24,2 ms. Förhållandena sträcker sig från 5,08× till 5,80×, med förbättringar i både typiska förfrågningar och svansen.

Latens för batchläsning i produktion före och efter migreringen: P50 sjönk från 31,4 till 5,60 ms, P90 från 56,7 till 9,77 ms och P99 från 123 till 24,2 ms. Detta är läslatens i hot store från live-trafik vid olika tidpunkter, inte matchade begäranden eller svarslatens från början till slut.

Dessa siffror härleds från batchläsningar, där varje fråga begär ungefär 10-15 nycklar. Den genomsnittliga storleken på nyttolastobjektet är 50 KB. Båda systemen frågades med cirka 200 k rps, även om vi också körde belastningstester upp till 500 k rps utan försämring.

För att isolera CobbleDBs bidrag hölls alla andra aspekter av konfigurationen konstanta. Detta förblir dock en observationell före-och-efter-jämförelse; systemen hanterade live-produktionspeltrafik vid olika tidpunkter snarare än samma förfrågningar under kontrollerade förhållanden. Vi mätte därför också prestandan för båda systemen på ett syntetiskt riktmärke. För att approximera de förfrågningar som ses i produktionen frågade vi båda med batchar på 10-15 nycklar och värden som sträcker sig från 100B till 100 KiB.

Syntetisk latens för batchläsningar för batcher med 10 och 15 nycklar över värdestorlekar från 100 B till 100 KiB. Varje punkt är medelvärdet av körningsnivåns P50-, P90- eller P99-latens från två körningar per backend och konfiguration.

DynamoDB kämpar i detta sammanhang eftersom dess hanterade lässökväg inte kan finjusteras för batchläsningar av denna storlek. Däremot är CobbleDB optimerat för just denna miljö, eftersom vi hanterar placering, cachning och replikkontroller. Det grupperar nycklar efter partition, utför läsningar parallellt mot partitionsrepliker och kan skydda långsamma läsningar mot andra repliker. Våra resultat visar att dessa designval översätts till snabbare läsningar i en produktionsmiljö.

20 % billigare

CobbleDB är också avsevärt billigare än DynamoDB. Utöver lagring debiterar DynamoDB även för varje byte som läses eller skrivs. Vid den skala som vårt korpus och vår betjäningstrafik har blir dessa användningsbaserade avgifter en betydande del av totalkostnaden. Kontinuerliga uppdateringar förbrukar skrivkapacitet, medan varje batch med förberedda sidor som hämtas för en fråga förbrukar läskapacitet.

Vi beräknade kostnadsbesparingarna baserat på interna uppskattningar av lagringsstorlek, skrivkapacitetsenheter och läskapacitetsenheter. Vid varje åtagandenivå är CobbleDB minst 20 % billigare än DynamoDB. De verkliga besparingarna är sannolikt ännu högre, eftersom dessa resultat inte tar hänsyn till de troliga backup-besparingarna från komprimering.

Hur en svärm av AI-agenter hjälper oss att bygga det

Att bygga CobbleDB och den omgivande infrastrukturen var långt ifrån enkelt. CobbleDBs kärndatabas enbart är ungefär 40 000 rader Rust, och migreringen krävde också omfattande arbete för att verifiera korrekthet och felbeteende i produktionsskala, integrera CobbleDB med både bearbetnings- och betjäningsvägar och säkert övergå livetrafik.

Vi accelererade detta arbete med hjälp av ett internt system som driver en svärm av kodningsagenter. Systemet är utformat för att vara mycket beständigt; det behåller projektets mål, aktiva risker, lagringshistorik och tidigare beslut över olika sessioner.

Vi tilldelade en kollektiv grupp av sådana proaktiva, ständigt aktiva agenter till CobbleDB-projektet. Deras första uppgift var att bygga en exakt modell av det arbete som redan pågick. De granskade projektkanalen och aktiva trådar, kopplade utestående objekt till deras pull requests och ägare, och tog fram en aktuell rapport om systemhälsan och nästa åtgärder. Därifrån arbetade de över flera delar av ingenjörsslingan:

  • Granskade kod- och infrastrukturändringar.
  • Tog upp blockeringar som var lätta att missa, inklusive osäkra återställningsantaganden, föråldrade byggreferenser och konfigurationer som skulle misslyckas vid körning.
  • Förberedde fokuserade korrigeringar, tester, övervakningsändringar och operativ dokumentation.
  • Spårade CI-, gransknings- och infrastrukturgrindar samtidigt som produktionsåtgärder uttryckligen förblev mänskligt ägda.
  • Övervakade utrullningar och backup- eller återställningsövningar jämförde sedan det observerade systemtillståndet med utrullningens framgångskriterier.
  • Underhöll projekutrapporter så att ingenjörer kunde se vad som hade skeppats, vad som förblev blockerat och var mänsklig bedömning fortfarande krävdes.

Ingenjörer sätter fortfarande arkitekturen, granskar följdriktiga ändringar och auktoriserar produktionsåtgärder. Agenterna hanterade mycket av den kontinuerliga inspektionen och uppföljningen mellan dessa beslut. Detta gjorde det möjligt för ett litet team att driva projektet effektivt – kärninfrastrukturen för CobbleDB byggdes av två mänskliga ingenjörer och hundratals AI-agenter på två månader.

Slutsats

CobbleDB är inte en generell databas; den är skräddarsydd för vår specifika arbetsbelastning med upprepade batchläsningar av förberedda sidposter, matad av en asynkron bearbetningspipeline. Genom att separera beständigt tillstånd, uppdateringsleverans och betjäning vid frågetillfället kan vi optimera varje ansvarsområde oberoende utan att tvinga ett enda databasgränssnitt att rymma alla eller skjuta över underhållsarbete på den aktiva frågesökvägen. Att migrera till CobbleDB förbättrade avsevärt både median- och svanslatens samtidigt som infrastrukturkostnaderna minskade.

När vi fortsätter att utveckla AI-inbyggd infrastruktur internt kan vi optimera prestanda och kostnad på sätt som inte är möjliga när vi är bundna till tredje parts implementeringar och prismodeller. Kraftfulla AI-agenter flyttar kalkylen ännu mer till förmån för att bygga framför att köpa.

Vi planerar att släppa CobbleDB med öppen källkod inom kort, så att andra team som bygger AI-inbyggda sökssystem kan dra nytta av samma lagringslager som gör sökningen snabbare och billigare i våra produkter.

Citering

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://perplexity.coolkeypoint.com/hub/blog/cobbledb}
}