CobbleDB: Reconstrucción del almacenamiento de búsqueda de IA para menor latencia y coste
Una arquitectura desacoplada con estado duradero separado, actualizaciones por lotes y recuperación en el momento de la consulta.
La búsqueda nativa de IA impone diferentes requisitos sobre cómo se debe procesar, almacenar y servir el contenido web. Para satisfacer esas demandas a escala, hemos estado reconstruyendo toda la pila desde cero para mejorar el rendimiento y reducir los costos.
Construimos nuestra propia infraestructura de rastreo, indexación y clasificación, modelos de incrustación y motores de inferencia para impulsar un procesamiento eficiente y un servicio rápido. Ahora estamos extendiendo ese enfoque al almacenamiento, el tejido conectivo en el que el procesamiento escribe y desde el cual el servicio lee.
Las bases de datos gestionadas como DynamoDB proporcionaban un almacenamiento confiable, pero teníamos un control limitado sobre cómo se almacenaban los datos y cómo se servían las lecturas, y los precios basados en el uso crecían con el tamaño de nuestro índice y el volumen de consultas. Por separado, nuestra canalización anterior acoplaba el procesamiento de documentos directamente a la base de datos de servicio, lo que dificultaba la gestión de las actualizaciones de forma independiente de las lecturas en vivo.
Con la ayuda de un nuevo enjambre interno de agentes de codificación, reconstruimos esta capa en torno a CobbleDB, un almacenamiento en caliente de clave-valor, respaldado por Pillar para el estado duradero de los documentos y la publicación, y Lorry para actualizaciones por lotes. El rediseño separa el procesamiento de documentos de la ingesta en el almacenamiento en caliente y nos permite ajustar el servicio a nuestra carga de trabajo. Las mediciones de producción de antes y después muestran una latencia de lectura por lotes aproximadamente 5 veces menor, y nuestro modelo de costo interno estima al menos un 20 % de ahorros en relación con DynamoDB.
Infraestructura de almacenamiento para la búsqueda nativa de IA
Una pila de búsqueda clásica está diseñada para un lector humano, no para un modelo de lenguaje. Esa diferencia cambia la granularidad a la que el sistema necesita preparar, almacenar y servir contenido web.
Para convertir HTML bruto en contenido listo para modelos, nuestra canalización de procesamiento limpia cada página, la divide en pasajes semánticamente coherentes, calcula sus incrustaciones y almacena los pasajes y las incrustaciones juntos en la base de datos. En el momento de la consulta, la canalización de servicio selecciona el texto más relevante para la consulta y se lo pasa al modelo.
Dos flujos de trabajo distintos
Por lo tanto, la capa de almacenamiento debe admitir dos flujos de trabajo con diferentes demandas: ingerir actualizaciones de la canalización de procesamiento y servir lecturas de baja latencia.
En el lado de la escritura, las páginas nuevas llegan continuamente, las páginas existentes cambian y los cambios en los métodos de fragmentación o los modelos de incrustación pueden requerir el reprocesamiento de gran parte del corpus. La capa de almacenamiento debe incorporar estas actualizaciones en el estado duradero del documento, realizar un seguimiento de las versiones actuales, publicar las representaciones correctas en los sistemas posteriores y admitir grandes cargas de datos históricos (backfills) sin repetir toda la canalización desde el rastreo original.
Por el contrario, las lecturas en el momento de la consulta solicitan lotes de claves de página y recuperan el texto del pasaje correspondiente y las incrustaciones para la clasificación y la selección de pasajes. Estas lecturas se encuentran en el camino hacia la respuesta final, por lo que deben completarse rápidamente.
Estas cargas de trabajo exigen diseños diferentes. El procesamiento se beneficia de las colas duraderas y de la capacidad de entregar actualizaciones de manera eficiente, reintentar fallas y actualizar registros existentes a escala. El servicio se beneficia de registros precalculados, cachés activas y lecturas rápidas por lotes con un trabajo mínimo en el momento de la consulta. Un acoplamiento estrecho entre ambos puede hacer que el trabajo de recuperación y las grandes actualizaciones compitan con las lecturas sensibles a la latencia.
Dónde se quedan corta la base de datos gestionada
DynamoDB nos proporcionó un almacenamiento altamente disponible sin la carga de operar una base de datos distribuida. Una tabla dedicada de páginas preparadas contenía pasajes e incrustaciones (embeddings) para lecturas en el momento de la consulta, mientras que el contenido completo de la página y los metadatos se almacenaban por separado. Sin embargo, a medida que nuestra carga de trabajo crecía, tres limitaciones se volvieron importantes.
Primero, los precios basados en el uso cobran cada byte leído o escrito. Una sola solicitud de la API de búsqueda contiene entre 100 y 120 claves correspondientes a páginas que necesitamos recuperar. El proceso de recuperación de datos divide una solicitud en lotes más pequeños de 10 a 20 claves de página, con un tamaño de elemento promedio de aproximadamente 50 KB. La lectura repetida de esos registros consumía una capacidad sustancial, mientras que los rastreos continuos y los cambios en los modelos de fragmentación o incrustación generaban más escrituras. La economía también dependía del tamaño y la frecuencia de los registros, no solo del número de llamadas a la API.
Segundo, el servicio gestionado carece de la flexibilidad que necesitamos para la optimización del rendimiento. La operación que necesitábamos era específica: dado un lote de identificadores de página, devolver sus contenidos preparados rápidamente. Queríamos tener el control sobre qué máquina albergaba una partición, cuánta memoria se dedicaba a su caché y qué réplica gestionaba una solicitud. Esos controles importan porque una lectura no almacenada en caché, un salto entre zonas o una réplica lenta pueden retrasar todo el lote. DynamoDB gestiona su propio almacenamiento y el comportamiento de las réplicas detrás de la API. Podríamos configurar el servicio, pero no podíamos ajustar su ubicación interna, las cachés del motor de almacenamiento o la política de selección de réplicas en función de nuestra carga de trabajo.
Tercero, el procesamiento estaba vinculado directamente a las escrituras en el almacenamiento en caliente. Nuestra canalización de procesamiento anterior escribía cada página preparada directamente en DynamoDB. Por lo tanto, un gran trabajo de reprocesamiento se convirtió en una ola de actualizaciones individuales del almacenamiento en caliente, y el procesamiento tuvo que adaptarse a los límites de rendimiento y al comportamiento de reintento del almacenamiento en caliente. No pudimos ejecutar MapReduce en toda la base de datos en 1-2 días para probar un nuevo método de fragmentación, cambiar nuestro modelo de incrustación o agregar un nuevo campo. No había un flujo independiente de actualizaciones de páginas guardadas que un servicio separado pudiera procesar por lotes y entregar al almacenamiento en caliente a su propio ritmo.
Abordar estas limitaciones requirió más que reemplazar la base de datos. Necesitábamos separar el estado duradero del documento y la entrega de actualizaciones del almacenamiento en el momento de la consulta, y luego optimizar el almacenamiento en caliente específicamente para lecturas rápidas por lotes.
Separar el estado del documento, la entrega de actualizaciones y el servicio
La nueva arquitectura asigna cada responsabilidad a un componente diferente. Pillar mantiene un estado de documento duradero y decide qué publicar. Lorry convierte esas exportaciones en lotes particionados. CobbleDB ingiere los lotes y sirve registros preparados por clave de página.
CobbleDB: la ruta de lectura optimizable
CobbleDB es nuestro almacenamiento en caliente optimizado para servir registros preparados en el momento de la consulta. Es un almacén de clave-valor distribuido: las claves son identificadores de página (URL hash) y los valores son la representación procesada de la página (sus pasajes fragmentados previamente y las incrustaciones vectoriales por fragmento).
El cambio importante es nuestro control sobre la configuración de almacenamiento.
Los datos se particionan y se dividen entre nodos de datos. Este formato distribuido garantiza que la base de datos pueda escalar horizontalmente para acomodar un corpus enorme de páginas procesadas. Cada partición tiene tres réplicas en tres nodos diferentes; si una copia deja de estar disponible, las demás pueden continuar sirviendo lecturas.
A cada nodo de datos se le asigna un subconjunto de las particiones. El nodo almacena sus datos utilizando RocksDB, un motor de almacenamiento de clave-valor integrado y de código abierto muy adecuado para cargas de trabajo con uso intensivo de lectura alimentadas por ingesta por lotes.
Los datos almacenados en caché se pueden servir desde la memoria, mientras que las lecturas no almacenadas en caché utilizan almacenamiento NVMe local. Esto nos proporciona una forma directa de equilibrar la memoria y el disco para la carga de trabajo observada, en lugar de aceptar la política de caché interna de un servicio gestionado.
Un enrutador de consultas sin estado aplica hash a las claves para asignarlas a particiones y enruta las solicitudes a los nodos en paralelo. La arquitectura también incluye un servicio de metadatos respaldado por PostgreSQL y almacenamiento de objetos duradero.
Este diseño admite los patrones de lectura que exige nuestra carga de trabajo. Después de que una consulta pasa por la recuperación y la clasificación para obtener un conjunto candidato de páginas relevantes, se consulta el almacenamiento en caliente de forma masiva para obtener el contenido de esas páginas. Estas solicitudes de lectura llegan al enrutador y se enrutan a un nodo de datos correspondiente. El enrutador prioriza el envío de solicitudes a un nodo en la misma zona de disponibilidad para minimizar la latencia entre zonas, si es posible. Cada nodo de datos puede recuperar el contenido de la página utilizando MultiGet, una API de lectura por lotes de RocksDB diseñada para recuperar múltiples claves simultáneamente con baja latencia. Si un nodo responde lentamente, el sistema también puede consultar otra réplica de partición en un nodo diferente, mejorando la latencia de cola.
También podemos evitar la implementación de funciones comunes que nuestro flujo de trabajo no requiere. Por ejemplo, la mayoría de las bases de datos ofrecen garantías de consistencia y soporte de transacciones, pero nuestro almacenamiento en caliente no requiere transacciones ni réplicas sincronizadas. Es aceptable que haya un breve intervalo entre el momento en que se escribe un documento y el momento en que está disponible para lecturas, y también está bien que una réplica ingiera datos más rápido que otra. Omitir funciones que no necesitamos reduce la sobrecarga y el costo de nuestro sistema.
Pillar y Lorry: la ruta de escritura duradera
Una capa de almacenamiento independiente realiza un seguimiento de todo lo que rastreamos y procesamos, fusionando las actualizaciones en un estado de documento versionado y publicando las representaciones necesarias en CobbleDB y en el índice de búsqueda. Para este propósito, construimos Pillar, nuestra capa de estado de documentos y publicación.
Pillar está respaldado por YTsaurus. Almacena componentes de páginas (como metadatos, fragmentos e incrustaciones) en familias de tablas de YTsaurus separadas. También realiza un seguimiento de las versiones de los fragmentos y las incrustaciones, lo que permite que múltiples representaciones coexistan sin sobrescribirse entre sí para que los modelos subyacentes puedan cambiar. Dado que Pillar y YTsaurus utilizan almacenamiento HDD económico, podemos almacenar muchos más documentos de los que almacenábamos anteriormente en DynamoDB.
Pillar realiza un seguimiento de subconjuntos, o grupos de páginas definidos por políticas, como páginas recientes o páginas de alto valor. La pertenencia a un subconjunto determina qué actualizaciones deben transmitirse a los componentes posteriores, como CobbleDB. CobbleDB utiliza almacenamiento NVMe más costoso, por lo que solo exportamos los documentos que realmente queremos utilizar en los resultados de búsqueda. Cuando se actualiza el contenido de una página, o si se agrega o elimina de un subconjunto, Pillar es responsable de encolar el trabajo de exportación correspondiente.
Las transacciones atómicas de YTsaurus cubren el estado, las intenciones de exportación y la entrada consumida para garantizar la corrección. Para que una transacción se complete con éxito, el sistema debe actualizar el estado duradero, marcar la entrada como procesada y encolar todas las exportaciones requeridas; si la transacción se anula, no se realiza ningún cambio en ninguna capa.
CobbleDB espera que las actualizaciones se agrupen por lotes según la partición. Las exportaciones de Pillar se convierten a este formato mediante un servicio sin estado que llamamos Lorry. Su única tarea es leer registros de exportación de una cola persistente alineada con particiones, acumularlos en archivos de lotes por partición y entregarlos a CobbleDB.
Entre Lorry y Cobble, un protocolo adicional separa el plano de datos (S3) del plano de control. Lorry escribe el lote en S3 bajo un identificador único y registra el lote dentro de Cobble. Las réplicas de partición de Cobble consultan de forma independiente el lote o la API del plano de control para obtener el siguiente lote de S3 que se va a ingerir y aplican de forma asíncrona las actualizaciones por lotes en orden cronológico. Esto significa que un nodo lento o en proceso de recuperación puede ponerse al día a su propio ritmo, en lugar de ralentizar al resto del sistema.
La nueva ruta separa la transacción de procesamiento inmediato de la ingesta en el almacenamiento en caliente y proporciona una ruta reproducible para actualizaciones incrementales y reconstrucciones masivas. Esta división del trabajo garantiza que las actualizaciones del estado duradero del documento no interfieran con la latencia de la ruta de lectura.
Validación
5x más rápido
Tras migrar el almacenamiento en caliente (hot store), la latencia media de lectura por lotes cayó de 31,4 ms a 5,60 ms. El percentil 90 (p90) bajó de 56,7 ms a 9,77 ms, y el percentil 99 (p99) cayó de 123 ms a 24,2 ms. Las proporciones oscilan entre 5,08× y 5,80×, con mejoras tanto en las solicitudes típicas como en la cola.
Estos números se derivan de lecturas por lotes, donde cada consulta solicita aproximadamente 10-15 claves. El tamaño medio del elemento de carga útil es de 50 KB. Ambos sistemas fueron consultados a unas 200 mil rps (solicitudes por segundo), aunque también ejecutamos pruebas de carga de hasta 500 mil rps sin degradación.
Para aislar la contribución de CobbleDB, todos los demás aspectos de la configuración se mantuvieron constantes. Sin embargo, esto sigue siendo una comparación observacional de antes y después; los sistemas sirvieron tráfico de producción en vivo en diferentes momentos en lugar de las mismas solicitudes bajo condiciones controladas. Por lo tanto, también medimos el rendimiento de ambos sistemas en una prueba de rendimiento sintética. Para aproximar las solicitudes vistas en producción, consultamos a ambos con lotes de 10-15 claves y valores que van desde 100B hasta 100 KiB.
DynamoDB tiene dificultades en este contexto porque su ruta de lectura gestionada no se puede optimizar para lecturas por lotes de este tamaño. Por el contrario, CobbleDB está optimizado exactamente para este escenario, ya que gestionamos la ubicación, el almacenamiento en caché y los controles de réplica. Agrupa las claves por partición, ejecuta lecturas en paralelo contra las réplicas de partición y puede mitigar las lecturas lentas frente a otras réplicas. Nuestros resultados demuestran que estas opciones de diseño se traducen en lecturas más rápidas en un entorno de producción.
20% más barato
CobbleDB también es significativamente más barato que DynamoDB. Además del almacenamiento, DynamoDB también cobra por cada byte leído o escrito. A la escala de nuestro corpus y nuestro tráfico de servicio, estos cargos basados en el uso se convierten en una parte sustancial del costo total. Las actualizaciones continuas consumen capacidad de escritura, mientras que cada lote de páginas preparadas recuperadas para una consulta consume capacidad de lectura.
Calculamos el ahorro de costos basándonos en estimaciones internas del tamaño del almacenamiento, las unidades de capacidad de escritura y las unidades de capacidad de lectura. En cada nivel de compromiso, CobbleDB es al menos un 20 % más barato que DynamoDB. Es probable que los ahorros reales sean aún mayores, ya que estos resultados no tienen en cuenta los probables ahorros en copias de seguridad derivados de la compresión.
Cómo un enjambre de agentes de IA nos ayuda a construirlo
Construir CobbleDB y la infraestructura circundante no fue tarea fácil. La base de datos central de CobbleDB por sí sola tiene aproximadamente 40 000 líneas de Rust, y la migración también requirió un trabajo extenso para verificar la corrección y el comportamiento ante fallas a escala de producción, integrar CobbleDB con las rutas de procesamiento y servicio, y realizar una transición segura del tráfico en vivo.
Aceleramos este esfuerzo utilizando un sistema interno que opera un enjambre de agentes de codificación. El sistema está diseñado para ser altamente persistente; retiene los objetivos del proyecto, los riesgos activos, el historial del repositorio y las decisiones anteriores a lo largo de las sesiones.
Asignamos un colectivo de dichos agentes proactivos y siempre activos al proyecto CobbleDB. Su primera tarea fue construir un modelo preciso del trabajo que ya estaba en marcha. Auditaron el canal del proyecto y los hilos activos, conectaron los elementos pendientes con sus pull requests y propietarios, y produjeron un informe actualizado sobre la salud del sistema y las próximas acciones. A partir de ahí, operaron en varias partes del ciclo de ingeniería:
- Se revisaron los cambios de código e infraestructura.
- Se sacaron a la luz bloqueadores que eran fáciles de pasar por alto, incluidas suposiciones de restauración inseguras, referencias de compilación obsoletas y configuraciones que fallarían en tiempo de ejecución.
- Se prepararon correcciones específicas, pruebas, cambios de monitorización y documentación operacional.
- Se realizó un seguimiento de la integración continua (CI), las revisiones y los controles de infraestructura, manteniendo al mismo tiempo las acciones de producción explícitamente bajo la responsabilidad humana.
- Se realizaron despliegues monitorizados y ejercicios de copia de seguridad o restauración, y luego se comparó el estado observado del sistema con los criterios de éxito del despliegue.
- Se mantuvieron informes de estado del proyecto para que los ingenieros pudieran ver qué se había enviado, qué seguía bloqueado y dónde aún se requería juicio humano.
Los ingenieros siguen estableciendo la arquitectura, revisando los cambios trascendentales y autorizando las operaciones de producción. Los agentes se encargaron de gran parte de la inspección continua y el seguimiento entre esas decisiones. Esto permitió a un equipo pequeño impulsar el proyecto de manera eficiente: la infraestructura central de CobbleDB fue construida por dos ingenieros humanos y cientos de agentes de IA en dos meses.
Conclusión
CobbleDB no es una base de datos de propósito general; está diseñada específicamente para nuestra carga de trabajo específica de lecturas por lotes repetidas de registros de páginas preparadas, alimentadas por una canalización de procesamiento asíncrona. Al separar el estado duradero, la entrega de actualizaciones y el servicio en el momento de la consulta, podemos optimizar cada responsabilidad de forma independiente sin obligar a una única interfaz de base de datos a acomodarlas todas ni trasladar el trabajo de mantenimiento a la ruta de consulta en vivo. Migrar a CobbleDB mejoró significativamente tanto la latencia mediana como la de la cola, al tiempo que redujo los costos de infraestructura.
A medida que continuamos desarrollando infraestructura nativa de IA de forma interna, podemos optimizar el rendimiento y el costo de maneras que no son posibles cuando estamos atados a implementaciones de terceros y modelos de precios. Los potentes agentes de IA desplazan el cálculo aún más a favor de construir en lugar de comprar.
Tenemos la intención de abrir el código fuente de CobbleDB próximamente, para que otros equipos que construyen sistemas de búsqueda nativos de IA puedan beneficiarse de la misma capa de almacenamiento que hace que la búsqueda sea más rápida y económica dentro de nuestros productos.
Citación
@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}
}