Pular para o conteúdo principal
Neste guia, você vai:
  • Ter uma breve introdução à busca vetorial
  • Aprender sobre Vizinhos Mais Próximos Aproximados (ANN) e Hierarchical Navigable Small World (HNSW)
  • Conhecer o Quantised Bit (QBit)
  • Usar o QBit para realizar busca vetorial com o dataset DBPedia

Noções básicas sobre busca vetorial

Na matemática e na física, um vetor é formalmente definido como um objeto que tem magnitude e direção. Isso geralmente assume a forma de um segmento de reta ou de uma seta no espaço e pode ser usado para representar grandezas como velocidade, força e aceleração. Na ciência da computação, um vetor é uma sequência finita de números. Em outras palavras, é uma estrutura de dados usada para armazenar valores numéricos. Em machine learning, vetores são as mesmas estruturas de dados de que falamos na ciência da computação, mas os valores numéricos armazenados neles têm um significado especial. Quando pegamos um bloco de texto ou uma imagem e o reduzimos aos conceitos-chave que ele representa, esse processo é chamado de codificação. O resultado é a representação numérica, por uma máquina, desses conceitos-chave. Isso é um embedding, e ele é armazenado em um vetor. Em outras palavras, quando esse significado contextual é incorporado a um vetor, podemos nos referir a ele como um embedding. A busca vetorial está em toda parte hoje. Ela viabiliza recomendações de música, geração aumentada por recuperação (RAG) para grandes modelos de linguagem, em que conhecimento externo é buscado para melhorar as respostas, e até mesmo as buscas no Google são impulsionadas, em certa medida, pela busca vetorial. Os usuários frequentemente preferem bancos de dados tradicionais com recursos vetoriais ad hoc a armazenamentos vetoriais totalmente especializados, apesar das vantagens dos bancos de dados especializados. O ClickHouse oferece suporte à busca vetorial por força bruta, bem como a métodos de busca aproximada de vizinho mais próximo (ANN), incluindo HNSW — o padrão atual para recuperação vetorial rápida.

Entendendo embeddings

Vamos ver um exemplo simples para entender como a busca vetorial funciona. Considere embeddings (representações vetoriais) de palavras: Crie a tabela abaixo com alguns embeddings de exemplo:
Você pode procurar as palavras mais semelhantes a um determinado embedding:
O embedding da consulta está mais próximo de “apple” (menor distância), o que faz sentido se observarmos os dois embeddings lado a lado:

Vizinhos mais próximos aproximados (ANN)

Para grandes conjuntos de dados, a busca por força bruta se torna lenta demais. É aí que entram os métodos de vizinhos mais próximos aproximados.

Quantização

A quantização envolve converter para tipos numéricos menores. Números menores significam menos dados, e menos dados significam cálculos de distância mais rápidos. O mecanismo de execução vetorizada de consultas do ClickHouse consegue acomodar mais valores nos registradores do processador a cada operação, aumentando diretamente a taxa de transferência. Você tem duas opções:
  1. Manter a cópia quantizada junto da coluna original - Isso dobra o armazenamento, mas é seguro, pois sempre podemos voltar à precisão total
  2. Substituir totalmente os valores originais (convertendo para tipos menores na inserção) - Isso economiza espaço e E/S, mas não tem volta

Hierarchical Navigable Small World (HNSW)

O HNSW é construído a partir de várias camadas de nós (vetores). Cada nó é atribuído aleatoriamente a uma ou mais camadas, e a probabilidade de aparecer em camadas mais altas diminui exponencialmente. Ao realizar uma busca, começamos por um nó na camada superior e avançamos de forma gulosa em direção aos vizinhos mais próximos. Quando não é possível encontrar um nó mais próximo, descemos para a próxima camada, mais densa. Graças a esse design em camadas, o HNSW alcança complexidade de busca logarítmica em relação ao número de nós.
Limitação do HNSWO principal gargalo é a memória. O ClickHouse usa a implementação usearch de HNSW, que é uma estrutura de dados em memória e não oferece suporte à divisão. Como resultado, conjuntos de dados maiores exigem proporcionalmente mais RAM.

Comparação das abordagens

Análise detalhada do QBit

Quantised Bit (QBit)

QBit é uma nova estrutura de dados capaz de armazenar valores BFloat16, Float32 e Float64 aproveitando a forma como números de ponto flutuante são representados — em bits. Em vez de armazenar cada número inteiro, o QBit divide os valores em planos de bits: o primeiro bit de cada valor, o segundo, o terceiro e assim por diante. Essa abordagem resolve a principal limitação da quantização tradicional. Não é necessário armazenar dados duplicados nem correr o risco de fazer com que os valores percam o significado. Ela também evita os gargalos de RAM do HNSW, já que o QBit trabalha diretamente com os dados armazenados, em vez de manter um índice em memória.
BenefícioMais importante ainda, não é preciso tomar decisões antecipadamente. A precisão e o desempenho podem ser ajustados dinamicamente em tempo de consulta, permitindo que os usuários explorem o equilíbrio entre exatidão e velocidade com o mínimo de esforço.
LimitaçãoEmbora o QBit acelere a busca vetorial, sua complexidade computacional continua sendo O(n). Em outras palavras: se seu dataset for pequeno o suficiente para que um índice HNSW caiba confortavelmente na RAM, essa ainda será a opção mais rápida.

O tipo de dado

Veja como criar uma coluna QBit:
Quando os dados são inseridos em uma coluna QBit, eles são transpostos para que todos os primeiros bits fiquem alinhados, todos os segundos bits fiquem alinhados, e assim por diante. Chamamos esses conjuntos de grupos. Cada grupo é armazenado em uma coluna FixedString(N) separada: strings de comprimento fixo de N bytes, armazenadas consecutivamente na memória, sem separadores entre elas. Todos esses grupos são então reunidos em uma única Tuple, que forma a estrutura subjacente do QBit. Exemplo: Se começarmos com um vetor de 8 elementos Float64, cada grupo conterá 8 bits. Como um Float64 tem 64 bits, acabamos com 64 grupos (um para cada bit). Portanto, o layout interno de QBit(Float64, 8) se parece com uma Tuple de 64 colunas FixedString(1).
Se o comprimento do vetor original não for divisível por 8, a estrutura será preenchida com elementos invisíveis para alinhá-la a 8. Isso garante compatibilidade com FixedString, que opera estritamente com bytes completos.

O cálculo da distância

Para consultar com o QBit, use a função L2DistanceTransposed com um parâmetro de precisão:
O terceiro parâmetro (16) especifica o nível de precisão, em bits.

Otimização de I/O

Antes de podermos calcular distâncias, os dados necessários precisam ser lidos do disco e depois destranspostos (convertidos de volta da representação de bits agrupados para vetores completos). Como o QBit armazena valores transpostos em bits por nível de precisão, o ClickHouse pode ler apenas os planos de bits mais significativos necessários para reconstruir os números com a precisão desejada. Na consulta acima, usamos um nível de precisão de 16. Como um Float64 tem 64 bits, lemos apenas os primeiros 16 planos de bits, ignorando 75% dos dados. Após a leitura, reconstruímos apenas a parte mais significativa de cada número a partir dos planos de bits carregados, deixando zerados os bits que não foram lidos.

Otimização do cálculo

Pode-se perguntar se converter para um tipo menor, como Float32 ou BFloat16, poderia eliminar essa parte não utilizada. Funciona, mas casts explícitos são custosos quando aplicados a cada linha. Em vez disso, podemos fazer downcast apenas do vetor de referência e tratar os dados QBit como se contivessem valores de menor largura (“esquecendo” a existência de algumas colunas), já que seu layout frequentemente corresponde a uma versão truncada desses tipos.

Otimização de BFloat16

BFloat16 é um Float32 truncado à metade. Ele mantém o mesmo bit de sinal e o expoente de 8 bits, mas apenas os 7 bits mais altos da mantissa de 23 bits. Por isso, ler os primeiros 16 planos de bits de uma coluna QBit reproduz, na prática, o layout dos valores BFloat16. Portanto, neste caso, podemos (e de fato fazemos isso) converter com segurança o vetor de referência para BFloat16.

Complexidade do Float64

O Float64, porém, é outra história. Ele usa um expoente de 11 bits e uma mantissa de 52 bits, o que significa que não é simplesmente um Float32 com o dobro de bits. Sua estrutura e o viés do expoente são completamente diferentes. Fazer o downcast de um Float64 para um formato menor, como Float32, exige uma conversão IEEE-754 propriamente dita, em que cada valor é arredondado para o Float32 representável mais próximo. Essa etapa de arredondamento é computacionalmente cara.
Se você tiver interesse em uma análise aprofundada dos aspectos de desempenho do QBit, veja “Vamos vetorizar”

Exemplo com DBpedia

Vamos ver o QBit em ação em um exemplo real usando o dataset DBpedia, que contém 1 milhão de artigos da Wikipedia representados por embeddings Float32.

Configuração

Primeiro, crie a tabela
Insira os dados pela linha de comando:
A inserção dos dados pode demorar um pouco. Hora de fazer uma pausa para o café!
Como alternativa, instruções SQL individuais podem ser executadas, como mostrado abaixo, para carregar cada um dos 25 arquivos Parquet:
Verifique se há 1 milhão de linhas na tabela dbpedia:
Em seguida, adicione uma coluna QBit:

Consulta de busca

Vamos procurar os conceitos mais relacionados a estes termos de busca sobre o espaço: Lua, Apollo 11, Ônibus Espacial, Astronauta, Foguete:
A consulta busca as 1.000 entradas semanticamente mais semelhantes a cada um dos cinco conceitos. Ela retorna as entradas que aparecem em pelo menos três desses resultados, classificadas por quantos conceitos correspondem e pela menor distância até qualquer um deles (excluindo os originais). Usando apenas 5 bits (1 bit de sinal + 4 bits de expoente, mantissa igual a zero):
Desempenho: 10 rows in set. Elapsed: 0.271 sec. Foram processadas 8.46 milhões de linhas, 4.54 GB (31.19 milhões de linhas/s., 16.75 GB/s.) Uso máximo de memória: 739.82 MiB.
Desempenho: 10 rows in set. Elapsed: 1.157 sec. Processadas 10,00 milhões de linhas, 32,76 GB (8,64 milhões de linhas/s., 28,32 GB/s.) Peak memory usage: 6,05 GiB.

Insight principal

Os resultados? Não apenas bons. Surpreendentemente bons. Não é óbvio que números de ponto flutuante, mesmo sem toda a mantissa e metade do expoente, ainda mantenham informações significativas. O insight principal por trás do QBit é que a busca vetorial ainda funciona se ignorarmos bits insignificantes. Uso de memória reduzido de 6,05 GB para 740 MB, mantendo excelente qualidade de busca semântica!

Conclusão

QBit é um tipo de coluna que armazena números de ponto flutuante como planos de bits. Ele permite escolher quantos bits ler durante a busca vetorial, ajustando o recall e o desempenho sem alterar os dados. Cada método de busca vetorial tem seus próprios parâmetros, que definem os trade-offs entre recall, precisão e desempenho. Normalmente, eles precisam ser definidos antecipadamente. Se você errar nessa escolha, muito tempo e recursos serão desperdiçados, e mudar de rumo depois se torna trabalhoso. Com o QBit, não é preciso tomar decisões antecipadas. Você pode ajustar diretamente, em tempo de consulta, o trade-off entre precisão e velocidade, encontrando o equilíbrio certo à medida que avança.
Adaptado do post no blog de Raufs Dunamalijevs, publicado em 28 de outubro de 2025
Última modificação em 25 de junho de 2026