Elasticsearch simdvec é o motor por trás de cada cálculo de distância vetorial no Elasticsearch. Ele fornece kernels AVX-512 e NEON ajustados manualmente para cada tipo de vetor que o Elasticsearch aceita. A arquitetura de pontuação em lote oculta a latência de memória por meio de pré-busca explícita em x86 e carregamento intercalado no ARM, superando bibliotecas como FAISS e jvector por até 4x quando os dados excedem o cache da CPU. Neste post, explicamos por que o construímos, o que ele contém e como ele deixa a busca vetorial do Elasticsearch uma das mais rápidas do mundo.
Como construímos o Elasticsearch com simdvec
Toda consulta de busca vetorial no Elasticsearch, seja por meio de percurso Hierarchical Navigable Small World (HNSW), varredura de arquivo invertido (IVF) ou reclassificação, se resume ao mesmo problema: calcular as distâncias entre vetores, milhões de vezes por consulta. O Elasticsearch é compatível com uma ampla variedade de tipos de dados e estratégias de quantização, desde float32 até int8, bfloat16, binário e Quantização Binária Aprimorada (BBQ). Cada uma traz diferentes contrapartidas entre memória, taxa de transferência e recuperação. Por trás de tudo isso há um único mecanismo: simdvec.
Criamos o simdvec para tornar cada cálculo de distância o mais rápido que o hardware permite. Neste post, explicamos por que o criamos, o que está dentro e onde ele entrega o maior impacto.
Construído como um carro de corrida
Como entusiastas da Fórmula 1, e um de nós tendo trabalhado anteriormente com a equipe Ferrari de Fórmula 1, vemos um paralelo claro. Um carro de Fórmula 1 é projetado com um único propósito: alcançar o melhor tempo de volta. Potência do motor, aerodinâmica e design do chassi só importam na medida em que contribuem para esse resultado. O mesmo vale para um banco de dados vetorial, onde a taxa de transferência de indexação, a latência de consulta e o recall definem o sucesso.
Embora o resultado final seja o que importa, alcançar os mais altos níveis de desempenho exige que cada componente esteja no estado ideal. Não pode ser só bom o suficiente, tem que ser o melhor na categoria. O Simdvec foi construído com essa mentalidade, focando uma parte crítica do sistema: o mecanismo. Trata-se de uma biblioteca de kernel otimizada para SIMD (Single Instruction Multiple Data), criada especificamente para fornecer funções de distância nativas em C++ ajustadas manualmente, chamadas a partir do Java via interface de função estrangeira (FFI) Panama. Ele trabalha com pontuação em lote, pré-busca de linhas de cache e todos os tipos e layouts vetoriais usados no Elasticsearch.
Esse é o mecanismo por trás de cada consulta.
Por que criamos nosso
Começamos em 2023 com a Panama Vector API no Apache Lucene. Funcionou bem para produtos dot float32, mas as necessidades do Elasticsearch logo superaram o que ele podia oferecer. O Elasticsearch é compatível com uma ampla variedade de tipos de vetores quantizados: int8, int4, bfloat16, bit único e BBQ assimétrico. Cada um possui estratégias SIMD diferentes, layouts de empacotamento e requisitos de acumuladores. Além da cobertura de tipos, os caminhos de pontuação do Elasticsearch exigem mais do que a taxa de transferência de pares únicos: o HNSW precisa pontuar vários vizinhos do gráfico em uma única passagem, o IVF precisa da pontuação em lote de milhares de candidatos com pré-busca e a pontuação baseada em disco precisa funcionar diretamente na memória mapeada em memória (mmap) sem cópia. Vimos o que estava disponível e nada abrangia o conjunto completo.
Então, criamos o simdvec: kernels C++ nativos ajustados manualmente, chamados de Java via FFI, com pontuação em massa, pré-busca e suporte para cada tipo de vetor que o Elasticsearch usa. Ao possuir a biblioteca, controlamos toda a stack. Quando adicionamos um novo tipo de quantização como BBQ, ele recebe um kernel SIMD ajustado ligado por todo o sistema. Não esperamos uma biblioteca upstream para suportá-lo e não comprometemos o desempenho de nenhum tipo. Toda consulta vetorial no Elasticsearch, seja HNSW, IVF, de reclassificação ou híbrida, pode ser executada nesse mecanismo, construído em torno das operações e tipos que realmente usamos.
O Simdvec possui bibliotecas nativas separadas para x86 e ARM, cada uma com múltiplos níveis de arquitetura de conjunto de instruções (ISA) selecionados no início. A sobrecarga de chamadas do Java via FFI é muito baixa, com nanossegundos de um dígito.
O cenário
Não somos os únicos a construir kernels de distância vetorial otimizados para SIMD. O ecossistema é rico, e queríamos entender como o simdvec funciona. Não para classificar projetos, mas para fornecer contexto e explicar onde o mecanismo do Elasticsearch está localizado. Selecionamos três projetos como pontos de referência, cada um representando uma abordagem diferente:
- jvector: uma biblioteca Java de busca aproximada de vizinhos mais próximos (ANN) que usa a Panama Vector API para cálculo vetorizado de distância, com aceleração nativa em C opcional no x86.
- FAISS: um framework de busca vetorial open source amplamente utilizada, com kernels AVX2/AVX-512 otimizados manualmente.
- NumKong (anteriormente SimSIMD): um conjunto abrangente de mais de 2.000 kernels SIMD ajustados manualmente, abrangendo funções de distância, operações matriciais e computação geoespacial.
Cada projeto tem um propósito diferente e realiza diferentes compensações. Incluímos números de referência deles para dar contexto sobre o desempenho do simdvec nas operações específicas que o Elasticsearch precisa.
Como medimos
Os benchmarks simdvec e jvector são escritos em Java com o JMH, o conjunto padrão de microbenchmark da JVM, com a sobrecarga de FFI incluída. Para os benchmarks NumKong e FAISS, criamos programas em C/C++ reduzidos usando o Google Benchmark, que é a estrutura padrão de microbenchmarks em C++. Ambos os frameworks reportam nanossegundos por operação com calibração de aquecimento e iteração. Verificamos por meio de contadores de desempenho de hardware que todas as bibliotecas estão usando SIMD em ambas as plataformas. Todo o código do benchmark está disponível publicamente nos repositórios vinculados do GitHub (e, no caso do simdvec, no repositório elasticsearch).

Software: JDK 25.0.2, JMH 1.37, GCC 14, Google Benchmark (a versão mais recente).
Um vetor de cada vez
A operação mais fundamental na busca vetorial é calcular a distância entre dois vetores. Cada avaliação de vizinho HNSW, cada pontuação de candidata a IVF, cada comparação de reclassificação se reduz a esse ciclo interno.
Medimos a taxa de transferência de pares individuais em 1024 dimensões em ambas as plataformas, começando com float32, o tipo de referência e aquele em que o ecossistema é mais competitivo. Comparamos simdvec com FAISS e jvector; excluímos o NumKong porque ele usa acumuladores float64 para float32, tornando-o 3,2x-5,3x mais lento (dependendo da plataforma), priorizando precisão numérica em vez de throughput. Para manter a comparação comparável, comparamos o NumKong no int8, onde ele usa a mesma estratégia de acumulador do simdvec.

No x86, o FAISS AVX-512 é o kernel de par único mais rápido, com 23 ns. O Simdvec AVX-512 segue a 28 ns, uma lacuna que reflete a sobrecarga de chamadas FFI. Ambos usam FMA de 512 bits com desenrolamento de multi-acumuladores. No nível AVX2, os dois são muito mais próximos, 36 ns e 39 ns respectivamente, ambos limitados pela largura de carga de registrador e memória de 256 bits. O jvector é executado em 44 ns usando a API Java Panama Vector. O Panama gera um bom código SIMD, mas os intrínsecos do C++ ajustados manualmente mantêm uma vantagem.

No ARM, o simdvec lidera com 70 ns, bem à frente do jvector com 110 ns e do FAISS com 156 ns. O Simdvec tem kernels NEON ajustados manualmente para aarch64. O Jvector não tem código ARM nativo e depende do Panama. O FAISS depende da autovetorização do compilador em vez de intrínsecos explícitos do NEON, o que explica a lacuna maior. Isso reflete uma vantagem prática de ter a biblioteca do kernel: quando o Elasticsearch expandiu para o Graviton, adicionamos kernels NEON construídos especificamente para isso. Nem o jvector, nem o FAISS priorizaram código nativo ARM na mesma medida.
Mas o Elasticsearch não pontua apenas em float32. A quantização Int8 reduz a memória em 4x, bfloat16 em 2x e BBQ em 32x. Cada tipo precisa de sua própria estratégia SIMD, e o simdvec fornece kernels nativos ajustados manualmente para todos eles.
Das bibliotecas que comparamos, apenas a NumKong tem kernels comparáveis para int8. Medimos o produto escalar int8, euclidiano ao quadrado e cosseno em 1.024 dimensões.
Pontuação Int8 par único (1024 dimensões, ns/vec op – quanto menor, melhor)

Em ambas as arquiteturas, o NumKong é igual ou mais rápido em dimensões pequenas a médias, onde a diferença se deve em grande parte à menor sobrecarga de chamadas (chamada direta em C vs Java FFI). Em dimensões maiores, o simdvec alcança o desempenho do kernel, onde a implementação mais eficiente (que usa desenrolamento em cascata) amortiza o custo da chamada: conforme a dimensão aumenta, essa diferença diminui e eventualmente se inverte. O crossover está em dimensões entre 768 e 1.536, dependendo da função e arquitetura.
Apesar da sobrecarga ligeiramente maior do Java FFI, o simdvec está no mesmo nível das bibliotecas altamente otimizadas em C/C++. Além de ser a única biblioteca com kernels otimizados tanto para float32 quanto para int8, também lidera em ARM e fica apenas um pouco atrás de FAISS em x86 (para float32), e muito próxima de NumKong em ambas as arquiteturas (para int8). E, para bfloat16, int4, binário e BBQ, embora existam alternativas, o simdvec se destaca graças ao SIMD ajustado manualmente, adaptado ao layout de dados de cada tipo.
Mas um mecanismo de busca de produção não pontua um vetor de cada vez; ele marca milhares por consulta. A próxima pergunta é o que acontece nessa escala.
Milhares de uma só vez
O desempenho de um único par é apenas parte do panorama. O que importa na prática é como os sistemas se comportam sob carga. Uma única consulta HNSW pode pontuar centenas de vizinhos do gráfico. Uma varredura de IVF pode pontuar milhares de entradas da lista de postagens. Uma passagem de reclassificação pode pontuar dezenas de milhares de candidatos. A taxa de transferência de pares individuais é importante, mas o que importa ainda mais é a rapidez com que você consegue pontuar vários vetores e a suavidade com que o desempenho se degrada à medida que o conjunto de trabalho transborda dos caches da CPU.
O Simdvec fornece pontuação em lote para todos os tipos de dados. Esses não são apenas loops sobre kernels de par único; eles usam loops internos multiacumuladores que carregam o vetor de consulta uma vez por passo dimensional e o compartilham entre múltiplos vetores de documentos, com pré-busca explícita de linha de cache para o lote seguinte. Nem jvector, nem FAISS oferecem algo equivalente (no momento em que escrevo). O Jvector não tem bulk API, então os chamadores marcam um par por vez em um loop. O FAISS expõe fvec_inner_products_ny, que, no momento da escrita, é implementado como um ciclo sobre a função de distância de par único, sem amortização de consulta ou pré-busca.
Float32. Para medir o impacto no nível do kernel, avaliamos uma única consulta contra números crescentes de vetores de documento float32 de 1.024 dimensões, usando padrões de acesso aleatório que simulam buscas de vizinhos de gráficos dispersos semelhantes ao HNSW. Os três tamanhos de conjunto de dados, 32, 625 e 32.500 vetores, são escolhidos para que o conjunto de trabalho exceda o cache L1, L2 e L3, respectivamente.

Quando os dados cabem no cache, o simdvec é o mais rápido em ambas as plataformas, mas as margens são modestas, já que a aritmética do kernel predomina. A verdadeira separação surge à medida que o conjunto de trabalho cresce além do nível L3. Em x86, o simdvec atinge 95 ns por vetor, enquanto o FAISS precisa de 165 ns e o jvector, de 412 ns. Em ARM, o padrão é o mesmo: o simdvec se mantém em 162 ns, enquanto o FAISS sobe para 347 ns e o jvector para 476 ns. A pré-busca e a amortização de consultas no simdvec mantêm a latência de memória oculta de uma forma que um simples loop sobre kernels de par único não consegue igualar, e a vantagem se amplia precisamente onde as cargas de trabalho de busca reais operam, nas profundezas da memória principal.
Int8. O mesmo padrão se aplica aos tipos quantizados. Medimos a pontuação em lote do produto escalar int8 em 1.024 dimensões, com tamanhos de conjuntos de dados escolhidos para exceder os mesmos limites do cache L1, L2 e L3, comparando a pontuação em lote do simdvec com a pontuação de par único do NumKong em um ciclo.


No x86, o simdvec é de 1,2x a 1,9x mais rápido, impulsionado pela combinação de pré-busca explícita e processamento em lote. No ARM, o simdvec vence novamente (1,7x a 1,9x mais rápido) em todos os tamanhos de conjuntos de dados. A vantagem vem do processamento em lote de quatro vetores por vez, oferecendo paralelismo em nível de memória por meio de um padrão de acesso intercalado. Em ambos os casos, o resultado mais impressionante é o que ocorre no maior tamanho de conjunto de dados, onde mais importa.
Os resultados para distância ao quadrado e cosseno mostram um padrão semelhante, com acelerações de 1,4x a 1,8x para ARM e de 1,3x a 3,0x para x86 (detalhes aqui).
Quando a memória é o mais importante
Índices vetoriais de produção normalmente não cabem no cache da CPU. Um índice int8 de 10 milhões de vetores, com 1.024 dimensões, tem 10 GB. Pontuar candidatos significa fazer streaming de dados a partir da DRAM e é aí que a arquitetura de pontuação em lote faz a diferença.
Usamos contadores de desempenho de hardware para medir o que acontece dentro da CPU durante a pontuação em lote e descobrimos que ocultar a latência de memória exige duas estratégias fundamentalmente diferentes, uma por arquitetura.
No x86, a pré-busca explícita elimina os erros de cache. O kernel em massa processa os vetores sequencialmente, um totalmente computado antes do próximo, enquanto emite instruções de pré-busca para o próximo lote. Os dados futuros são puxados para L1 antes que a CPU precise deles.

Em ARM, a mesma abordagem sequencial teve desempenho ruim, mesmo com prefetching. Em vez disso, o kernel bulk intercala leituras de quatro vetores em cada posição do stride, dando ao mecanismo de execução fora de ordem quatro fluxos de memória independentes. A CPU não está buscando dados mais rápido, mas sim esperando menos, porque sempre há outra coisa para calcular enquanto as requisições de memória estão em voo. Você pode encontrar uma análise detalhada nesta edição do GitHub.

Os números contam duas histórias diferentes:
- Em x86, a pré-busca transforma 139K erros de cache em 19K, e as instruções por ciclo (IPC) mais que dobram. A grande vantagem aumenta com o tamanho do conjunto de dados, de 1,2x em L2 para 2,8x além de L3, porque a pré-busca oculta viagens de ida e volta de DRAM cada vez mais caras.
- No ARM, as falhas de cache mal mudam. O que muda é o uso: as estagnações de backend caem 40% porque o padrão de acesso intercalado mantém o pipeline alimentado. Essa vantagem se mantém consistente em 1,8x, independentemente do tamanho do conjunto de dados, porque o paralelismo no nível da memória se aplica independentemente de os dados virem do cache ou da DRAM.
Duas arquiteturas, duas estratégias, um resultado: em escala de produção, o simdvec mantém o pipeline da CPU ocupado mesmo quando os vetores estão espalhados pela memória principal.
O que isso significa para os usuários do Elasticsearch
Essas capacidades em nível de kernel se acumulam. Uma única consulta vetorial pode calcular milhões de operações de distância: percurso de gráficos HNSW, pontuação de candidatos, reclassificação. Ao longo de milhares de consultas concorrentes, nanossegundos por operação se traduzem diretamente em latência de consulta e transferência do cluster. Seja usando float32, int8, bfloat16 ou BBQ, seja seu índice na memória ou no disco, simdvec é o motor por baixo, e cada uma dessas operações executa pelo mesmo motor, ajustado até o último nanosegundo.
A principal conclusão é que, em escala de produção, o desempenho da busca vetorial não é determinado principalmente pela taxa de transferência SIMD bruta. Ele é dominado pela eficiência com que o sistema oculta a latência da memória e, ao mesmo tempo, mantém a computação em milhões de pequenas operações.
Os kernels simdvec são aprimorados em quase todas as versões do Elasticsearch. Quando surgem novos tipos de quantização e plataformas de hardware, eles recebem kernels ajustados desde o primeiro dia. E os tipos existentes continuam a ficar mais rápidos à medida que refinamos as implementações que já estão sendo lançadas.




