Indexação segura de grandes bases de código

Por Jeremy Stribling em Pesquisa
Indexação segura de grandes bases de código

A busca semântica é um dos maiores impulsionadores do desempenho dos agentes. Em nossa avaliação recente, ela melhorou a precisão das respostas em 12,5% em média, produziu alterações de código mais propensas a serem mantidas nas bases de código e elevou a satisfação geral com as solicitações.

Para viabilizar a busca semântica, o Cursor cria um índice pesquisável da sua base de código quando você abre um projeto. Para projetos pequenos, isso acontece quase instantaneamente. Mas grandes repositórios com dezenas de milhares de arquivos podem levar horas para serem processados se forem indexados de forma ingênua, e a busca semântica só fica disponível quando pelo menos 80% desse trabalho é concluído.

Buscamos maneiras de acelerar a indexação com base na simples observação de que a maioria das equipes trabalha a partir de cópias quase idênticas da mesma base de código. Na verdade, clones da mesma base de código apresentam, em média, 92% de similaridade entre usuários de uma mesma organização.

Isso significa que, em vez de reconstruir cada índice do zero quando alguém entra na equipe ou troca de máquina, podemos reutilizar com segurança o índice existente de um colega. Isso reduz o tempo até a primeira consulta de horas para segundos nos maiores repositórios.

Construindo o primeiro índice

O Cursor constrói sua primeira visão de uma base de código usando uma árvore de Merkle, o que permite detectar exatamente quais arquivos e diretórios foram alterados sem reprocessar tudo. A árvore de Merkle contém um hash criptográfico de cada arquivo, junto com hashes de cada pasta calculados a partir dos hashes de seus filhos.

Pequenas edições no lado do cliente alteram apenas os hashes do próprio arquivo editado e os hashes dos diretórios pai até a raiz da base de código. O Cursor compara esses hashes com a versão do servidor para ver exatamente onde as duas árvores de Merkle divergem. Entradas cujos hashes diferem são sincronizadas. Entradas que coincidem são ignoradas. Qualquer entrada ausente no cliente é excluída do servidor, e qualquer entrada ausente no servidor é adicionada. O processo de sincronização nunca modifica arquivos no lado do cliente.

A abordagem com árvore de Merkle reduz significativamente a quantidade de dados que precisam ser transferidos em cada sincronização. Em um workspace com cinquenta mil arquivos, apenas os nomes de arquivos e os hashes SHA-256 somam cerca de 3,2 MB. Sem a árvore, você teria que mover esses dados em cada atualização. Com a árvore, o Cursor percorre apenas os ramos em que os hashes diferem.

Quando um arquivo muda, o Cursor o divide em blocos sintáticos. Esses blocos são convertidos em embeddings que viabilizam a busca semântica. A criação de embeddings é a etapa cara, por isso o Cursor a executa de forma assíncrona em segundo plano.

A maioria das edições deixa a maior parte dos blocos inalterada. O Cursor armazena em cache os embeddings pelo conteúdo do bloco. Blocos inalterados batem no cache, e as respostas do Agente continuam rápidas sem pagar esse custo novamente no momento da inferência. O índice resultante é rápido de atualizar e leve de manter.

Encontrando o melhor índice para reutilizar

O pipeline de indexação acima envia todos os arquivos quando uma base de código é nova no Cursor. Porém, novos usuários dentro de uma organização não precisam passar por todo esse processo.

Quando um novo usuário entra, o cliente calcula a árvore de Merkle para uma nova base de código e deriva um valor chamado hash de similaridade (simhash) a partir dessa árvore. Esse é um único valor que funciona como um resumo dos hashes de conteúdo de arquivo na base de código.

O cliente envia o simhash para o servidor. O servidor então o usa como um vetor para buscar em um banco de dados vetorial composto por todos os outros simhashes atuais de todos os outros índices no Cursor na mesma equipe (ou do mesmo usuário) que o cliente. Para cada resultado retornado pelo banco de dados vetorial, verificamos se ele corresponde ao hash de similaridade do cliente acima de um valor de limiar. Se corresponder, usamos esse índice como índice inicial para a nova base de código.

Essa cópia acontece em segundo plano. Enquanto isso, o cliente já pode fazer novas buscas semânticas contra o índice original que está sendo copiado, resultando em um tempo muito curto até a primeira consulta para o cliente.

Mas isso só funciona se duas restrições forem atendidas. Os resultados precisam refletir a base de código local do usuário, mesmo quando ela difere do índice copiado. E o cliente nunca pode ver resultados para código que ele ainda não possui.

Comprovando acesso

Para garantir que arquivos não vazem entre diferentes cópias da base de código, reaproveitamos as propriedades criptográficas da árvore de Merkle.

Cada nó na árvore é um hash criptográfico do conteúdo que está abaixo dele. Você só consegue calcular esse hash se tiver o arquivo. Quando um workspace é iniciado a partir de um índice copiado, o cliente envia sua árvore de Merkle completa junto com o hash de similaridade. Isso associa um hash a cada caminho criptografado na base de código.

O servidor armazena essa árvore como um conjunto de provas de conteúdo. Durante a busca, o servidor filtra os resultados comparando esses hashes com a árvore do cliente. Se o cliente não conseguir provar que possui um arquivo, o resultado é descartado.

Isso permite que o cliente faça consultas imediatamente e veja resultados apenas para o código que compartilha com o índice copiado. A sincronização em segundo plano resolve as diferenças restantes. Quando as raízes das árvores de Merkle do cliente e do servidor forem idênticas, o servidor exclui as provas de conteúdo e as próximas consultas passam a usar o índice totalmente sincronizado.

Onboarding mais rápido

Reutilizar os índices dos membros da equipe melhora o tempo de configuração para repositórios de todos os tamanhos. O efeito se amplifica conforme o tamanho do repositório:

  • Para o repositório típico, o tempo até a primeira consulta cai de 7,87 segundos para 525 milissegundos

  • No percentil 90, ele cai de 2,82 minutos para 1,87 segundos

  • No percentil 99, ele cai de 4,03 horas para 21 segundos.

Essas mudanças eliminam uma grande fonte de retrabalho e permitem que o Cursor entenda até bases de código muito grandes em segundos, não em horas.

Arquivado em: Pesquisa

Autor: Jeremy Stribling

Indexação segura de grandes bases de código · Cursor