CobbleDB: Neuaufbau des KI-Suchspeichers für geringere Latenz und Kosten

Eine entkoppelte Architektur mit getrenntem dauerhaftem Status, Batch-Aktualisierungen und Abfragezeit-Retrieval.

AutorenPerplexity Engineering

Die KI-native Suche stellt andere Anforderungen an die Verarbeitung, Speicherung und Bereitstellung von Webinhalten. Um diesen Anforderungen im großen Maßstab gerecht zu werden, haben wir den gesamten Stack von Grund auf neu aufgebaut, um die Leistung zu verbessern und die Kosten zu senken.

Wir haben unsere eigene Infrastruktur für Crawling, Indizierung und Ranking, Embedding-Modelle und Inferenz-Engines entwickelt, um eine effiziente Verarbeitung und schnelle Bereitstellung zu ermöglichen. Diesen Ansatz weiten wir nun auf den Speicher aus – das verbindende Element, in das die Verarbeitung schreibt und aus dem das Serving liest.

Verwaltete Datenbanken wie DynamoDB boten zuverlässigen Speicher, aber wir hatten nur begrenzte Kontrolle darüber, wie Daten gespeichert und Lesevorgänge bereitgestellt wurden, und die nutzungsbasierte Preisgestaltung wuchs mit unserer Indexgröße und unserem Abfragevolumen. Unabhängig davon koppelte unsere vorherige Pipeline die Dokumentverarbeitung direkt an die Serving-Datenbank, was es schwierig machte, Aktualisierungen unabhängig von Live-Lesevorgängen zu verwalten.

Mit Hilfe eines neuen internen Programmieragenten-Schwrms haben wir diese Ebene rund um CobbleDB neu aufgebaut – einen Key-Value-Hot-Store, unterstützt von Pillar für den dauerhaften Dokumentstatus und die Veröffentlichung sowie Lorry für Batch-Aktualisierungen. Das Redesign trennt die Dokumentverarbeitung von der Hot-Store-Ingetion und ermöglicht es uns, das Serving auf unseren Workload abzustimmen. Vorher-Nachher-Messungen in der Produktion zeigen eine etwa 5-mal geringere Latenz bei Batch-Lesevorgängen, und unser internes Kostenmodell schätzt Einsparungen von mindestens 20 % im Vergleich zu DynamoDB.

Speicherinfrastruktur für KI-native Suche

Ein klassischer Such-Stack ist für menschliche Leser konzipiert, nicht für Sprachmodelle. Dieser Unterschied verändert die Granularität, mit der das System Webinhalte aufbereiten, speichern und bereitstellen muss.

Um rohe HTML-Inhalte in modellfertige Inhalte zu verwandeln, bereinigt unsere Verarbeitungspipeline jede Seite, unterteilt sie in semantisch kohärente Passagen, berechnet deren Embeddings und speichert die Passagen und Embeddings gemeinsam in der Datenbank. Zur Abfragezeit wählt die Serving-Pipeline den für die Abfrage relevantesten Text aus und leitet ihn an das Modell weiter.

Zwei verschiedene Workflows

Die Speicherebene muss daher zwei Workflows mit unterschiedlichen Anforderungen unterstützen: das Einlesen von Aktualisierungen aus der Verarbeitungspipeline und das Bereitstellen von Lesevorgängen mit geringer Latenz.

Auf der Schreibseite kommen kontinuierlich neue Seiten hinzu, bestehende Seiten ändern sich, und Änderungen an Chunking-Methoden oder Embedding-Modellen können eine Neuverarbeitung großer Teile des Korpus erfordern. Die Speicherebene muss diese Aktualisierungen in den dauerhaften Dokumentstatus integrieren, die aktuellen Versionen verfolgen, die richtigen Darstellungen an nachgelagerte Systeme übergeben und große Backfills unterstützen, ohne die gesamte Pipeline ab dem rohen Crawl zu wiederholen.

Im Gegensatz dazu fordern Lesevorgänge zur Abfragezeit Batches von Seitenschlüsseln an und rufen den entsprechenden Passagentext sowie Embeddings für Ranking und Passagenauswahl ab. Diese Lesevorgänge liegen auf dem Pfad zur finalen Antwort und müssen daher schnell abgeschlossen werden.

Diese Workloads erfordern unterschiedliche Designs. Die Verarbeitung profitiert von dauerhaften Warteschlangen und der Fähigkeit, Aktualisierungen effizient zu liefern, Fehler erneut zu versuchen und vorhandene Datensätze im großen Stil auf den neuesten Stand zu bringen. Das Serving profitiert von vorab berechneten Datensätzen, warmen Caches und schnellen Batch-Lesevorgängen mit minimalem Aufwand zur Abfragezeit. Eine enge Kopplung zwischen beiden kann dazu führen, dass Wiederherstellungsarbeiten und große Aktualisierungen mit latenzempfindlichen Lesevorgängen konkurrieren.

Wo verwaltete Datenbanken an ihre Grenzen stoßen

DynamoDB bot uns hochverfügbaren Speicher ohne den Aufwand des Betriebs einer verteilten Datenbank. Eine dedizierte Tabelle für vorbereitete Seiten enthielt Textpassagen und Embeddings für Lesevorgänge zur Abfragezeit, während der vollständige Seiteninhalt und die Metadaten separat gespeichert wurden. Als unser Workload wuchs, wurden jedoch drei Einschränkungen wichtig.

Erstens wird bei der nutzungsbasierten Preisgünstigung jedes gelesene oder geschriebene Byte berechnet. Eine einzelne Search-API-Anfrage enthält 100-120 Schlüssel, die Seiten entsprechen, die wir abrufen müssen. Der Datenabrufvorgang unterteilt eine Anfrage in kleinere Batches von 10-20 Seitenschlüsseln, wobei die durchschnittliche Elementgröße etwa 50 KB beträgt. Das wiederholte Lesen dieser Datensätze verbrauchte erhebliche Kapazitäten, während kontinuierliche Crawls und Änderungen an Chunking- oder Embedding-Modellen mehr Schreibvorgänge erzeugten. Die Wirtschaftlichkeit hing auch von der Größe und Häufigkeit der Datensätze ab, nicht nur von der Anzahl der API-Aufrufe.

Zweitens fehlt verwalteten Diensten die Flexibilität, die wir für die Leistungsoptimierung benötigen. Der erforderliche Vorgang war einfach: Gegeben ein Batch von Seitenbezeichnern, geben Sie deren vorbereitete Inhalte schnell zurück. Wir wollten die Kontrolle darüber haben, welche Maschine eine Partition enthält, wie viel Arbeitsspeicher für den Cache reserviert ist und welches Replikate eine Anfrage verarbeitet. Diese Steuerungen sind wichtig, da ein nicht zwischengespeicherter Lesevorgang, ein zonenübergreifender Hop oder ein langsames Replikate die gesamte Charge verzögern können. DynamoDB verwaltet seinen eigenen Speicher und das Replikatsverhalten hinter der API. Wir konnten den Dienst konfigurieren, aber wir konnten seine interne Platzierung, die Caches der Storage-Engine oder die Replikatsauswahlrichtlinie nicht auf unseren Workload abstimmen.

Drittens war die Verarbeitung direkt an die Hot-Store-Schreibvorgänge gekoppelt. Unsere vorherige Verarbeitungspipeline schrieb jede vorbereitete Seite direkt in DynamoDB. Ein großer Neuverarbeitungjob wurde daher zu einer Welle einzelner Hot-Store-Aktualisierungen, und die Verarbeitung musste sich den Durchsatzlimits und dem Wiederholungsverhalten (Retry-Verhalten) des Hot-Stores anpassen. Wir konnten MapReduce nicht innerhalb von 1–2 Tagen über die gesamte Datenbank ausführen, um eine neue Chunking-Methode auszuprobieren, unser Embedding-Modell zu ändern oder ein neues Feld hinzuzufügen. Es gab keinen unabhängigen Stream gespeicherter Seitenaktualisierungen, den ein separater Dienst in Batches zusammenfassen und in seinem eigenen Tempo an den Hot-Store liefern konnte.

Die Behebung dieser Einschränkungen erforderte mehr als nur den Austausch der Datenbank. Wir mussten den dauerhaften Dokumentstatus und die Aktualisierungsbereitstellung vom Speicher zur Abfragezeit trennen und dann den Hot-Store speziell für schnelle Batch-Lesevorgänge optimieren.

Trennung von Dokumentstatus, Aktualisierungsbereitstellung und Serving

Die neue Architektur weist jede Verantwortung einer anderen Komponente zu. Pillar verwaltet den dauerhaften Dokumentstatus und entscheidet, was veröffentlicht wird. Lorry wandelt diese Exporte in partitionierte Batches um. CobbleDB liest die Batches ein und stellt vorbereitete Datensätze nach Seitenschlüssel bereit.

Vor und nach der Speichermigration. Pillar und Lorry trennen dauerhaften Dokumentzustand und Auslieferung von Aktualisierungen von Lesezugriffen zur Abfragezeit, die von CobbleDB bereitgestellt werden.

CobbleDB: Der anpassbare Lesepfad

CobbleDB ist unser für die Bereitstellung vorbereiteter Datensätze zur Abfragezeit optimierter Hot-Store. Es handelt sich um einen verteilten Key-Value-Store: Die Schlüssel sind Seitenbezeichner (gehashte URLs) und die Werte sind die verarbeitete Darstellung der Seite (ihre vorab aufgeteilten Passagen und Vektor-Embeddings pro Chunk).

Die wichtige Änderung ist unsere Kontrolle über die Speicher Konfiguration.

Daten werden partitioniert und auf Datenknoten verteilt. Dieses verteilte Format stellt sicher, dass die Datenbank horizontal skaliert werden kann, um ein enormes Korpus verarbeiteter Seiten aufzunehmen. Jede Partition verfügt über drei Replikate auf drei verschiedenen Knoten; wenn eine Kopie nicht verfügbar ist, können die anderen weiterhin Lesevorgänge bedienen.

Jedem Datenknoten wird eine Untermenge der Partitionen zugewiesen. Der Knoten speichert seine Daten mithilfe von RocksDB, einer Open-Source-Key-Value-Embedded-Speicherengine, die sich gut für leseintensive Workloads eignet, die durch Batch-Ingetion gespeist werden.

Zwischengespeicherte Daten können aus dem Arbeitsspeicher bereitgestellt werden, während nicht zwischengespeicherte Lesevorgänge den lokalen NVMe-Speicher nutzen. Dies gibt uns eine direkte Möglichkeit, das Verhältnis von Arbeitsspeicher zu Festplatte für den beobachteten Workload abzustimmen, anstatt die interne Cache-Richtlinie eines verwalteten Dienstes zu akzeptieren.

Ein zustandsloser Abfragerouter hasht Schlüssel auf Partitionen und leitet Anfragen parallel an Knoten weiter. Die Architektur umfasst zudem einen auf PostgreSQL basierenden Metadatendienst und dauerhaften Objektspeicher.

Dieses Design unterstützt die von unserem Workload geforderten Lesemuster. Nachdem eine Abfrage das Abrufen (Retrieval) und Ranking durchlaufen hat, um eine Kandidatenmenge relevanter Seiten zu erhalten, wird der Hot-Store massenhaft nach den Inhalten dieser Seiten abgefragt. Diese Leseanfragen treffen beim Router ein und werden an einen entsprechenden Datenknoten weitergeleitet. Der Router priorisiert nach Möglichkeit das Senden von Anfragen an einen Knoten in derselben Availability Zone, um die Zonenübergreifende Latenz zu minimieren. Jeder Datenknoten kann den Seiteninhalt über MultiGet abrufen, eine RocksDB-Batch-Lese-API, die entwickelt wurde, um mehrere Schlüssel gleichzeitig mit geringer Latenz abzurufen. Wenn ein Knoten langsam reagiert, kann das System auch ein anderes Partitionsreplikat auf einem anderen Knoten abfragen, was die Tail-Latenz verbessert.

Wir können auch die Implementierung gängiger Funktionen vermeiden, die unser Workflow nicht benötigt. Beispielsweise bieten die meisten Datenbanken Konsistenzgarantien und Transaktionsunterstützung, aber unser Hot-Store erfordert keine Transaktionen oder synchronisierten Replikate. Es ist akzeptabel, wenn es eine kurze Lücke zwischen dem Schreiben eines Dokuments und seiner Verfügbarkeit für Lesevorgänge gibt, und es ist auch in Ordnung, wenn ein Replikate Daten schneller einliest als ein anderes. Das Weglassen von Funktionen, die wir nicht benötigen, reduziert den Overhead und die Kosten unseres Systems.

Ein zustandsloser Router gruppiert Seitenschlüssel nach Partition und liest Replikate parallel. Langsame Lesevorgänge können an eine andere Replik gesendet werden; die erste Antwort für jede Partition wird in das Ergebnis zusammengeführt.

Pillar und Lorry: Der dauerhafte Schreibpfad

Eine separate Speicherebene protokolliert alles, was wir crawlen und verarbeiten, führt Aktualisierungen mit dem versionierten Dokumentstatus zusammen und veröffentlicht die erforderlichen Darstellungen in CobbleDB und dem Suchindex. Zu diesem Zweck haben wir Pillar entwickelt, unsere Ebene für Dokumentstatus und Veröffentlichung.

Pillar wird von YTsaurus unterstützt. Es speichert Seitenkomponenten (wie Metadaten, Chunks und Embeddings) in separaten YTsaurus-Tabellenfamilien. Es verfolgt auch die Versionen von Chunks und Embeddings, sodass mehrere Darstellungen nebeneinander existieren können, ohne sich gegenseitig zu überschreiben, wodurch die zugrunde liegenden Modelle geändert werden können. Da Pillar und YTsaurus günstigen HDD-Speicher verwenden, können wir weitaus mehr Dokumente speichern als zuvor in DynamoDB.

Pillar verfolgt Untermengen oder richtliniendefinierte Gruppen von Seiten wie frische Seiten oder Seiten mit hohem Wert. Die Mitgliedschaft in einer Untermenge bestimmt, welche Aktualisierungen an nachgelagerte Komponenten wie CobbleDB weitergegeben werden müssen. CobbleDB verwendet teureren NVMe-Speicher, weshalb wir nur die Dokumente exportieren, die wir tatsächlich in den Suchergebnissen verwenden möchten. Wenn der Inhalt einer Seite aktualisiert wird oder sie zu einer Untermenge hinzugefügt oder daraus entfernt wird, ist Pillar dafür verantwortlich, die entsprechende Exportarbeit in die Warteschlange einzureihen.

Atomare YTsaurus-Transaktionen umfassen Status, Exportabsichten und verarbeitete Eingaben, um die Korrektheit sicherzustellen. Damit eine Transaktion erfolgreich abgeschlossen wird, muss das System den dauerhaften Status aktualisieren, die Eingabe als erfasst markieren und alle erforderlichen Exporte in die Warteschlange einreihen; wird die Transaktion abgebrochen, wird auf keiner Ebene eine Änderung vorgenommen.

CobbleDB erwartet, dass Aktualisierungen nach Partitionen in Batches zusammengefasst werden. Pillar-Exporte werden von einem zustandslosen Dienst namens Lorry in dieses Format konvertiert. Seine einzige Aufgabe besteht darin, Exportdatensätze aus einer persistenten, partitionsausgerichteten Warteschlange zu lesen, sie in Batch-Dateien pro Partition anzusammeln und an CobbleDB zu übergeben.

Zwischen Lorry und Cobble trennt ein zusätzliches Protokoll die Datenebene (S3) von der Steuerungsebene. Lorry schreibt den Batch unter einem eindeutigen Bezeichner in S3 und registriert den Batch innerhalb von Cobble. Cobble-Partitionsreplikate fragen unabhängig voneinander den Batch oder die Control-Plane-API nach dem nächsten einzulesenden S3-Batch ab und wenden Batch-Aktualisierungen asynchron in chronologischer Reihenfolge an. Dies bedeutet, dass ein langsamer oder sich erholender Knoten in seinem eigenen Tempo aufholen kann, anstatt den Rest des Systems zu verlangsamen.

Pillar veröffentlicht dauerhafte Exporte, und Lorry schreibt partitionierte Batches nach S3 und registriert sie bei der Control Plane. Jede CobbleDB-Replik entdeckt und ingestiert ihre Batches unabhängig in chronologischer Reihenfolge.

Der neue Pfad trennt die unmittelbare Verarbeitungstransaktion von der Hot-Store-Ingetion und bietet einen wiederholbaren Pfad für inkrementelle Aktualisierungen und vollständige Rebuilds. Diese Arbeitsteilung stellt sicher, dass Aktualisierungen des dauerhaften Dokumentstatus die Latenz des Lesepfads nicht beeinträchtigen.

Validierung

5x schneller

Nach der Migration des Hot-Store sank die mittlere Latenz für Batch-Lesevorgänge von 31,4 ms auf 5,60 ms. Der p90 sank von 56,7 ms auf 9,77 ms und der p99 sank von 123 ms auf 24,2 ms. Die Verhältnisse reichen von 5,08× bis 5,80×, wobei sowohl bei typischen Anfragen als auch im Tail-Bereich Verbesserungen zu verzeichnen sind.

Produktions-Batch-Lese-Latenz vor und nach der Migration: P50 sank von 31,4 auf 5,60 ms, P90 von 56,7 auf 9,77 ms und P99 von 123 auf 24,2 ms. Dies sind Lese-Latenzen des Hot Stores aus Live-Traffic zu unterschiedlichen Zeitpunkten, nicht abgeglichene Anfragen oder End-to-End-Antwortlatenz.

Diese Zahlen stammen von Batch-Lesevorgängen, bei denen jede Abfrage etwa 10-15 Schlüssel anfordert. Die durchschnittliche Nutzdatengröße beträgt 50 KB. Beide Systeme wurden mit etwa 200.000 rps abgefragt, obwohl wir auch Lasttests bis zu 500.000 rps ohne Leistungseinbußen durchgeführt haben.

Um den Beitrag von CobbleDB zu isolieren, wurden alle anderen Aspekte des Setups konstant gehalten. Dies bleibt jedoch ein beobachtender Vorher-Nachher-Vergleich; die Systeme dienten zu unterschiedlichen Zeiten dem Live-Produktionsverkehr und nicht denselben Anfragen unter kontrollierten Bedingungen. Daher haben wir auch die Leistung beider Systeme anhand eines synthetischen Benchmarks gemessen. Um die in der Produktion auftretenden Anfragen zu approximieren, haben wir beide mit Batches von 10-15 Schlüsseln und Werten im Bereich von 100B bis 100 KiB abgefragt.

Synthetische Batch-Lese-Latenz für Batches mit 10 und 15 Schlüsseln über Wertgrößen von 100 B bis 100 KiB. Jeder Punkt ist der Mittelwert der run-level P50-, P90- oder P99-Latenz aus zwei Durchläufen pro Backend und Konfiguration.

DynamoDB tut sich in diesem Kontext schwer, da der verwaltete Lesepfad nicht für Batch-Lesevorgänge dieser Größe optimiert werden kann. Im Gegensatz dazu ist CobbleDB genau für dieses Szenario optimiert, da wir die Platzierung, das Caching und die Replikatssteuerung selbst verwalten. Es gruppiert Schlüssel nach Partition, führt Lesevorgänge parallel gegen Partitionsreplikate aus und kann langsame Lesevorgänge gegen andere Replikate absichern. Unsere Ergebnisse zeigen, dass sich diese Designentscheidungen in einer Produktionsumgebung in schnellere Lesevorgänge umsetzen.

20 % günstiger

CobbleDB ist zudem deutlich günstiger als DynamoDB. Zusätzlich zum Speicher berechnet DynamoDB auch jedes gelesene oder geschriebene Byte. Bei der Größe unseres Korpus und unseres Serving-Datenverkehrs machen diese nutzungsbasierten Gebühren einen wesentlichen Teil der Gesamtkosten aus. Kontinuierliche Aktualisierungen verbrauchen Schreibkapazität, während jeder Batch vorbereiteter Seiten, der für eine Abfrage abgerufen wird, Lesekapazität verbraucht.

Wir haben die Kosteneinsparungen auf Basis interner Schätzungen der Speichergröße, der Write Capacity Units und Read Capacity Units berechnet. In jeder Verpflichtungsstufe ist CobbleDB mindestens 20 % günstiger als DynamoDB. Die tatsächlichen Einsparungen sind wahrscheinlich sogar noch höher, da diese Ergebnisse die potenziellen Backup-Einsparungen durch Komprimierung nicht berücksichtigen.

Wie uns ein Schwarm von KI-Agenten beim Bau hilft

Der Bau von CobbleDB und der umliegenden Infrastruktur war keineswegs trivial. Allein die Kerndatenbank von CobbleDB umfasst rund 40.000 Zeilen Rust, und die Migration erforderte außerdem umfangreiche Arbeiten zur Überprüfung von Korrektheit und Fehlerverhalten im Produktionsmaßstab, zur Integration von CobbleDB in sowohl Verarbeitungs- als auch Serving-Pfade sowie zum sicheren Übergang des Live-Verkehrs.

Wir haben diesen Aufwand mithilfe eines internen Systems beschleunigt, das einen Schwarm von Programmieragenten betreibt. Das System ist auf hohe Persistenz ausgelegt; es bewahrt die Projektziele, aktiven Risiken, die Repository-Historie und frühere Entscheidungen sitzungsübergreifend auf.

Wir haben dem CobbleDB-Projekt eine Gruppe solcher proaktiver, rund um die Uhr aktiver Agenten zugewiesen. Ihre erste Aufgabe bestand darin, ein präzises Modell der bereits laufenden Arbeiten zu erstellen. Sie prüften den Projekt-Thread und aktive Threads, verknüpften ausstehende Punkte mit ihren pull requests und Verantwortlichen und erstellten einen aktuellen Statusbericht zum Systemzustand und zu den nächsten Schritten. Von dort aus agierten sie in verschiedenen Bereichen der Engineering-Schleife:

  • Code- und Infrastrukturänderungen wurden überprüft.
  • Es wurden Blocker aufgedeckt, die leicht zu übersehen waren, darunter unsichere Wiederherstellungsannahmen, veraltete Build-Referenzen und Konfigurationen, die zur Laufzeit fehlschlagen würden.
  • Erstellte zielgerichtete Korrekturen, Tests, Überwachungsänderungen und Betriebsdokumentationen.
  • Verfolgte CI-, Review- und Infrastruktur-Gates, während Produktionsaktionen explizit in menschlicher Hand blieben.
  • Überwachte Rollouts sowie Backup- oder Wiederherstellungsübungen verglichen dann den beobachteten Systemstatus mit den Erfolgskriterien des Rollouts.
  • Pflegte Projektberichte, damit Ingenieure sehen konnten, was ausgeliefert wurde, was blockiert blieb und wo weiterhin menschliches Urteilsvermögen erforderlich war.

Ingenieure legten weiterhin die Architektur fest, überprüften folgenschwere Änderungen und autorisierten Produktionsabläufe. Die Agenten übernahmen einen Großteil der kontinuierlichen Inspektion und Nachbereitung zwischen diesen Entscheidungen. Dies ermöglichte es einem kleinen Team, das Projekt effizient voranzutreiben – die CobbleDB-Kerninfrastruktur wurde in zwei Monaten von zwei menschlichen Ingenieuren und Hunderten von KI-Agenten erbaut.

Fazit

CobbleDB ist keine universelle Datenbank; sie ist speziell auf unsere spezifischen Workloads ausgerichtet – wiederholte Batch-Lesevorgänge vorbereiteter Seiteneinträge, gespeist durch eine asynchrone Verarbeitungspipeline. Durch die Trennung von dauerhaftem Status, Aktualisierungsbereitstellung und Abfragezeit-Serving können wir jede Verantwortung unabhängig optimieren, ohne eine einzige Datebank-Schnittstelle zu zwingen, all dies unter einen Hut zu bringen, oder Wartungsarbeiten auf den Live-Abfragepfad zu verlagern. Die Migration zu CobbleDB hat sowohl die mittlere Latenz als auch die Tail-Latenz erheblich verbessert und gleichzeitig die Infrastrukturkosten gesenkt.

Indem wir weiterhin KI-native Infrastruktur intern entwickeln, können wir Leistung und Kosten auf eine Weise optimieren, die bei einer Bindung an Implementierungen und Preismodelle von Drittanbietern nicht möglich ist. Leistungsstarke KI-Agenten verschieben das Kalkül noch weiter zugunsten des Eigenbaus gegenüber dem Zukauf.

Wir planen, CobbleDB bald als Open-Source zu veröffentlichen, damit andere Teams, die KI-native Suchsysteme entwickeln, von derselben Speicherebene profitieren können, die die Suche in unseren Produkten schneller und kostengünstiger macht.

Zitat

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