CobbleDB: Reconstruirea stocării de căutare AI pentru latență și costuri mai scăzute

O arhitectură decuplată cu stare durabilă separată, actualizări în lot și regăsire în momentul interogării.

AutoriPerplexity Engineering

Căutarea nativă AI impune cerințe diferite asupra modului în care conținutul web trebuie procesat, stocat și servit. Pentru a satisface aceste cerințe la scară, am reconstruit întreaga stivă de la zero pentru a îmbunătăți performanța și a reduce costurile.

Am construit propriile noastre infrastructuri de crawling, indexare și clasare, modele de embedding și motoare de inferență pentru a alimenta procesarea eficientă și servirea rapidă. Extindem acum această abordare către stocare, țesutul conjunctiv în care procesarea scrie și din care servirea citește.

Bazele de date gestionate, cum ar fi DynamoDB, au oferit o stocare fiabilă, dar am avut un control limitat asupra modului în care erau stocate datele și erau servite citirile, iar prețurile bazate pe utilizare au crescut odată cu dimensiunea indexului și volumul interogărilor. Separat, conducta noastră anterioară a cuplat procesarea documentelor direct la baza de date de servire, îngreunând gestionarea actualizărilor independent de citirile în timp real.

Cu ajutorul unui nou roi intern de agenți de codificare, am ricostruit acest strat în jurul CobbleDB, un magazin cheie-valoare rapid, susținut de Pillar pentru starea durabilă a documentelor și publicare, și Lorry pentru actualizări în lot. Redesignul separă procesarea documentelor de ingestia în stocul rapid și ne permite să ajustăm servirea la fluxul nostru de lucru. Măsurătorile de producție înainte și după arată o latență a citirii în lot de aproximativ 5× mai mică, iar modelul nostru intern de costuri estimează economii de cel puțin 20% față de DynamoDB.

Infrastructură de stocare pentru căutarea nativă AI

O stivă de căutare clasică este construită pentru un cititor uman, nu pentru un model de limbaj. Această diferență modifică granularitatea la care sistemul trebuie să pregătească, să stocheze și să servească conținutul web.

Pentru a transforma HTML-ul brut în conținut gata de model, conducta noastră de procesare curăță fiecare pagină, o împarte în pasaje coerente semantic, calculează reprezentările lor vectoriale (embeddings) și stochează pasajele și embeddings împreună în baza de date. În momentul interogării, conducta de servire selectează textul cel mai relevant pentru interogare și îl transmite modelului.

Două fluxuri de lucru distincte

Prin urmare, stratul de stocare trebuie să suporte două fluxuri de lucru cu cerințe diferite: ingestia actualizărilor din conducta de procesare și servirea citirilor cu latență redusă.

În ceea ce privește scrierea, paginile noi sosesc continuu, paginile existente se modifică, iar modificările aduse metodelor de segmentare sau modelelor de embedding pot necesita reprocesarea unor porțiuni mari din corpus. Stratul de stocare trebuie să încorporeze aceste actualizări în starea durabilă a documentului, să țină evidența versiunilor curente, să publice reprezentările corecte către sistemele din aval și să accepte completări mari (backfills) fără a repeta întreaga conductă de la accesarea brută (raw crawl).

În schimb, citirile din timpul interogării solicită loturi de chei de pagini și preiau textul pasajului corespunzător și reprezentările vectoriale (embeddings) pentru clasare și selectarea pasajelor. Aceste citiri se află pe calea către răspunsul final, așa că trebuie finalizate rapid.

Aceste volume de lucru necesită designuri diferite. Procesarea beneficiază de cozi durabile și de capacitatea de a livra actualizări în mod eficient, de a reîncerca eșecurile și de a aduce înregistrările existente la zi la scară largă. Servirea beneficiază de înregistrări precalculate, cache-uri calde și citiri rapide în lot, cu o muncă minimă în momentul interogării. Cuplarea strânsă între cele două poate face ca lucrările de recuperare și actualizările mari să concureze cu citirile sensibile la latență.

Unde baza de date gestionată nu face față

DynamoDB ne-a oferit o stocare cu disponibilitate ridicată, fără povara operării unei baze de date distribuite. Un tabel dedicat cu pagini pregătite conținea fragmente și reprezentări vectoriale (embeddings) pentru citirile efectuate în timpul interogării, în timp ce conținutul complet al paginii și metadatele erau stocate separat. Cu toate acestea, pe măsură ce volumul nostru de lucru a crescut, trei constrângeri au devenit importante.

În primul rând, prețul bazat pe utilizare taxează fiecare octet citit sau scris. O singură cerere de API de căutare conține 100-120 de chei corespunzătoare paginilor pe care trebuie să le preluăm. Procesul de preluare a datelor împarte o cerere în loturi mai mici de 10-20 de chei de pagină, cu dimensiunea medie a elementului de aproximativ 50 KB. Citirea repetată a acelor înregistrări a consumat o capacitate substanțială, în timp ce accesările (crawls) continue și modificările aduse segmentării sau modelelor de embedding au generat mai multe scrieri. Economia depindea, de asemenea, de dimensiunea și frecvența înregistrărilor, nu doar de numărul de apeluri API.

În al doilea rând, serviciul gestionat nu are flexibilitatea de care avem nevoie pentru optimizarea performanței. Operațiunea de care aveam nevoie era restrânsă: având în vedere un lot de identificatori de pagină, returnează conținutul lor pregătit rapid. Ne-am dorit controlul asupra mașinii care deținea o partiție, asupra memoriei dedicate cache-ului său și asupra replicii care a gestionat o cerere. Aceste controale contează deoarece o citire neetectată în cache, un salt între zone sau o replică lentă poate întârzia întregul lot. DynamoDB își gestionează propriul comportament de stocare și replică în spatele API-ului. Am putut configura serviciul, dar nu am putut regla amplasarea sa internă, cache-urile motorului de stocare sau politica de selecție a replicilor în funcție de volumul nostru de lucru.

În al treilea rând, procesarea era legată direct de scrierile în stocul rapid. Conducta noastră de procesare anterioară a scris fiecare pagină pregătită direct în DynamoDB. Prin urmare, o lucrare mare de reprocesare a devenit un val de actualizări individuale ale stocului rapid, iar procesarea trebuia să se adapteze la limitele de debit și comportamentul de reîncercare ale stocului rapid. Nu am putut rula MapReduce pe întreaga bază de date în 1-2 zile pentru a încerca o nouă metodă de segmentare, pentru a ne schimba modelul de embedding sau pentru a adăuga un nou câmp. Nu exista un flux independent de actualizări ale paginilor salvate pe care un serviciu separat să le poată grupa în loturi și să le livreze în stocul rapid în propriul său ritm.

Abordarea acestor constrângeri a necesitat mai mult decât simpla înlocuire a bazei de date. A trebuit să separăm starea durabilă a documentului și livrarea actualizărilor de stocarea din timpul interogării, urmând să optimizăm stocul rapid în mod specific pentru citiri rapide în lot.

Separarea stării documentului, a livrării actualizărilor și a servirii

Noua arhitectură atribuie fiecare responsabilitate unei componente diferite. Pillar menține o stare durabilă a documentului și decide ce să publice. Lorry transformă acele exporturi în loturi partiționate. CobbleDB preia loturile și servește înregistrări pregătite după cheia paginii.

Înainte și după migrarea stocării. Pillar și Lorry separă starea durabilă a documentelor și livrarea actualizărilor de citirile la momentul interogării, deservite de CobbleDB.

CobbleDB: calea de citire ajustabilă

CobbleDB este stocul nostru rapid optimizat pentru servirea înregistrărilor pregătite în momentul interogării. Este un magazin cheie-valoare distribuit: cheile sunt identificatori de pagină (URL-uri hash), iar valorile sunt reprezentarea procesată a paginii (pasajele sale pre-segmentate și reprezentările vectoriale per pasaj).

Schimbarea importantă este controlul nostru asupra configurării stocării.

Datele sunt partiționate și împărțite între nodurile de date. Acest format distribuit se asigură că baza de date poate scala orizontal pentru a găzdui un corpus enorm de pagini procesate. Fiecare partiție are trei replici pe trei noduri diferite; dacă o copie devine indisponibilă, celelalte pot continua să servească citirile.

Fiecare nod de date primește un subset de partiții. Nodul își stochează datele folosind RocksDB, un motor de stocare cheie-valoare încorporat, open-source, foarte potrivit pentru volume de lucru intensive la citire alimentate de ingestia în loturi.

Datele memorate în cache pot fi servite din memorie, în timp ce citirile neetectate în cache utilizează stocarea NVMe locală. Acest lucru ne oferă o modalitate directă de a regla echilibrul dintre memorie și disc pentru volumul de lucru observat, mai degrabă decât să acceptăm politica internă de cache a unui serviciu gestionat.

Un ruter de interogare stateless aplică hash pe chei către partiții și direcționează cererile către noduri în paralel. Arhitectura include, de asemenea, un serviciu de metadate bazat pe PostgreSQL și stocare de obiecte durabilă.

Acest design acceptă modelele de citire pe care le solicită volumul nostru de lucru. După ce o interogare trece prin regăsire (retrieval) și clasare pentru a obține un set candidat de pagini relevante, stocul rapid este interogat în bloc pentru conținutul acelor pagini. Aceste cereri de citire ajung la ruter și sunt direcționate către un nod de date corespunzător. Ruterul acordă prioritate trimiterii cererilor către un nod din aceeași zonă de disponibilitate pentru a reduce la minimum latența între zone, dacă este posibil. Fiecare nod de date poate prelua conținutul paginii folosind MultiGet, un API de citire în lot RocksDB conceput pentru a prelua mai multe chei simultan la latență scăzută. Dacă un nod răspunde lent, sistemul poate interoga și o altă replică de partiție pe un nod diferit, îmbunătățind latența la coadă.

De asemenea, putem evita implementarea unor funcționalități comune pe care fluxul nostru de lucru nu le necesită. De exemplu, majoritatea bazelor de date oferă garanții de consistență și suport pentru tranzacții, dar stocul nostru rapid nu necesită tranzacții sau replici sincronizate. Este acceptabil să existe un interval scurt între momentul în care un document este scris și momentul în care devine disponibil pentru citire, și este de asemenea în regulă ca o replică să preia datele mai repede decât alta. Omiterea caracteristicilor de care nu avem nevoie reduce costurile generale și complexitatea sistemului nostru.

Un router fără stare grupează cheile paginilor după partiție și citește replicile în paralel. Citirile lente pot fi trimise către o altă replică; prima răspuns pentru fiecare partiție este îmbinat în rezultat.

Pillar și Lorry: calea de scriere durabilă

Un strat de stocare separat ține evidența a tot ceea ce indexăm și procesăm, îmbinând actualizările într-o stare de document versionată și publicând reprezentările necesare în CobbleDB și în indexul de căutare. Am construit Pillar, stratul nostru de stare a documentelor și publicare, în acest scop.

Pillar este susținut de YTsaurus. Acesta stochează componentele paginii (cum ar fi metadatele, segmentele și reprezentările vectoriale) în familii de tabele YTsaurus separate. De asemenea, ține evidența versiunilor de segmente și embeddings, permițând mai multor reprezentări să coexiste fără a se suprascrie reciproc, astfel încât modelele subiacente să se poată schimba. Deoarece Pillar și YTsaurus folosesc stocare HDD ieftină, putem stoca mult mai multe documente decât am stocat anterior în DynamoDB.

Pillar urmărește subseturi sau grupuri de pagini definite prin politici, cum ar fi pagini proaspete sau pagini cu valoare ridicată. Memoria subsetului determină ce actualizări trebuie transmise componentelor din aval, cum ar fi CobbleDB. CobbleDB utilizează stocare NVMe mai scumpă, așa că exportăm doar documentele pe care dorim efectiv să le folosești în rezultatele căutării. Când conținutul unei pagini este actualizat, sau dacă este adăugat sau eliminat dintr-un subsubset, Pillar este responsabil pentru punerea în coadă a lucrării de export corespunzătoare.

Tranzacțiile atomice YTsaurus acoperă starea, intențiile de export și intrarea consumată pentru a asigura corectitudinea. Pentru ca o tranzacție să se finalizeze cu succes, sistemul trebuie să actualizeze starea durabilă, să marcheze intrarea ca fiind înregistrată și să pună în coadă toate exporturile necesare; dacă tranzacția se anulează, nicio modificare nu este realizată la niciun nivel.

CobbleDB se așteaptă ca actualizările să fie grupate în loturi în funcție de partiție. Exporturile Pillar sunt convertite în acest format de un serviciu stateless pe care îl numim Lorry. Singura sa sarcină este de a citi înregistrările de export dintr-o coadă persistentă aliniată la partiții, de a le acumula în fișiere batch per partiție și de a le preda către CobbleDB.

Între Lorry și Cobble, un protocol suplimentar separă planul de date (S3) de planul de control. Lorry scrie lotul în S3 sub un identificator unic și înregistrează lotul în cadrul Cobble. Replicile de partiție Cobble interoghează în mod independent API-ul planului de control sau de date pentru următorul lot S3 de preluat și aplică asincron actualizările de lot în ordine cronologică. Aceasta înseamnă că un nod lent sau în curs de recuperare poate recupera decalajul în propriul său ritm, în loc să încetinească restul sistemului.

Pillar publică exporturi durabile, iar Lorry scrie loturi partiționate în S3 și le înregistrează în planul de control. Fiecare replică CobbleDB descoperă și ingerează independent loturile sale în ordine cronologică.

Noua cale separă tranzacția de procesare imediată de ingestia în stocul rapid (hot-store) și oferă o cale reexecutabilă pentru actualizări incrementale și reconstrucții în masă. Această diviziune a muncii asigură că actualizările aduse stării durabile a documentului nu interferează cu latența căii de citire.

Validare

de 5x mai rapid

După migrarea stocului rapid (hot store), latența mediană a citirilor în lot a scăzut de la 31,4 ms la 5,60 ms. Valoarea p90 a scăzut de la 56,7 ms la 9,77 ms, iar p99 a scăzut de la 123 ms la 24,2 ms. Rapoartele variaza de la 5,08× la 5,80×, cu îmbunătățiri atât în cazul cererilor tipice, cât și la nivelul cozii (tail latency).

Latența de citire a loturilor în producție înainte și după migrare: P50 a scăzut de la 31.4 la 5.60 ms, P90 de la 56.7 la 9.77 ms, iar P99 de la 123 la 24.2 ms. Acestea sunt latențe de citire din hot-store din trafic live, la momente diferite, nu cereri potrivite sau latența de răspuns end-to-end.

Aceste numere sunt derivate din citiri în lot, unde fiecare interogare solicită aproximativ 10-15 chei. Dimensiunea medie a elementului din sarcină utilă (payload) este de 50KB. Ambele sisteme au fost interogate la aproximativ 200k rps, deși am rulat și teste de sarcină de până la 500k rps fără nicio degradare.

Pentru a izola contribuția CobbleDB, toate celelalte aspecte ale configurării au fost menținute constante. Cu toate acestea, aceasta rămâne o comparație observațională de tipul înainte și după; sistemele au deservit traficul de producție real în momente diferite, mai degrabă decât aceleași cereri în condiții controlate. Prin urmare, am măsurat și performanța ambelor sisteme pe un benchmark sintetic. Pentru a aproxima cererile observate în producție, am interogat ambele sisteme cu loturi de 10-15 chei și valori cuprinse între 100B și 101 KiB.

Latența sintetică de citire a loturilor pentru loturi de 10 și 15 chei, pe dimensiuni ale valorii de la 100 B la 100 KiB. Fiecare punct reprezintă media latenței P50, P90 sau P99 la nivel de rulare din două execuții per backend și configurație.

DynamoDB întâmpină dificultăți în acest context, deoarece calea sa de citire gestionată nu poate fi ajustată pentru citiri în lot de această dimensiune. În schimb, CobbleDB este optimizat exact pentru acest cadru, deoarece gestionăm amplasarea, memorarea în cache și controalele replicilor. Grupează cheile după partiție, execută citiri în paralel împotriva replicilor de partiție și poate atenua citirile lente pe alte replici. Rezultatele noastre demonstrează că aceste alegeri de design se traduc în citiri mai rapide într-un mediu de producție.

cu 20% mai ieftin

CobbleDB este, de asemenea, semnificativ mai ieftin decât DynamoDB. Pe lângă stocare, DynamoDB taxează și fiecare octet citit sau scris. La scară corpusului nostru și a traficului de servire, aceste taxe bazate pe utilizare devin o parte substanțială din costul total. Actualizările continue consumă capacitate de scriere, în timp ce fiecare lot de pagini pregătite preluate pentru o interogare consumă capacitate de citire.

Am calculat economiile de costuri pe baza estimărilor interne privind dimensiunea stocului, unitățile de capacitate de scriere și unitățile de capacitate de citire. La fiecare nivel de angajament, CobbleDB este cu cel puțin 20% mai ieftin decât DynamoDB. Economiile reale sunt probabil și mai mari, deoarece aceste rezultate nu iau în considerare economiile probabile la backup datorate compresiei.

Cum ne ajută un roi de agenți AI să îl construim

Construirea CobbleDB și a infrastructurii aferente a fost departe de a fi simplă. Numai baza de date principală CobbleDB are aproximativ 40.000 de linii de cod Rust, iar migrarea a necesitat, de asemenea, o muncă amplă pentru a verifica corectitudinea și comportamentul în caz de eșec la scară de producție, pentru a integra CobbleDB atât cu căile de procesare, cât și cu cele de servire și pentru a face tranziția în siguranță a traficului live.

Am accelerat acest efort folosind un sistem intern care operează un roi de agenți de codificare. Sistemul este proiectat să fie extrem de persistent; el reține obiectivele proiectului, riscurile active, istoricul depozitului și deciziile anterioare de-a lungul sesiunilor.

Am alocat un colectiv de astfel de agenți proactivi, activi în permanență, proiectului CobbleDB. Prima lor sarcină a fost să construiască un model precis al muncii deja în desfășurare. Au auditat canalul de proiect și firele (threads) active, au conectat elementele restante la pull request-urile și proprietarii lor și au produs o situație actuală a stării de sănătate a sistemului și a următoarelor acțiuni. De acolo, au operat în mai multe părți ale buclei inginerești:

  • A revizuit codul și modificările de infrastructură.
  • A adus în prim-plan blocaje care au fost ușor de ratat, inclusiv ipoteze nesigure de restaurare, referințe de build depășite și configurații care ar eșua în timpul rulării.
  • A pregătit remedieri punctuale, teste, modificări de monitorizare și documentație operațională.
  • A urmărit porțile de CI, revizuire și infrastructură, menținând în același timp acțiunile de producție deținute în mod explicit de oameni.
  • Lansări monitorizate și exerciții debackup sau restaurare, comparând apoi starea observată a sistemului cu criteriile de succes ale lansării.
  • A menținut rapoartele de proiect, astfel încât inginerii să poată vedea ce a fost livrat, ce a rămas blocat și unde era încă nevoie de discernământ uman.

Inginerii stabilesc în continuare arhitectura, au revizuit modificările majore și au autorizat operațiunile de producție. Agenții s-au ocupat de o mare parte din inspecția continuă și urmărirea dintre acele decizii. Acest lucru a permis unei echipe mici să conducă proiectul în mod eficient – infrastructura de bază CobbleDB a fost construită de doi ingineri umani și sute de agenți AI în două luni.

Concluzie

CobbleDB nu este o bază de date cu scop general; este construită special pentru volumul nostru de lucru specific de citiri repetate în lot ale înregistrărilor de pagini pregătite, alimentat de un flux de procesare asincron. Separând starea durabilă, livrarea actualizărilor și servirea în timpul interogării, putem optimiza fiecare responsabilitate în mod independent, fără a obliga o singură interfață de bază de date să le găzduiască pe toate sau fără a impune lucrări de întreținere pe calea de interogare în timp real. Migrarea la CobbleDB a îmbunătățit semnificativ atât latența mediană, cât și latența la coadă (tail latency), reducând în același timp costurile de infrastructură.

Pe măsură ce continuăm să dezvoltăm infrastructură nativă AI intern, putem optimiza performanța și costurile în moduri care nu sunt posibile atunci când suntem legați de implementări terțe și modele de prețuri. Agenții AI puternici înclină și mai mult balanța în favoarea construirii în detrimentul achiziționării.

Planificăm să lansăm curând CobbleDB ca open-source, astfel încât alte echipe care construiesc sisteme de căutare native AI să poată beneficia de același strat de stocare care face căutarea mai rapidă și mai ieftină în produsele noastre.

Citare

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