CobbleDB: Reconstruindo o armazenamento de busca de IA para menor latência e custo
Uma arquitetura desacoplada com estado durável separado, atualizações em lote e recuperação em tempo de consulta.
A busca nativa de IA impõe requisitos diferentes sobre como o conteúdo da web precisa ser processado, armazenado e servido. Para atender a essas demandas em escala, temos reconstruído toda a pilha do zero para melhorar o desempenho e reduzir custos.
Construímos nossa própria infraestrutura de rastreamento, indexação e ranqueamento, modelos de embedding e mecanismos de inferência para potencializar o processamento eficiente e o atendimento rápido. Agora estamos estendendo essa abordagem para o armazenamento, o tecido conjuntivo para o qual o processamento grava e do qual o atendimento lê.
Bancos de dados gerenciados, como o DynamoDB, forneciam armazenamento confiável, mas tínhamos controle limitado sobre como os dados eram armazenados e as leituras eram servidas, e os preços baseados no uso cresciam com o tamanho do nosso índice e o volume de consultas. Separadamente, nosso pipeline anterior acoplava o processamento de documentos diretamente ao banco de dados de atendimento, dificultando o gerenciamento de atualizações independentemente das leituras ativas.
Com a ajuda de um novo enxame de agentes de codificação internos, reconstruímos essa camada em torno do CobbleDB, um armazenamento ativo de chave-valor, suportado pelo Pillar para estado durável de documentos e publicação, e pelo Lorry para atualizações em lote. O redesenho separa o processamento de documentos da ingestão no armazenamento ativo e nos permite ajustar o atendimento à nossa carga de trabalho. As medições de antes e depois em produção mostram uma latência de leitura em lote cerca de 5× menor, e nosso modelo de custo interno estima uma economia de pelo menos 20% em relação ao DynamoDB.
Infraestrutura de armazenamento para busca nativa de IA
Uma pilha de busca clássica é construída para um leitor humano, não para um modelo de linguagem. Essa diferença altera a granularidade na qual o sistema precisa preparar, armazenar e servir conteúdo da web.
Para transformar HTML bruto em conteúdo pronto para modelos, nosso pipeline de processamento limpa cada página, divide-a em passagens semanticamente coerentes, calcula seus embeddings e armazena as passagens e os embeddings juntos no banco de dados. No momento da consulta, o pipeline de atendimento seleciona o texto mais relevante para a consulta e o passa para o modelo.
Dois fluxos de trabalho distintos
A camada de armazenamento deve, portanto, suportar dois fluxos de trabalho com demandas diferentes: ingerir atualizações do pipeline de processamento e servir leituras de baixa latência.
No lado da gravação, novas páginas chegam continuamente, páginas existentes mudam e alterações nos métodos de divisão em blocos ou nos modelos de embedding podem exigir o reprocessamento de grandes partes do corpus. A camada de armazenamento deve incorporar essas atualizações ao estado durável do documento, rastrear as versões atuais, publicar as representações corretas para sistemas a jusante e dar suporte a grandes preenchimentos retroativos sem repetir o pipeline completo a partir do rastreamento bruto.
Em contrapartida, as leituras no momento da consulta solicitam lotes de chaves de página e recuperam o texto da passagem correspondente e os embeddings para ranqueamento e seleção de passagens. Essas leituras estão no caminho para a resposta final, por isso precisam ser concluídas rapidamente.
Essas cargas de trabalho exigem designs diferentes. O processamento se beneficia de filas duráveis e da capacidade de entregar atualizações com eficiência, tentar novamente em caso de falha e atualizar registros existentes em escala. O atendimento se beneficia de registros pré-computados, caches aquecidos e leituras em lote rápidas com o mínimo de trabalho no momento da consulta. O acoplamento forte entre os dois pode fazer com que o trabalho de recuperação e grandes atualizações concorram com leituras sensíveis à latência.
Onde o banco de dados gerenciado fica aquém
O DynamoDB nos forneceu armazenamento altamente disponível sem o ônus de operar um banco de dados distribuído. Uma tabela dedicada de páginas preparadas continha passagens e embeddings para leituras no momento da consulta, enquanto o conteúdo completo da página e os metadados eram armazenados separadamente. No entanto, à medida que nossa carga de trabalho crescia, três restrições tornaram-se importantes.
Primeiro, a precificação baseada no uso cobra por cada byte lido ou gravado. Uma única solicitação de API de Busca contém de 100 a 120 chaves correspondentes às páginas que precisamos buscar. O processo de recuperação de dados divide uma solicitação em lotes menores de 10 a 20 chaves de página, com o tamanho médio do item em torno de 50 KB. A leitura repetida desses registros consumiu capacidade substancial, enquanto rastreamentos contínuos e alterações nos modelos de divisão em blocos ou embeddings geraram mais gravações. A economia também dependia do tamanho e da frequência dos registros, e não apenas do número de chamadas de API.
Segundo, o serviço gerenciado carece da flexibilidade de que precisamos para o ajuste de desempenho. A operação de que precisábamos era restrita: dado um lote de identificadores de página, retornar seu conteúdo preparado rapidamente. Queríamos controle sobre qual máquina mantinha uma partição, quanta memória era dedicada ao seu cache e qual réplica lidava com uma solicitação. Esses controles são importantes porque uma leitura não armazenada em cache, um salto entre zonas ou uma réplica lenta podem atrasar todo o lote. O DynamoDB gerencia seu próprio armazenamento e comportamento de réplica por trás da API. Podíamos configurar o serviço, mas não podíamos ajustar seu posicionamento interno, caches do mecanismo de armazenamento ou política de seleção de réplicas em torno da nossa carga de trabalho.
Terceiro, o processamento estava vinculado diretamente às gravações no armazenamento ativo. Nosso pipeline de processamento anterior gravava cada página preparada diretamente no DynamoDB. Um grande trabalho de reprocessamento tornava-se, portanto, uma onda de atualizações individuais no armazenamento ativo, e o processamento precisava acomodar os limites de taxa de transferência e o comportamento de nova tentativa do armazenamento ativo. Não conseguíamos executar o MapReduce em todo o banco de dados em 1 a 2 dias para testar um novo método de divisão em blocos, alterar nosso modelo de embedding ou adicionar um novo campo. Não havia um fluxo independente de atualizações de páginas salvas que um serviço separado pudesse agrupar e entregar ao armazenamento ativo em seu próprio ritmo.
Abordar essas restrições exigiu mais do que substituir o banco de dados. Precisábamos separar o estado durável do documento e a entrega de atualizações do armazenamento no momento da consulta, e então otimizar o armazenamento ativo especificamente para leituras em lote rápidas.
Separar estado do documento, entrega de atualizações e atendimento
A nova arquitetura atribui cada responsabilidade a um componente diferente. O Pillar mantém um estado de documento durável e decide o que publicar. O Lorry transforma essas exportações em lotes particionados. O CobbleDB ingere os lotes e atende registros preparados por chave de página.
CobbleDB: o caminho de leitura otimizado
O CobbleDB é nosso armazenamento ativo otimizado para servir registros preparados no momento da consulta. É um repositório de chave-valor distribuído: as chaves são identificadores de página (URLs com hash) e os valores são a representação processada da página (suas passagens divididas em blocos previamente e embeddings vetoriais por bloco).
A alteração importante é o nosso controle sobre a configuração de armazenamento.
Os dados são particionados e divididos entre nós de dados. Esse formato distribuído garante que o banco de dados possa escalar horizontalmente para acomodar um enorme corpus de páginas processadas. Cada partição tem três réplicas em três nós diferentes; se uma cópia ficar indisponível, as outras poderão continuar servindo leituras.
Cada nó de dados recebe um subconjunto das partições. O nó armazena seus dados usando o RocksDB, um mecanismo de armazenamento de chave-valor incorporado de código aberto, ideal para cargas de trabalho pesadas de leitura alimentadas por ingestão em lote.
Os dados em cache podem ser servidos a partir da memória, enquanto as leituras não armazenadas em cache usam armazenamento NVMe local. Isso nos dá uma maneira direta de ajustar o equilíbrio entre memória e disco para a carga de trabalho observada, em vez de aceitar a política de cache interna de um serviço gerenciado.
Um roteador de consulta sem estado aplica hash às chaves para partições e encaminha solicitações para nós em paralelo. A arquitetura também inclui um serviço de metadados apoiado por PostgreSQL e armazenamento de objetos durável.
Esse design suporta os padrões de leitura que nossa carga de trabalho exige. Após uma consulta passar pela recuperação e ranqueamento para obter um conjunto candidato de páginas relevantes, o armazenamento ativo é consultado em lote para obter o conteúdo dessas páginas. Essas solicitações de leitura chegam ao roteador e são encaminhadas para um nó de dados correspondente. O roteador prioriza o envio de solicitações para um nó na mesma zona de disponibilidade para minimizar a latência entre zonas, se possível. Cada nó de dados pode buscar o conteúdo da página usando o MultiGet, uma API de leitura em lote do RocksDB projetada para buscar várias chaves simultaneamente com baixa latência. Se um nó demorar para responder, o sistema também poderá consultar outra réplica de partição em um nó diferente, melhorando a latência da cauda.
Também podemos evitar a implementação de recursos comuns que nosso fluxo de trabalho não exige. Por exemplo, a maioria dos bancos de dados oferece garantias de consistência e suporte a transações, mas nosso armazenamento ativo não exige transações ou réplicas sincronizadas. É aceitável que haja uma pequena lacuna entre o momento em que um documento é gravado e quando ele está disponível para leituras, e também não há problema em uma réplica ingerir dados mais rápido do que outra. Omitir recursos de que não precisamos reduz a sobrecarga e o custo do nosso sistema.
Pillar e Lorry: o caminho de gravação durável
Uma camada de armazenamento separada monitora tudo o que rastreamos e processamos, mesclando atualizações em um estado de documento versionado e publicando as representações necessárias no CobbleDB e no índice de busca. Construímos o Pillar, nossa camada de publicação e estado de documento, para essa finalidade.
O Pillar é sustentado pelo YTsaurus. Ele armazena componentes de página (como metadados, blocos e embeddings) em famílias de tabelas do YTsaurus separadas. Ele também rastreia as versões dos blocos e embeddings, permitindo que várias representações coexistam sem sobrescrever umas às outras, para que os modelos subjacentes possam mudar. Como o Pillar e o YTsaurus usam armazenamento HDD barato, podemos armazenar muito mais documentos do que armazenávamos anteriormente no DynamoDB.
O Pillar rastreia subconjuntos, ou grupos de páginas definidos por políticas, como páginas recentes ou páginas de alto valor. A associação a subconjuntos determina quais atualizações precisam ser repassadas para componentes a jusante, como o CobbleDB. O CobbleDB usa armazenamento NVMe mais caro, portanto, exportamos apenas os documentos que realmente queremos usar nos resultados de pesquisa. Quando o conteúdo de uma página é atualizado, ou se ela é adicionada ou removida de um subconjunto, o Pillar é responsável por enfileirar o trabalho de exportação correspondente.
As transações atômicas do YTsaurus cobrem o estado, intenções de exportação e entradas consumidas para garantir a correção. Para que uma transação seja concluída com sucesso, o sistema deve atualizar o estado durável, marcar a entrada como contabilizada e enfileirar todas as exportações necessárias; se a transação for abortada, nenhuma alteração é feita em nenhuma camada.
O CobbleDB espera que as atualizações sejam agrupadas em lotes de acordo com a partição. As exportações do Pillar são convertidas para esse formato por um serviço sem estado que chamamos de Lorry. Sua única função é ler registros de exportação de uma fila persistente alinhada a partições, acumulá-los em arquivos de lote por partição e entregá-los ao CobbleDB.
Entre o Lorry e o Cobble, um protocolo adicional separa o plano de dados (S3) do plano de controle. O Lorry grava o lote no S3 sob um identificador exclusivo e registra o lote dentro do Cobble. As réplicas de partição do Cobble consultam de forma independente o lote ou a API do plano de controle para o próximo lote do S3 a ser ingerido e aplicam assincronamente as atualizações de lote em ordem cronológica. Isso significa que um nó lento ou em recuperação pode se atualizar no seu próprio ritmo, em vez de desacelerar o resto do sistema.
O novo caminho separa a transação de processamento imediato da ingestão do armazenamento ativo e fornece um caminho reproduzível para atualizações incrementais e reconstruções em massa. Essa divisão de trabalho garante que as atualizações no estado durável do documento não interfiram na latência do caminho de leitura.
Validação
5x mais rápido
Após migrar o armazenamento ativo, a latência mediana de leitura em lote caiu de 31,4 ms para 5,60 ms. O p90 caiu de 56,7 ms para 9,77 ms, e o p99 caiu de 123 ms para 24,2 ms. As proporções variam de 5,08× a 5,80×, com melhorias tanto nas solicitações típicas quanto na cauda.
Esses números são derivados de leituras em lote, onde cada consulta solicita aproximadamente 10 a 15 chaves. O tamanho médio do item de carga útil é de 50 KB. Ambos os sistemas foram consultados a cerca de 200 mil rps, embora também tenhamos executado testes de carga de até 500 mil rps sem degradação.
Para isolar a contribuição do CobbleDB, todos os outros aspectos da configuração foram mantidos constantes. No entanto, esta continua sendo uma comparação observacional de antes e depois; os sistemas atenderam ao tráfego de produção real em momentos diferentes, em vez das mesmas solicitações sob condições controladas. Portanto, também medimos o desempenho de ambos os sistemas em um benchmark sintético. Para aproximar as solicitações vistas em produção, consultamos ambos com lotes de 10 a 15 chaves e valores variando de 100B a 100 KiB.
O DynamoDB tem dificuldades nesse contexto porque seu caminho de leitura gerenciado não pode ser ajustado para leituras em lote desse tamanho. Em contrapartida, o CobbleDB é otimizado exatamente para esse cenário, uma vez que gerenciamos o posicionamento, o cache e os controles de réplica. Ele agrupa chaves por partição, executa leituras em paralelo contra réplicas de partição e pode proteger contra leituras lentas usando outras réplicas. Nossos resultados demonstram que essas escolhas de design se traduzem em leituras mais rápidas em um ambiente de produção.
20% mais barato
O CobbleDB também é significativamente mais barato que o DynamoDB. Além do armazenamento, o DynamoDB também cobra por cada byte lido ou gravado. Na escala do nosso corpus e do tráfego de atendimento, essas cobranças baseadas no uso tornam-se uma parte substancial do custo total. Atualizações contínuas consomem capacidade de gravação, enquanto cada lote de páginas preparadas buscado para uma consulta consome capacidade de leitura.
Calculamos a economia de custos com base em estimativas internas de tamanho de armazenamento, unidades de capacidade de gravação e unidades de capacidade de leitura. Em cada nível de compromisso, o CobbleDB é pelo menos 20% mais barato que o DynamoDB. A economia real é provavelmente ainda maior, uma vez que esses resultados não levam em conta a provável economia de backup obtida com a compressão.
Como um enxame de agentes de IA nos ajuda a construí-lo
Construir o CobbleDB e a infraestrutura ao redor não foi nada trivial. O banco de dados principal do CobbleDB sozinho tem cerca de 40.000 linhas de Rust, e a migração também exigiu um extenso trabalho para verificar a correção e o comportamento de falha em escala de produção, integrar o CobbleDB aos caminhos de processamento e atendimento, e transicionar com segurança o tráfego ativo.
Aceleramos esse esforço usando um sistema interno que opera um enxame de agentes de codificação. O sistema foi projetado para ser altamente persistente; ele retém os objetivos do projeto, riscos ativos, histórico do repositório e decisões anteriores entre sessões.
Atribuímos um coletivo desses agentes proativos e sempre ativos ao projeto CobbleDB. A primeira tarefa deles foi criar um modelo preciso do trabalho já em andamento. Eles auditaram o canal do projeto e os tópicos ativos, conectaram itens pendentes aos seus pull requests e proprietários, e produziram um relatório atualizado sobre a integridade do sistema e as próximas ações. A partir daí, eles atuaram em várias partes do ciclo de engenharia:
- Código e alterações de infraestrutura revisados.
- Bloqueadores surgidos que eram fáceis de perder, incluindo suposições de restauração inseguras, referências de compilação obsoletas e configurações que falhariam em tempo de execução.
- Correções focadas preparadas, testes, alterações de monitoramento e documentação operacional.
- Controle de CI, revisão e barreiras de infraestrutura, mantendo as ações de produção explicitamente sob responsabilidade humana.
- Lançamentos monitorados e exercícios de backup ou restauração, comparando o estado observado do sistema com os critérios de sucesso do lançamento.
- Relatórios de projetos mantidos para que os engenheiros pudessem ver o que foi entregue, o que permaneceu bloqueado e onde o julgamento humano ainda era necessário.
Os engenheiros ainda definem a arquitetura, revisam alterações consequenciais e autorizam operações de produção. Os agentes lidaram com grande parte da inspeção contínua e do acompanhamento entre essas decisões. Isso permitiu que uma pequena equipe conduzisse o projeto com eficiência: a infraestrutura principal do CobbleDB foi construída por dois engenheiros humanos e centenas de agentes de IA em dois meses.
Conclusão
O CobbleDB não é um banco de dados de uso geral; ele foi criado especificamente para nossa carga de trabalho de leituras em lote repetidas de registros de páginas preparadas, alimentadas por um pipeline de processamento assíncrono. Ao separar o estado durável, a entrega de atualizações e o atendimento no momento da consulta, podemos otimizar cada responsabilidade de forma independente, sem forçar uma única interface de banco de dados a acomodar todas elas ou transferir trabalho de manutenção para o caminho de consulta ativo. A migração para o CobbleDB melhorou significativamente a latência mediana e da cauda, reduzindo os custos de infraestrutura.
À medida que continuamos a desenvolver infraestrutura nativa de IA internamente, podemos otimizar o desempenho e o custo de maneiras que não são possíveis quando estamos presos a implementações e modelos de preços de terceiros. Agentes de IA poderosos inclinam ainda mais o cálculo a favor de construir em vez de comprar.
Planejamos disponibilizar o CobbleDB como código aberto em breve, para que outras equipes que constroem sistemas de busca nativos de IA possam se beneficiar da mesma camada de armazenamento que torna a busca mais rápida e barata dentro de nossos produtos.
Citação
@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}
}