Skip to main content
O problema de encontrar os N pontos mais próximos, em um espaço multidimensional (vetorial), para um determinado ponto é conhecido como busca do vizinho mais próximo ou, resumidamente, busca vetorial. Existem duas abordagens gerais para resolver a busca vetorial:
  • A busca vetorial exata calcula a distância entre o ponto fornecido e todos os pontos do espaço vetorial. Isso garante a melhor precisão possível, ou seja, os pontos retornados são, de fato, os vizinhos mais próximos. Como o espaço vetorial é percorrido exaustivamente, a busca vetorial exata pode ser lenta demais para uso em cenários reais.
  • A busca vetorial aproximada se refere a um conjunto de técnicas (por exemplo, estruturas de dados especiais, como grafos e florestas aleatórias) que calculam resultados muito mais rapidamente do que a busca vetorial exata. A precisão do resultado normalmente é “boa o suficiente” para uso prático. Muitas técnicas aproximadas oferecem parâmetros para ajustar o equilíbrio entre a precisão do resultado e o tempo de busca.
Uma busca vetorial (exata ou aproximada) pode ser escrita em SQL da seguinte forma:
Os pontos no espaço vetorial são armazenados em uma coluna vectors do tipo array, por exemplo, Array(Float64), Array(Float32) ou Array(BFloat16). O vetor de referência é um array constante e é definido como uma expressão de tabela comum. <DistanceFunction> calcula a distância entre o ponto de referência e todos os pontos armazenados. Qualquer uma das funções de distância disponíveis pode ser usada para isso. <N> especifica quantos vizinhos devem ser retornados. Uma busca vetorial exata pode ser realizada usando a consulta SELECT acima, sem alterações. O tempo de execução dessas consultas geralmente é proporcional ao número de vetores armazenados e à sua dimensão, ou seja, ao número de elementos do Array. Além disso, como o ClickHouse faz uma varredura por força bruta de todos os vetores, o tempo de execução também depende do número de threads usadas pela consulta (consulte a configuração max_threads).

Exemplo

retorna

Índices de similaridade vetorial

O ClickHouse fornece um índice especial de “similaridade vetorial” para realizar busca vetorial aproximada.
Os índices de similaridade vetorial estão disponíveis no ClickHouse versão 25.8 ou superior. Se você encontrar problemas, abra uma issue no repositório do ClickHouse.

Como criar um índice de similaridade vetorial

Um índice de similaridade vetorial pode ser criado em uma nova tabela da seguinte forma:
Alternativamente, para adicionar um índice de similaridade vetorial a uma tabela existente:
Índices de similaridade vetorial são tipos especiais de índices de skipping (veja aqui e aqui). Assim, a instrução ALTER TABLE acima faz com que o índice seja criado apenas para dados inseridos futuramente na tabela. Para criar o índice também para os dados existentes, você precisa materializá-lo:
A função <distance_function> deve ser
  • L2Distance, a distância euclidiana, que representa o comprimento do segmento de reta entre dois pontos no espaço euclidiano,
  • cosineDistance, a distância de cosseno, que representa o ângulo entre dois vetores não nulos, ou
  • dotProduct, o produto escalar (produto interno), que representa a soma dos produtos elemento a elemento de dois vetores. Equivalente a cosineDistance em dados normalizados.
Para dados normalizados, L2Distance geralmente é a melhor escolha; caso contrário, recomenda-se cosineDistance para compensar a escala.
Para as funções de distância L2Distance e cosineDistance, um valor menor significa maior similaridade, enquanto para dotProduct, um valor maior significa maior similaridade. Como resultado, índices vetoriais com L2Distance e cosineDistance só podem ser usados por consultas SELECT [...] ORDER BY [...] ASC (ASC é o padrão de ORDER BY), enquanto índices vetoriais criados para dotProduct só podem ser usados por consultas SELECT [...] ORDER BY [...] DESC.
<dimensions> especifica a cardinalidade do array (número de elementos) na coluna subjacente. Se o ClickHouse encontrar um array com cardinalidade diferente durante a criação do índice, o índice será descartado e um erro será retornado. O parâmetro opcional GRANULARITY <N> refere-se ao tamanho dos grânulos de índice (veja aqui). Ao contrário dos skip indexes comuns, que usam uma granularidade de índice padrão de 1, os índices de similaridade vetorial usam 100 milhões como granularidade de índice padrão. Esse valor garante que apenas poucos índices sejam criados internamente, mesmo para partes grandes. Recomendamos alterar a granularidade do índice apenas para usuários avançados que entendam as implicações do que estão fazendo (veja abaixo). Os índices de similaridade vetorial são genéricos no sentido de que podem acomodar diferentes métodos de busca aproximada. O método efetivamente usado é especificado pelo parâmetro <type>. No momento, o único método disponível é HNSW (artigo acadêmico), uma técnica popular e de última geração para busca vetorial aproximada baseada em grafos hierárquicos de proximidade. Se HNSW for usado como tipo, os usuários poderão especificar opcionalmente outros parâmetros específicos do HNSW:
Os seguintes parâmetros específicos do HNSW estão disponíveis:
  • <quantization> controla a quantização dos vetores no grafo de proximidade. Os valores possíveis são f64, f32, f16, bf16, i8 ou b1. O valor padrão é bf16. Observe que esse parâmetro não afeta a representação dos vetores na coluna subjacente.
  • <hnsw_max_connections_per_layer> controla o número de vizinhos por nó do grafo, também conhecido como o hiperparâmetro HNSW M. O valor padrão é 32. O valor 0 significa usar o valor padrão.
  • <hnsw_candidate_list_size_for_construction> controla o tamanho da lista dinâmica de candidatos durante a construção do grafo HNSW, também conhecido como o hiperparâmetro HNSW ef_construction. O valor padrão é 128. O valor 0 significa usar o valor padrão.
Os valores padrão de todos os parâmetros específicos do HNSW funcionam razoavelmente bem na maioria dos casos de uso. Portanto, não recomendamos personalizar os parâmetros específicos do HNSW. Aplicam-se as seguintes restrições adicionais:
  • Índices de similaridade vetorial só podem ser criados em colunas do tipo Array(Float32), Array(Float64) ou Array(BFloat16). Arrays de tipos de ponto flutuante anuláveis e de baixa cardinalidade, como Array(Nullable(Float32)) e Array(LowCardinality(Float32)), não são permitidos.
  • Índices de similaridade vetorial devem ser criados em uma única coluna.
  • Índices de similaridade vetorial podem ser criados em expressões calculadas (por exemplo, INDEX index_name arraySort(vectors) TYPE vector_similarity([...])), mas esses índices não podem ser usados posteriormente para busca aproximada de vizinhos.
  • Índices de similaridade vetorial exigem que todos os arrays na coluna subjacente tenham <dimension> elementos — isso é verificado durante a criação do índice. Para detectar violações desse requisito o mais cedo possível, os usuários podem adicionar uma restrição à coluna vetorial, por exemplo, CONSTRAINT same_length CHECK length(vectors) = 256.
  • Da mesma forma, os valores de array na coluna subjacente não podem estar vazios ([]) nem ter o valor padrão (também []).
Estimando o consumo de armazenamento e memória Um vetor gerado para uso com um modelo de IA típico (por exemplo, um Large Language Model, LLMs) consiste em centenas ou milhares de valores de ponto flutuante. Assim, um único vetor pode consumir vários kilobytes de memória. Os usuários que quiserem estimar o armazenamento necessário para a coluna vetorial subjacente na tabela, bem como a memória principal necessária para o índice de similaridade vetorial, podem usar as duas fórmulas abaixo: Consumo de armazenamento da coluna vetorial na tabela (não comprimido):
Exemplo com o conjunto de dados dbpedia:
O índice de similaridade vetorial deve ser totalmente carregado do disco na memória principal para realizar as buscas. Da mesma forma, o índice vetorial também é construído integralmente na memória e depois salvo em disco. Consumo de memória necessário para carregar um índice vetorial:
Exemplo com o conjunto de dados dbpedia:
A fórmula acima não considera a memória adicional exigida pelos índices de similaridade vetorial para alocar estruturas de dados em tempo de execução, como buffers pré-alocados e caches.

Usando um índice de similaridade vetorial

Para usar índices de similaridade vetorial, a configuração compatibility deve estar definida como '' (o valor padrão), ou '25.1' ou posterior.
Os índices de similaridade vetorial suportam consultas SELECT neste formato:
O otimizador de consultas do ClickHouse tenta corresponder ao template de consulta acima e utilizar os índices de similaridade vetorial disponíveis. Uma consulta só pode usar um índice de similaridade vetorial se a função de distância na consulta SELECT for a mesma que a função de distância na definição do índice. Usuários avançados podem fornecer um valor personalizado para a configuração hnsw_candidate_list_size_for_search (também conhecida como hiperparâmetro HNSW “ef_search”) para ajustar o tamanho da lista de candidatos durante a busca (por exemplo, SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>). O valor padrão da configuração, 256, funciona bem na maioria dos casos de uso. Valores mais altos resultam em maior precisão, porém com desempenho mais lento. Se a consulta puder usar um índice de similaridade vetorial, o ClickHouse verifica se o LIMIT <N> fornecido nas consultas SELECT está dentro de limites razoáveis. Mais especificamente, um erro é retornado se <N> for maior que o valor da configuração max_limit_for_vector_search_queries, cujo valor padrão é 100. Valores de LIMIT muito grandes podem tornar as buscas mais lentas e geralmente indicam um erro de uso. Para verificar se uma consulta SELECT utiliza um índice de similaridade vetorial, você pode adicionar o prefixo EXPLAIN indexes = 1 à consulta. Como exemplo, consulte
pode retornar
Neste exemplo, 1 milhão de vetores do dataset dbpedia, cada um com dimensão 1536, estão armazenados em 575 grânulos, ou seja, 1,7 mil linhas por grânulo. A consulta solicita 10 vizinhos e o índice de similaridade vetorial localiza esses 10 vizinhos em 10 grânulos distintos. Esses 10 grânulos serão lidos durante a execução da consulta. Os índices de similaridade vetorial são utilizados quando a saída contém Skip e o nome e tipo do índice vetorial (no exemplo, idx e vector_similarity). Nesse caso, o índice de similaridade vetorial descartou dois dos quatro grânulos, ou seja, 50% dos dados. Quanto mais grânulos puderem ser descartados, mais eficaz será o uso do índice.
Para forçar o uso do índice, você pode executar a consulta SELECT com a configuração force_data_skipping_indexes (informe o nome do índice como valor da configuração).
Pós-filtragem e Pré-filtragem Os usuários podem, opcionalmente, especificar uma cláusula WHERE com condições de filtro adicionais para a consulta SELECT. O ClickHouse avaliará essas condições de filtro usando a estratégia de pós-filtragem ou de pré-filtragem. Em resumo, ambas as estratégias determinam a ordem em que os filtros são avaliados:
  • Pós-filtragem significa que o índice de similaridade vetorial é avaliado primeiro; depois, ClickHouse avalia o(s) filtro(s) adicional(is) especificado(s) na cláusula WHERE.
  • Pré-filtragem significa que a ordem de avaliação do filtro é inversa.
As estratégias apresentam diferentes trade-offs:
  • A pós-filtragem tem o problema geral de poder retornar menos linhas do que o número solicitado na cláusula LIMIT <N>. Essa situação ocorre quando uma ou mais linhas de resultado retornadas pelo índice de similaridade vetorial não atendem aos filtros adicionais.
  • A pré-filtragem geralmente ainda é um problema sem solução. Alguns bancos de dados vetoriais especializados oferecem algoritmos de pré-filtragem, mas a maioria dos bancos de dados relacionais (incluindo o ClickHouse) recorre à busca exata de vizinhos, isto é, a uma varredura por força bruta sem índice.
A estratégia usada depende da condição de filtro. Filtros adicionais fazem parte da chave de partição Se a condição de filtro adicional fizer parte da chave de partição, o ClickHouse aplicará a eliminação de partições. Por exemplo, uma tabela é particionada por intervalo pela coluna year e a seguinte consulta é executada:
O ClickHouse descartará todas as partições, exceto a de 2025. Filtros adicionais não podem ser avaliados usando índices Se condições de filtro adicionais não puderem ser avaliadas usando índices (índice de chave primária, índice de skipping), o ClickHouse aplicará pós-filtragem. Filtros adicionais podem ser avaliados usando o índice de chave primária Se condições de filtro adicionais puderem ser avaliadas usando a chave primária (ou seja, elas formam um prefixo da chave primária) e
  • se a condição de filtro eliminar pelo menos uma linha dentro de uma parte, o ClickHouse recorrerá à pré-filtragem para os intervalos “remanescentes” dentro da parte,
  • se a condição de filtro não eliminar nenhuma linha dentro de uma parte, o ClickHouse aplicará pós-filtragem à parte.
Em casos de uso práticos, este último caso é bastante improvável. Filtros adicionais podem ser avaliados usando índice de skipping Se condições de filtro adicionais puderem ser avaliadas usando índices de skipping (índice minmax, índice set etc.), o ClickHouse aplicará pós-filtragem. Nesses casos, o índice de similaridade vetorial é avaliado primeiro, pois espera-se que ele elimine mais linhas do que os outros índices de skipping. Para um controle mais preciso sobre pós-filtragem vs. pré-filtragem, duas configurações podem ser usadas: A configuração vector_search_filter_strategy (padrão: auto, que implementa as heurísticas acima) pode ser definida como prefilter. Isso é útil para forçar a pré-filtragem nos casos em que as condições de filtro adicionais são altamente seletivas. Como exemplo, a consulta a seguir pode se beneficiar da pré-filtragem:
Supondo que apenas um número muito pequeno de livros custe menos de 2 dólares, a pós-filtragem pode retornar zero linhas, porque os 10 resultados mais próximos retornados pelo índice vetorial podem ter preço acima de 2 dólares. Ao forçar a pré-filtragem (adicione SETTINGS vector_search_filter_strategy = 'prefilter' à consulta), o ClickHouse primeiro encontra todos os livros com preço inferior a 2 dólares e, em seguida, executa uma busca vetorial por força bruta nesses livros. Como abordagem alternativa para resolver o problema acima, vector_search_index_fetch_multiplier (padrão: 1.0, máximo: 1000.0) pode ser configurado com um valor > 1.0 (por exemplo, 2.0). O número de vizinhos mais próximos buscados no índice vetorial é multiplicado pelo valor dessa configuração, e então o filtro adicional é aplicado a essas linhas para retornar a quantidade de linhas definida por LIMIT. Como exemplo, podemos executar a consulta novamente, mas com o multiplicador 3.0:
O ClickHouse buscará 3,0 x 10 = 30 vizinhos mais próximos do índice vetorial em cada parte e, em seguida, avaliará os filtros adicionais. Apenas os dez vizinhos mais próximos serão retornados. Observamos que definir vector_search_index_fetch_multiplier pode mitigar o problema, mas, em casos extremos (condição WHERE muito seletiva), ainda é possível que sejam retornadas menos de N linhas solicitadas. Rescoring Os skip indexes no ClickHouse geralmente filtram no nível de grânulo, ou seja, uma busca em um skip index (internamente) retorna uma lista de grânulos com possível correspondência, o que reduz a quantidade de dados lidos na varredura subsequente. Isso funciona bem para skip indexes em geral, mas, no caso dos índices de similaridade vetorial, cria um “descompasso de granularidade”. Em mais detalhes, o índice de similaridade vetorial determina os números das linhas dos N vetores mais semelhantes para um determinado vetor de referência. Com a configuração vector_search_with_rescoring = 1, o ClickHouse lê os vetores originais com precisão total das linhas candidatas e calcula a distância final no pipeline SQL normal. Quando o plano de consulta permite, o ClickHouse filtra a varredura para as linhas candidatas retornadas pelo índice vetorial antes do cálculo final da distância. Essa etapa é chamada de rescoring e pode melhorar a precisão, especialmente com índices vetoriais quantizados, porque o ranking final usa os vetores armazenados em vez das distâncias do índice. Se filtros adicionais removerem candidatos demais ou se for necessário maior recall, aumente a configuração vector_search_index_fetch_multiplier para que o índice vetorial retorne mais linhas candidatas para rescoring. Por isso, o ClickHouse fornece uma otimização que desativa o rescoring e retorna diretamente do índice os vetores mais semelhantes e suas distâncias. Essa otimização vem habilitada por padrão; consulte a configuração vector_search_with_rescoring. Em alto nível, ela funciona assim: o ClickHouse disponibiliza os vetores mais semelhantes e suas distâncias como uma coluna virtual _distance. Para ver isso, execute uma consulta de busca vetorial com EXPLAIN header = 1:
Uma consulta executada sem rescoring (vector_search_with_rescoring = 0) e com réplicas paralelas ativadas pode voltar a usar rescoring.

Ajuste de desempenho

Ajuste da compressão Em praticamente todos os casos de uso, os vetores na coluna subjacente são densos e não se comprimem bem. Como resultado, a compressão deixa mais lentas as inserções e leituras na/da coluna de vetores. Por isso, recomendamos desativar a compressão. Para fazer isso, especifique CODEC(NONE) para a coluna de vetores assim:
Ajustando a criação de índices O ciclo de vida dos índices de similaridade vetorial está ligado ao ciclo de vida das partes. Em outras palavras, sempre que uma nova parte com um índice de similaridade vetorial definido é criada, o índice também é criado. Isso normalmente acontece quando os dados são inseridos ou durante mesclagens. Infelizmente, o HNSW é conhecido pelos longos tempos de criação de índices, o que pode tornar inserções e mesclagens significativamente mais lentas. Idealmente, os índices de similaridade vetorial devem ser usados apenas quando os dados são imutáveis ou raramente mudam. Para acelerar a criação de índices, as seguintes técnicas podem ser usadas: Primeiro, a criação de índices pode ser paralelizada. O número máximo de threads para criação de índices pode ser configurado usando a configuração do servidor max_build_vector_similarity_index_thread_pool_size. Para um desempenho ideal, esse valor deve ser configurado de acordo com o número de núcleos de CPU. Segundo, para acelerar instruções INSERT, os usuários podem desativar a criação de índices de skipping em partes recém-inseridas usando a configuração de sessão materialize_skip_indexes_on_insert. Consultas SELECT nessas partes recorrerão à busca exata. Como as partes inseridas tendem a ser pequenas em comparação com o tamanho total da tabela, espera-se que o impacto disso no desempenho seja insignificante. Terceiro, para acelerar mesclagens, os usuários podem desativar a criação de índices de skipping em partes mescladas usando a configuração de sessão materialize_skip_indexes_on_merge. Isso, em conjunto com a instrução ALTER TABLE […] MATERIALIZE INDEX […], fornece controle explícito sobre o ciclo de vida dos índices de similaridade vetorial. Por exemplo, a criação de índices pode ser adiada até que todos os dados tenham sido ingeridos ou até um período de baixa carga do sistema, como no fim de semana. Ajustando o uso de índices Consultas SELECT precisam carregar índices de similaridade vetorial na memória principal para usá-los. Para evitar que o mesmo índice de similaridade vetorial seja carregado repetidamente na memória principal, o ClickHouse fornece um cache dedicado em memória para esses índices. Quanto maior esse cache, menos carregamentos desnecessários ocorrerão. O tamanho máximo do cache pode ser configurado usando a configuração do servidor vector_similarity_index_cache_size. Por padrão, o cache pode crescer até 5 GB. As seguintes mensagens de log (system.text_log) indicam que o índice de similaridade vetorial está sendo carregado. Se essas mensagens aparecerem repetidamente em diferentes consultas de busca vetorial, isso indica que o tamanho do cache está baixo demais.
O cache do índice de similaridade vetorial armazena grânulos do índice vetorial. Se os grânulos individuais do índice vetorial forem maiores que o tamanho do cache, eles não serão armazenados em cache. Portanto, calcule o tamanho do índice vetorial (com base na fórmula em “Estimando o consumo de armazenamento e memória” ou em system.data_skipping_indices) e dimensione o cache de acordo.
Reiteramos que verificar e, se necessário, aumentar o cache do índice vetorial deve ser a primeira etapa ao investigar consultas lentas de busca vetorial. O tamanho atual do cache do índice de similaridade vetorial é exibido em system.metrics:
Os acertos e as falhas de cache de uma consulta com um determinado ID de consulta podem ser obtidos em system.query_log:
Para casos de uso em produção, recomendamos dimensionar o cache de modo que todos os índices vetoriais permaneçam na memória o tempo todo. Ajustando a quantização A quantização é uma técnica para reduzir o consumo de memória dos vetores e os custos computacionais de criar e percorrer índices vetoriais. Os índices vetoriais do ClickHouse oferecem suporte às seguintes opções de quantização: A quantização reduz a precisão das buscas vetoriais em comparação com a busca nos valores originais de ponto flutuante em precisão total (f32). No entanto, na maioria dos datasets, a quantização brain float de meia precisão (bf16) resulta em perda de precisão desprezível; por isso, os índices de similaridade vetorial usam essa técnica de quantização por padrão. A quantização em quarto de precisão (i8) e a quantização binária (b1) causam perda de precisão perceptível em buscas vetoriais. Recomendamos essas duas quantizações apenas se o tamanho do índice de similaridade vetorial for significativamente maior que a DRAM disponível. Nesse caso, também sugerimos habilitar o rescoring (vector_search_index_fetch_multiplier, vector_search_with_rescoring) para melhorar a precisão. A quantização binária é recomendada apenas para 1) embeddings normalizados (ou seja, comprimento do vetor = 1; os modelos da OpenAI geralmente são normalizados) e 2) quando a distância de cosseno é usada como função de distância. Internamente, a quantização binária usa a distância de Hamming para construir e pesquisar o grafo de proximidade. A etapa de rescoring usa os vetores originais em precisão total armazenados na tabela para identificar os vizinhos mais próximos por meio da distância de cosseno. Ajustando a transferência de dados O vetor de referência em uma consulta de busca vetorial é fornecido pelo usuário e, em geral, obtido por meio de uma chamada a um Large Language Model (LLM). Um código Python típico que executa uma busca vetorial no ClickHouse pode ser assim
Vetores de embedding (search_v no trecho acima) podem ter uma dimensionalidade muito alta. Por exemplo, a OpenAI fornece modelos que geram vetores de embedding com 1536 ou até 3072 dimensões. No código acima, o driver Python do ClickHouse substitui o vetor de embedding por uma string legível e, em seguida, envia a consulta SELECT inteiramente como uma string. Supondo que o vetor de embedding seja composto por 1536 valores de ponto flutuante de precisão simples, a string enviada chega a 20 kB de comprimento. Isso gera alto consumo de CPU para tokenização, parsing e milhares de conversões de string para float. Além disso, é necessário um espaço considerável no arquivo de log do servidor ClickHouse, o que também causa crescimento excessivo em system.query_log. Observe que a maioria dos modelos de LLM retorna um vetor de embedding como uma lista ou um array do NumPy de floats nativos. Portanto, recomendamos que aplicações Python vinculem o parâmetro do vetor de referência em formato binário usando o seguinte estilo:
No exemplo, o vetor de referência é enviado como está, em formato binário, e reinterpretado como um array de números de ponto flutuante no servidor. Isso economiza tempo de CPU no servidor e evita o aumento desnecessário dos logs do servidor e de system.query_log.

Administração e monitoramento

O tamanho em disco dos índices de similaridade vetorial pode ser consultado em system.data_skipping_indices:
Exemplo de saída:

Diferenças em relação aos índices de skipping regulares

Assim como os índices de skipping regulares, os índices de similaridade vetorial são construídos sobre grânulos, e cada bloco indexado consiste em GRANULARITY = [N] grânulos ([N] = 1 por padrão para índices de skipping normais). Por exemplo, se a granularidade do índice primário da tabela for 8192 (configuração index_granularity = 8192) e GRANULARITY = 2, então cada bloco indexado conterá 16384 linhas. No entanto, as estruturas de dados e os algoritmos para busca aproximada de vizinhos são inerentemente orientados a linhas. Eles armazenam uma representação compacta de um conjunto de linhas e também retornam linhas para consultas de busca vetorial. Isso gera algumas diferenças um tanto contraintuitivas na forma como os índices de similaridade vetorial se comportam em comparação com os índices de skipping normais. Quando um usuário define um índice de similaridade vetorial em uma coluna, o ClickHouse cria internamente um “subíndice” de similaridade vetorial para cada bloco de índice. O subíndice é “local”, no sentido de que conhece apenas as linhas do bloco de índice ao qual pertence. No exemplo anterior, supondo que uma coluna tenha 65536 linhas, obtemos quatro blocos de índice (abrangendo oito grânulos) e um subíndice de similaridade vetorial para cada bloco de índice. Em teoria, um subíndice consegue retornar diretamente as linhas com os N pontos mais próximos dentro do seu bloco de índice. Para consultas com vector_search_with_rescoring = 1, o ClickHouse pode usar essas posições de linhas para filtrar linhas antes de calcular a distância final a partir dos vetores armazenados, quando o plano de consulta permite essa otimização. Sem rescoring, o ClickHouse usa diretamente as distâncias do índice vetorial por meio da coluna virtual _distance. Ambos os modos ainda usam os intervalos de grânulos subjacentes para agendar leituras, o que difere dos índices de skipping regulares, que ignoram dados na granularidade dos blocos de índice. O parâmetro GRANULARITY determina quantos subíndices de similaridade vetorial são criados. Valores maiores de GRANULARITY significam menos subíndices de similaridade vetorial, porém maiores, até o ponto em que uma coluna (ou a parte de dados de uma coluna) tenha apenas um único subíndice. Nesse caso, o subíndice tem uma visão “global” de todas as linhas da coluna e pode retornar diretamente todos os grânulos da coluna (parte) com linhas relevantes (há no máximo LIMIT [N] desses grânulos). Com vector_search_with_rescoring = 1, o ClickHouse pode então ler as posições de linhas correspondentes e calcular a distância exata para essas linhas. Com um valor pequeno de GRANULARITY, cada subíndice pode retornar até LIMIT N linhas candidatas. Como resultado, mais linhas candidatas talvez precisem ser lidas e pós-filtradas. Observe que a precisão da busca é igualmente boa nos dois casos; apenas o desempenho do processamento difere. Em geral, recomenda-se usar um GRANULARITY alto para índices de similaridade vetorial e recorrer a valores menores de GRANULARITY apenas em caso de problemas, como consumo excessivo de memória pelas estruturas de similaridade vetorial. Se nenhum GRANULARITY tiver sido especificado para índices de similaridade vetorial, o valor padrão será 100 milhões.

Exemplo

Consultas:
Query
Response
Mais conjuntos de dados de exemplo que usam busca vetorial aproximada:

Busca vetorial com codecs quantizados

O codec Quantized é experimental. Ative-o com SET allow_experimental_codecs = 1. Se encontrar problemas, abra uma issue no repositório do ClickHouse.

Introdução

Um índice de similaridade vetorial responde a uma consulta de vizinho mais próximo percorrendo um grafo e tem desempenho muito bom quando o grafo pode ser mantido em memória. Duas condições limitam sua aplicabilidade:
  • Escala. O tempo para construir o grafo e a memória necessária para armazená-lo — além dos próprios vetores — passam a ser o custo predominante.
  • Filtragem. Com um filtro WHERE seletivo, o percurso de grafo se torna ineficaz, porque ou não consegue alcançar o pequeno conjunto de linhas que satisfaz o predicado, ou precisa inspecionar um número desproporcional de candidatos para encontrá-las.
Uma varredura exaustiva não sofre nenhuma dessas limitações: não requer nenhuma estrutura auxiliar, as partes são mescladas por concatenação, e um filtro simplesmente reduz o número de linhas a serem varridas. Sua única desvantagem é o volume de dados que precisa ler: uma varredura em vetores armazenados com precisão total Float32 é dominada pela E/S de armazenamento, porque toda a coluna de vetores precisa ser lida do disco (ou do armazenamento de objetos) — no caso de uma coluna de embedding denso, essa é a maior coluna da tabela e tem baixa taxa de compressão. O codec de coluna Quantized resolve essa desvantagem. Cada vetor é armazenado duas vezes: os valores originais em precisão total, inalterados, junto com um código quantizado compacto em um fluxo complementar. Uma consulta de busca vetorial primeiro varre os códigos usando uma função de distância de baixo custo, compatível com SIMD, para montar uma lista curta dos candidatos mais promissores e, em seguida, reclassifica essa lista com base nos vetores em precisão total. Como um código ocupa apenas uma fração do tamanho do vetor bruto, a varredura dessa lista curta lê muito menos bytes do armazenamento — e acessa a coluna em precisão total apenas para o pequeno conjunto de candidatos selecionados — enquanto o ranking final permanece preciso.

Declarando o codec

Adicione um codec Quantized(...) a uma coluna Array(Float32) (ou Array(Float64) / Array(BFloat16)). Como o codec é experimental, primeiro ative allow_experimental_codecs:
Os dados com precisão total são armazenados normalmente; o codec apenas adiciona o fluxo de códigos complementar. O codec é definido na criação da tabela e não pode ser adicionado nem alterado com ALTER TABLE.

Métodos de quantização

Cada método representa um ponto diferente no trade-off entre tamanho / precisão / métrica. O argumento dimensions é o comprimento do vetor.
  • Quantized('rabitq', dimensions) — um bit de sinal por coordenada, mais um fator de correção de cosseno sem viés (dimensions/8 + 4 bytes). Uma opção padrão forte, pequena e com baixo custo de popcount. Apenas cosineDistance.
  • Quantized('turboquant', dimensions) — dois bits por coordenada (um código MSE de 1 bit e um código residual de 1 bit) para candidatos de maior fidelidade (dimensions/4 + 4 bytes). Apenas cosineDistance.
  • Quantized('int8', dimensions) — um código Int8 por coordenada, mais a norma do vetor (dimensions + 4 bytes); é o maior, mas também o código flat mais fiel. Suporta L2Distance e cosineDistance.
  • Quantized('prefix', dimensions, leading_dimensions, 'int8'|'bf16') — Matryoshka: mantém apenas as leading_dimensions coordenadas iniciais, como Int8 (com uma escala por vetor) ou BFloat16. Códigos muito pequenos para embeddings treinados com Matryoshka Representation Learning. Suporta L2Distance e cosineDistance.
  • Quantized('product', dimensions, nbits, m) — Quantização de Produto: um codebook por partição treinado com k-means; cada vetor se torna m códigos de nbits bits (portanto, dimensions deve ser múltiplo de m). É a opção mais compacta e com maior recall por byte, ao custo de uma etapa de treinamento durante o insert. Suporta L2Distance e cosineDistance.
rabitq e turboquant exigem que dimensions seja múltiplo de 8.

Pesquisa transparente

Não há nenhuma sintaxe de consulta especial — escreva a mesma consulta top-k que você usaria na busca exata:
Com vector_search_use_quantized_codes = 1, o otimizador reescreve automaticamente a consulta em um plano de dois estágios: ele varre os códigos quantizados para montar uma lista curta e, em seguida, recalcula a pontuação dessa lista em relação ao vec com precisão total. A configuração vem desativada por padrão, portanto, sem ela, a mesma consulta é executada como uma varredura exata simples — o codec nunca altera os resultados; ele apenas oferece um caminho mais rápido quando você opta por usá-lo. Use uma função de distância compatível com o método escolhido: cosineDistance para todos os métodos e L2Distance, adicionalmente, para int8, prefix e product.

Configurações

  • allow_experimental_codecs — deve ser habilitado para declarar um codec Quantized (padrão: 0).
  • vector_search_use_quantized_codes — habilita a reescrita em dois estágios de lista curta e rescoring (padrão: 0). Quando desativada, as consultas fazem uma varredura exata dos vetores em precisão total.
  • vector_search_index_fetch_multiplier — quantos candidatos entram na lista curta em relação ao LIMIT da consulta: a varredura mantém os LIMIT × multiplier melhores códigos antes do rescoring. Valores maiores melhoram o recall, à custa de mais rescoring. O padrão é 1 (sem oversampling), portanto geralmente é necessário aumentá-lo — por exemplo, para 10 ou mais — para obter bom recall.

Projetado para escalar

O codec se encaixa bem no ClickHouse porque a parte cara — a varredura — é exatamente o que o mecanismo do ClickHouse foi projetado para fazer bem:
  • Vetorizado. Os kernels de varredura são escritos para SIMD, com encaminhamento em tempo de execução para as instruções mais largas compatíveis com a CPU: popcount em hardware para os métodos de código de sinal (rabitq, turboquant) e fused multiply-add largo para os demais.
  • Em paralelo entre núcleos e partes. Uma varredura plana é trivialmente paralela, e o ClickHouse a trata assim: as distâncias são calculadas em todas as threads disponíveis e em todas as partes de uma tabela ao mesmo tempo, com apenas a mesclagem final do top-k sendo serializada.
  • Distribuído. Em um cluster com shards, o trabalho é distribuído entre as máquinas — cada shard varre sua própria fatia em paralelo, e o coordenador mescla as listas curtas.
  • Colunar e compatível com filtros. Os códigos quantizados ocupam sua própria coluna, comprimidos e lidos pelo mesmo caminho de E/S que todas as outras colunas, de modo que um WHERE seletivo simplesmente deixa menos códigos para varrer.
  • Sem etapa de compilação separada. Os códigos são produzidos à medida que os vetores são gravados e mesclados por concatenação — não há índice para construir, ajustar ou reconstruir, então uma tabela está pronta para busca assim que seus dados chegam.
Os códigos servem apenas para gerar candidatos; a coluna de precisão total, mantida no lugar, fornece o ranking final preciso.

Quantized Bit (QBit)

Uma abordagem comum para acelerar a busca vetorial exata é usar um tipo de dado float com menor precisão. Por exemplo, se os vetores forem armazenados como Array(BFloat16) em vez de Array(Float32), o tamanho dos dados será reduzido pela metade, e espera-se que o tempo de execução das consultas diminua proporcionalmente. Esse método é conhecido como quantização. Embora acelere o processamento, ele pode reduzir a exatidão dos resultados, apesar de realizar uma varredura exaustiva de todos os vetores. Com a quantização tradicional, perdemos precisão tanto durante a busca quanto no armazenamento dos dados. No exemplo acima, armazenaríamos BFloat16 em vez de Float32, o que significa que nunca poderemos realizar uma busca mais exata depois, mesmo que isso seja desejado. Uma alternativa é armazenar duas cópias dos dados: uma quantizada e outra com precisão total. Embora isso funcione, exige armazenamento redundante. Considere um cenário em que temos Float64 como dado original e queremos executar buscas com diferentes níveis de precisão (16 bits, 32 bits ou 64 bits completos). Precisaríamos armazenar três cópias separadas dos dados. O ClickHouse oferece o tipo de dado Quantized Bit (QBit), que resolve essas limitações ao:
  1. Armazenar os dados originais com precisão total.
  2. Permitir que a precisão da quantização seja especificada em tempo de consulta.
Isso é feito armazenando os dados em um formato agrupado por bits (ou seja, todos os i-ésimos bits de todos os vetores são armazenados juntos), o que permite leituras apenas no nível de precisão solicitado. Assim, você obtém os benefícios de velocidade da redução de E/S e do processamento proporcionados pela quantização, ao mesmo tempo em que mantém todos os dados originais disponíveis quando necessário. Quando a precisão máxima é selecionada, a busca se torna exata. Para declarar uma coluna do tipo QBit, use a seguinte sintaxe:
Onde:
  • element_type – o tipo de cada elemento do vetor. Os tipos aceitos são Int8, BFloat16, Float32 e Float64
  • dimension – o número de elementos em cada vetor
  • stride – opcional. Um divisor de dimension que particiona as dimensões em dimension / stride grupos contíguos armazenados em streams separados, de modo que uma busca apenas nas dimensões iniciais leia menos streams (útil para embeddings Matryoshka). O padrão é dimension, caso em que o tipo é idêntico em bytes a um QBit sem stride. Consulte a página do tipo de dado QBit para mais detalhes.

Criando uma tabela QBit e adicionando dados

Vamos encontrar os vizinhos mais próximos de um vetor que representa a palavra ‘lemon’ usando a distância L2. O terceiro parâmetro da função de distância especifica a precisão em bits — valores mais altos oferecem mais exatidão, mas exigem mais processamento. Você pode encontrar todas as funções de distância disponíveis para QBit aqui. Busca com precisão total (64 bits):
Busca de precisão reduzida:
Observe que, com a quantização de 12 bits, obtemos uma boa aproximação das distâncias, com execução da consulta mais rápida. A ordenação relativa permanece em grande parte consistente, com ‘apple’ ainda sendo a correspondência mais próxima.

Considerações de desempenho

O ganho de desempenho do QBit vem da redução das operações de E/S, já que menos dados precisam ser lidos do armazenamento ao usar uma precisão menor. Além disso, quando o QBit contém dados Float32, se o parâmetro de precisão for 16 ou menos, há ganhos adicionais com a redução do processamento. O parâmetro de precisão controla diretamente o equilíbrio entre exatidão e velocidade:
  • Maior precisão (mais próxima da largura original dos dados): Resultados mais exatos, consultas mais lentas
  • Menor precisão: Consultas mais rápidas com resultados aproximados e menor uso de memória

Referências

Blog:
Última modificação em 23 de julho de 2026