CobbleDB: Reconstrucción del almacenamiento de búsqueda de IA

Una arquitectura desacoplada con estado duradero separado, actualizaciones por lotes y recuperación en el momento de la consulta.

AutoresPerplexity Engineering

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 representación vectorial (embeddings) y motores de inferencia para potenciar 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 del cual el servicio lee.

Las bases de datos administradas como DynamoDB proporcionaron un almacenamiento confiable, pero teníamos un control limitado sobre cómo se almacenaban los datos y se servían las lecturas, y los precios basados en el uso crecieron 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 activas.

Con la ayuda de un nuevo enjambre interno de agentes de codificación, reconstruimos esta capa alrededor de CobbleDB, un almacenamiento en caliente de clave-valor, respaldado por Pillar para el estado duradero de 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 sin procesar en contenido listo para modelos, nuestra canalización de procesamiento limpia cada página, la divide en pasajes semánticamente coherentes, calcula sus representaciones vectoriales y almacena los pasajes y las representaciones vectoriales 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 las modificaciones en los métodos de fragmentación o en los modelos de representación vectorial pueden requerir el reprocesamiento de grandes porciones 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 representaciones vectoriales (embeddings) para la clasificación y 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 requieren diferentes diseños. 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 precomputados, cachés activas y lecturas rápidas por lotes con un trabajo mínimo en el momento de la consulta. El 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 cortas las bases de datos administradas

DynamoDB nos proporcionó un almacenamiento altamente disponible sin la carga de operar una base de datos distribuida. Una tabla de páginas preparadas dedicada contenía pasajes y representaciones vectoriales (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 creció, tres restricciones se volvieron importantes.

Primero, los precios basados en el uso cobran cada byte leído o escrito. Una sola solicitud de API de búsqueda contiene entre 100 y 120 claves correspondientes a las 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 promedio de elemento 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 de representación vectorial 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 administrado 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 contenía 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 administra su propio almacenamiento y el comportamiento de las réplicas detrás de la API. Podíamos configurar el servicio, pero no podíamos ajustar su ubicación interna, los 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 convertía en una ola de actualizaciones individuales en el almacenamiento en caliente, y el procesamiento tenía que adaptarse a los límites de rendimiento y al comportamiento de reintento del almacenamiento en caliente. No podíamos ejecutar MapReduce en toda la base de datos en 1 o 2 días para probar un nuevo método de fragmentación (chunking), cambiar nuestro modelo de embeddings o agregar un nuevo campo. No había un flujo independiente de actualizaciones de páginas guardadas que un servicio separado pudiera agrupar 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 de los documentos y la entrega de actualizaciones del almacenamiento para 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.

Antes y después de la migración de almacenamiento. Pillar y Lorry separan el estado duradero del documento y la entrega de actualizaciones de las lecturas en tiempo de consulta servidas por CobbleDB.

CobbleDB: la ruta de lectura ajustable

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 troceadas o hasheadas) y los valores son la representación procesada de la página (sus pasajes previamente fragmentados y las representaciones vectoriales de vectores por fragmento).

El cambio importante es nuestro control sobre la configuración del almacenamiento.

Los datos se particionan y se dividen entre los nodos de datos. Este formato distribuido garantiza que la base de datos pueda escalar horizontalmente para dar cabida a 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 particiones. El nodo almacena sus datos utilizando RocksDB, un motor de almacenamiento de clave-valor incrustado de código abierto muy adecuado para cargas de trabajo con un uso intensivo de lectura alimentadas por la 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 ajustar el equilibrio entre memoria y disco para la carga de trabajo observada, en lugar de aceptar la política de caché interna de un servicio administrado.

Un enrutador de consultas sin estado aplica hash a las claves para asignarlas a las particiones y enruta las solicitudes a los nodos en paralelo. La arquitectura también incluye un servicio de metadatos respaldado por PostgreSQL y un almacenamiento de objetos duradero.

Este diseño es compatible con 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 de páginas relevantes candidatas, 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 dirigen 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 con lentitud, el sistema también puede consultar otra réplica de partición en un nodo diferente, lo que mejora la latencia de cola.

También podemos evitar implementar 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 para transacciones, pero nuestro almacenamiento en caliente no requiere transacciones ni réplicas sincronizadas. Es aceptable que haya un breve lapso entre el momento en que se escribe un documento y cuando 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.

Un enrutador sin estado agrupa las claves de página por partición y lee réplicas en paralelo. Las lecturas lentas se pueden enviar a otra réplica; la primera respuesta de cada partición se combina en el resultado.

Pillar y Lorry: la ruta de escritura duradera

Una capa de almacenamiento separada realiza un seguimiento de todo lo que rastreamos y procesamos, fusionando las actualizaciones en el estado de los documentos versionados y publicando las representaciones necesarias en CobbleDB y en el índice de búsqueda. Para este propósito, construimos Pillar, nuestra capa de publicación y estado de documentos.

Pillar está respaldado por YTsaurus. Almacena componentes de páginas (como metadatos, fragmentos y representaciones vectoriales) en familias de tablas de YTsaurus separadas. También realiza un seguimiento de las versiones de los fragmentos y las representaciones vectoriales, 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 componentes posteriores como CobbleDB. CobbleDB utiliza almacenamiento NVMe más costoso, por lo que solo exportamos los documentos que realmente queremos usar 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 poner en cola 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 contabilizada y poner en cola 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 los registros de exportación de una cola persistente alineada por partición, acumularlos en archivos por 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 la API del plano de control o de datos para obtener el siguiente lote de S3 que deben ingerir y aplican de forma asíncrona las actualizaciones por lotes en orden cronológico. Esto significa que un nodo lento o en recuperación puede ponerse al día a su propio ritmo, en lugar de ralentizar al resto del sistema.

Pillar publica exportaciones duraderas, y Lorry escribe lotes particionados a S3 y los registra con el plano de control. Cada réplica de CobbleDB detecta e ingiere de forma independiente sus lotes en orden cronológico.

La nueva ruta separa la transacción de procesamiento inmediato de la ingesta en el almacenamiento en caliente y proporciona una ruta repetible 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 promedio de lectura por lotes disminuyó de 31.4 ms a 5.60 ms. El p90 bajó de 56.7 ms a 9.77 ms, y el p99 disminuyó de 123 ms a 24.2 ms. Las proporciones varían de 5.08× a 5.80×, con mejoras tanto en las solicitudes típicas como en la cola.

Latencia de lectura por lotes en producción antes y después de la migración: P50 bajó de 31.4 a 5.60 ms, P90 de 56.7 a 9.77 ms y P99 de 123 a 24.2 ms. Estas son latencias de lectura del almacén activo a partir del tráfico en vivo en distintos momentos, no solicitudes emparejadas ni latencia de respuesta de extremo a extremo.

Estos números se derivan de lecturas por lotes, donde cada consulta solicita aproximadamente entre 10 y 15 claves. El tamaño promedio del elemento de carga útil es de 50 KB. Se consultó a ambos sistemas a unas 200k rps, aunque también realizamos pruebas de carga de hasta 500k 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 real 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 iban de 100B a 100 KiB.

Latencia sintética de lectura por lotes para lotes de 10 y 15 claves en tamaños de valor de 100 B a 100 KiB. Cada punto es el promedio de la latencia P50, P90 o P99 a nivel de ejecución de dos ejecuciones por backend y configuración.

DynamoDB tiene dificultades en este contexto porque su ruta de lectura administrada no se puede ajustar para lecturas por lotes de este tamaño. En cambio, CobbleDB está optimizado exactamente para este escenario, ya que nosotros gestionamos la ubicación, el almacenamiento en caché y los controles de réplica. Agrupa las claves por partición, ejecuta lecturas en paralelo frente a las réplicas de partición y puede compensar las lecturas lentas con otras réplicas. Nuestros resultados demuestran que estas decisiones de diseño se traducen en lecturas más rápidas en un entorno de producción.

20% más económico

CobbleDB también es significativamente más económico que DynamoDB. Además del almacenamiento, DynamoDB también cobra por cada byte leído o escrito. A la escala de nuestro corpus y 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 de 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 económico que DynamoDB. Es probable que el ahorro real sea aún mayor, ya que estos resultados no tienen en cuenta el probable ahorro en respaldos debido a la compresión.

Cómo un enjambre de agentes de IA nos ayuda a construirlo

Construir CobbleDB y la infraestructura circundante no fue para nada trivial. La base de datos central de CobbleDB por sí sola tiene aproximadamente 40 000 líneas de código en Rust, y la migración también requirió un extenso trabajo 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 activo.

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 está 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 de 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 monitoreo y documentación operativa.
  • Se realizó un seguimiento de la integración continua (CI), la revisión y las puertas de infraestructura, manteniendo las acciones de producción con supervisión humana explícita.
  • Se realizaron despliegues controlados monitorizados y ejercicios de respaldo o restauración, y luego se comparó el estado observado del sistema con los criterios de éxito del despliegue.
  • Se mantuvieron informes de proyectos actualizados para que los ingenieros pudieran ver qué se había lanzado, qué seguía bloqueado y dónde aún se requería criterio humano.

Los ingenieros aún establecen la arquitectura, revisan los cambios importantes y autorizan 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ó que un equipo pequeño impulsara 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 particular de lecturas por lotes repetidas de registros de páginas preparadas, alimentada por una canalización (pipeline) de procesamiento asíncrono. 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 sola interfaz de base de datos a adaptarse a todas ellas ni trasladar el trabajo de mantenimiento a la ruta de consulta activa. Migrar a CobbleDB mejoró significativamente tanto la latencia mediana como la de cola, al tiempo que redujo los costos de infraestructura.

A medida que continuamos desarrollando infraestructura nativa de IA internamente, podemos optimizar el rendimiento y el costo de maneras que no son posibles cuando estamos atados a implementaciones y modelos de precios de terceros. Los agentes de IA potentes cambian aún más el cálculo a favor de construir en lugar de comprar.

Planeamos abrir el código fuente de CobbleDB pronto, 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

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