Pular para o conteúdo principal
Os índices de texto no ClickHouse (também conhecidos como “índices invertidos”) oferecem recursos rápidos de busca por texto completo em dados do tipo string. O índice mapeia cada token da coluna para as linhas que contêm esse token. Os tokens são gerados por um processo chamado tokenização. Por exemplo, por padrão, o ClickHouse tokeniza a frase em inglês “All cat like mice.” como [“All”, “cat”, “like”, “mice”] (observe que o ponto final é ignorado). Tokenizers mais avançados estão disponíveis, por exemplo, para dados de log.

Criando um índice de texto

Para criar um índice de texto, primeiro ative a configuração experimental correspondente:
Um índice de texto pode ser definido em uma coluna do tipo String, FixedString, Array(String), Array(FixedString) e Map (por meio das funções de Map mapKeys e mapValues), usando a seguinte sintaxe:
Argumento tokenizer. O argumento tokenizer especifica o tokenizer:
  • splitByNonAlpha divide strings em caracteres ASCII não alfanuméricos (veja também a função splitByNonAlpha).
  • splitByString(S) divide strings com base em determinadas strings separadoras S definidas pelo usuário (veja também a função splitByString). Os separadores podem ser especificados usando um parâmetro opcional, por exemplo, tokenizer = splitByString([', ', '; ', '\n', '\\']). Observe que cada string pode ser composta por vários caracteres (', ' no exemplo). A lista padrão de separadores, se não for especificada explicitamente (por exemplo, tokenizer = splitByString), é um único espaço em branco [' '].
  • ngrams(N) divide strings em N-gramas de mesmo tamanho (veja também a função ngrams). O comprimento do ngram pode ser especificado usando um parâmetro inteiro opcional entre 2 e 8, por exemplo, tokenizer = ngrams(3). O tamanho padrão do ngram, se não for especificado explicitamente (por exemplo, tokenizer = ngrams), é 3.
  • array não realiza tokenização, ou seja, cada valor de uma linha é um token (veja também a função array).
  • sparseGrams(min_length, max_length, min_cutoff_length) — usa o mesmo algoritmo da função sparseGrams para dividir uma string em todos os ngrams de min_length e em vários ngrams maiores, até max_length, inclusive. Se min_cutoff_length for especificado, somente N-gramas com comprimento maior ou igual a min_cutoff_length serão salvos no índice. Diferentemente de ngrams(N), que gera apenas N-gramas de comprimento fixo, sparseGrams produz um conjunto de N-gramas de comprimento variável dentro do intervalo especificado, permitindo uma representação mais flexível do contexto do texto. Por exemplo, tokenizer = sparseGrams(3, 5, 4) gerará 3-, 4- e 5-gramas a partir da string de entrada e salvará apenas os 4- e 5-gramas no índice.
O tokenizer splitByString aplica os separadores de divisão da esquerda para a direita. Isso pode criar ambiguidades. Por exemplo, as strings separadoras ['%21', '%'] farão com que %21abc seja tokenizado como ['abc'], enquanto inverter a ordem dessas duas strings separadoras para ['%', '%21'] produzirá ['21abc']. Na maioria dos casos, o ideal é que a correspondência priorize primeiro os separadores mais longos. Em geral, isso pode ser feito passando as strings separadoras em ordem decrescente de comprimento. Se as strings separadoras formarem um código de prefixo, elas podem ser passadas em qualquer ordem.
No momento, não é recomendável criar índices de texto sobre textos em idiomas não ocidentais, por exemplo, chinês. Os tokenizers atualmente compatíveis podem levar a tamanhos de índice enormes e tempos de consulta elevados. Planejamos adicionar no futuro tokenizers especializados por idioma, que lidarão melhor com esses casos.
Para testar como os tokenizers dividem o texto de entrada, você pode usar a função tokens do ClickHouse: Por exemplo,
retorna
Argumento preprocessor. O argumento opcional preprocessor é uma expression que transforma a string de entrada antes da tokenização. Os casos de uso típicos do argumento preprocessor incluem
  1. Converter as strings de entrada em minúsculas (ou maiúsculas) para permitir correspondência sem diferenciar maiúsculas de minúsculas, por exemplo, lower, lowerUTF8; veja o primeiro exemplo abaixo.
  2. Normalização UTF-8, por exemplo, normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, toValidUTF8.
  3. Remover ou transformar caracteres ou substrings indesejados, por exemplo, extractTextFromHTML, substring, idnaEncode.
A expressão do preprocessor deve transformar um valor de entrada do tipo String ou FixedString em um valor do mesmo tipo. Exemplos:
  • INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))
  • INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = substringIndex(col, '\n', 1))
  • INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(extractTextFromHTML(col))
Além disso, a expressão do preprocessor deve referenciar apenas a coluna sobre a qual o índice de texto foi definido. Não é permitido usar funções não determinísticas. As funções hasToken, hasAllTokens e hasAnyTokens usam o preprocessor para primeiro transformar o termo de busca antes de tokenizá-lo. Por exemplo:
equivale a:
Outros argumentos. Os índices de texto no ClickHouse são implementados como índices secundários. No entanto, ao contrário de outros índices de skipping, os índices de texto têm uma GRANULARITY padrão de 64. Esse valor foi definido empiricamente e oferece um bom equilíbrio entre velocidade e tamanho do índice para a maioria dos casos de uso. Usuários avançados podem especificar uma granularidade de índice diferente (não recomendamos isso).
Os valores padrão dos parâmetros avançados a seguir funcionam bem em praticamente todas as situações. Não recomendamos alterá-los.O parâmetro opcional dictionary_block_size (padrão: 128) especifica o tamanho dos blocos do dicionário em linhas.O parâmetro opcional dictionary_block_frontcoding_compression (padrão: 1) especifica se os blocos do dicionário usam front coding para compressão.O parâmetro opcional max_cardinality_for_embedded_postings (padrão: 16) especifica o limite de cardinalidade abaixo do qual as posting lists devem ser incorporadas aos blocos do dicionário.O parâmetro opcional bloom_filter_false_positive_rate (padrão: 0.1) especifica a taxa de falso positivo do filtro de Bloom do dicionário.
Índices de texto podem ser adicionados a uma coluna ou removidos dela depois que a tabela for criada:

Usando um índice de texto

Usar um índice de texto em consultas SELECT é simples, pois funções comuns de pesquisa em strings usarão o índice automaticamente. Se não existir nenhum índice, as funções de pesquisa em strings abaixo recorrerão a varreduras lentas por força bruta.

Funções suportadas

O índice de texto pode ser usado quando funções de texto são usadas na cláusula WHERE de uma consulta SELECT:

= and !=

= (equals) and != (notEquals ) correspondem exatamente ao termo de busca fornecido. Exemplo:
O índice de texto oferece suporte a = e !=, mas a busca por igualdade e desigualdade só faz sentido com o tokenizer array (o que faz com que o índice armazene os valores completos da linha).

IN and NOT IN

IN (in) e NOT IN (notIn) são semelhantes às funções equals e notEquals, mas correspondem a todos (IN) ou a nenhum (NOT IN) dos termos de busca. Exemplo:
Aplicam-se as mesmas restrições de = e !=; ou seja, IN e NOT IN só fazem sentido em conjunto com o tokenizer array.

LIKE, NOT LIKE e match

Atualmente, essas funções usam o índice de texto para filtragem somente se o tokenizer do índice for splitByNonAlpha ou ngrams.
Para usar LIKE like, NOT LIKE (notLike) e a função match com índices de texto, o ClickHouse precisa conseguir extrair tokens completos do termo de pesquisa. Exemplo:
support no exemplo pode corresponder a support, supports, supporting etc. Esse tipo de consulta é uma consulta de substring e não pode ser acelerada por um índice de texto. Para usar um índice de texto em consultas LIKE, o padrão do LIKE deve ser reescrito da seguinte forma:
Os espaços à esquerda e à direita de support garantem que o termo possa ser extraído como um token.

startsWith and endsWith

Assim como LIKE, as funções startsWith e endsWith só podem usar um índice de texto se for possível extrair tokens completos do termo de pesquisa. Exemplo:
No exemplo, apenas clickhouse é considerado um token. support não é considerado um token porque pode corresponder a support, supports, supporting etc. Para encontrar todas as linhas que começam com clickhouse supports, termine o padrão de pesquisa com um espaço no final:
Da mesma forma, endsWith deve ser usado com um espaço à esquerda:

hasToken and hasTokenOrNull

As funções hasToken e hasTokenOrNull fazem a correspondência com um único token fornecido. Ao contrário das funções mencionadas anteriormente, elas não tokenizam o termo de busca (presumem que a entrada seja um único token). Exemplo:
As funções hasToken e hasTokenOrNull oferecem o melhor desempenho para uso com o índice text.

hasAnyTokens and hasAllTokens

As funções hasAnyTokens e hasAllTokens fazem correspondência com um ou com todos os tokens fornecidos. Essas duas funções aceitam os tokens de busca como uma string, que será tokenizada usando o mesmo tokenizer usado na coluna indexada, ou como um array de tokens já processados, aos quais não será aplicada nenhuma tokenização antes da busca. Consulte a documentação da função para mais informações. Exemplo:

has

A função de Array has faz a correspondência com um único token em um array de strings. Exemplo:

mapContains

A função mapContains(sinônimo de: mapContainsKey) faz correspondência com um único token nas chaves de um map. Exemplo:

operator[]

O operator[] de acesso pode ser usado com o índice de texto para filtrar chaves e valores. Exemplo:
Veja os exemplos a seguir de uso de Array(T) e Map(K, V) com o índice de texto.

Exemplos de suporte a Array e Map no índice de texto.

Indexação de Array(String)

Em uma plataforma simples de blogs, os autores atribuem palavras-chave às suas postagens para categorizar o conteúdo. Um recurso comum permite que os usuários descubram conteúdo relacionado clicando em palavras-chave ou pesquisando tópicos. Considere esta definição de tabela:
Sem um índice de texto, encontrar posts com uma palavra-chave específica (por exemplo, clickhouse) exige varrer todas as entradas:
À medida que a plataforma cresce, isso se torna cada vez mais lento, porque a consulta precisa examinar cada array de palavras-chave em cada linha. Para contornar esse problema de desempenho, podemos definir um índice de texto para keywords, que cria uma estrutura otimizada para pesquisa, pré-processando todas as palavras-chave e permitindo buscas instantâneas:
Importante: depois de adicionar o índice de texto, você precisa reconstruí-lo para os dados existentes:

Indexação de map

Em um sistema de logging, as solicitações do servidor frequentemente armazenam metadados em pares chave-valor. As equipes de operações precisam pesquisar com eficiência nos logs para depuração, incidentes de segurança e monitoramento. Considere esta tabela de logs:
Sem um índice de texto, pesquisar em dados Map exige varreduras completas da tabela:
  1. Encontra todos os logs com limitação de taxa:
  1. Encontra todos os logs de um IP específico:
À medida que o volume de logs aumenta, essas consultas ficam lentas. A solução é criar um índice de texto para as chaves e os valores do Map. Use mapKeys para criar um índice de texto quando precisar localizar logs por nomes de campos ou tipos de atributos:
Use mapValues para criar um índice de texto quando precisar pesquisar no conteúdo em si dos atributos:
Importante: após adicionar o índice de texto, você precisa recriá-lo para os dados existentes:
  1. Encontre todas as solicitações com taxa limitada:
  1. Encontre todos os logs de um IP específico:

Implementação

Layout do índice

Cada índice de texto consiste em duas estruturas de dados (abstratas):
  • um dicionário que associa cada token a uma lista de postings; e
  • um conjunto de listas de postings, cada uma representando um conjunto de números de linha.
Como um índice de texto é um skip index, essas estruturas de dados existem logicamente por grânulo de índice. Durante a criação do índice, três arquivos são criados (por part): Arquivo de blocos do dicionário (.dct) Os tokens em um grânulo de índice são ordenados e armazenados em blocos de dicionário de 128 tokens cada (o tamanho do bloco é configurável pelo parâmetro dictionary_block_size). Um arquivo de blocos do dicionário (.dct) contém todos os blocos de dicionário de todos os grânulos de índice em uma part. Arquivo de grânulos de índice (.idx) O arquivo de grânulos de índice contém, para cada bloco de dicionário, o primeiro token do bloco, seu deslocamento relativo no arquivo de blocos do dicionário e um filtro de Bloom para todos os tokens do bloco. Essa estrutura de índice esparso é semelhante ao índice esparso de chave primária) do ClickHouse. O filtro de Bloom permite ignorar blocos de dicionário logo no início se o token procurado não estiver presente em um bloco de dicionário. Arquivo de listas de postings (.pst) As listas de postings de todos os tokens são organizadas sequencialmente no arquivo de listas de postings. Para economizar espaço e ainda permitir operações rápidas de interseção e união, as listas de postings são armazenadas como bitmaps Roaring. Se a cardinalidade de uma lista de postings for menor que 16 (configurável pelo parâmetro max_cardinality_for_embedded_postings), ela é incorporada ao dicionário.

Leitura direta

Certos tipos de consultas de texto podem ser acelerados significativamente por uma otimização chamada “leitura direta”. Mais especificamente, a otimização pode ser aplicada se a consulta SELECT não incluir a coluna de texto na projeção. Exemplo:
A otimização de leitura direta no ClickHouse responde à consulta exclusivamente usando o índice de texto (isto é, consultas ao índice de texto), sem acessar a coluna de texto subjacente. As consultas ao índice de texto leem relativamente poucos dados e, por isso, são muito mais rápidas do que os skip indexes usuais no ClickHouse (que fazem uma consulta ao skip index, seguida do carregamento e da filtragem dos grânulos restantes). A leitura direta é controlada por duas configurações:
  • Configuração query_plan_direct_read_from_text_index (padrão: 1), que especifica se a leitura direta está habilitada de modo geral.
  • Configuração use_skip_indexes_on_data_read (padrão: 1), que é outro pré-requisito para a leitura direta. Observe que, em bancos de dados ClickHouse com compatibility < 25.10, use_skip_indexes_on_data_read fica desabilitada, portanto você precisa aumentar o valor da configuração de compatibility ou definir SET use_skip_indexes_on_data_read = 1 explicitamente.
Além disso, o índice de texto deve estar totalmente materializado para usar a leitura direta (use ALTER TABLE ... MATERIALIZE INDEX para isso). Funções suportadas A otimização de leitura direta oferece suporte às funções hasToken, hasAllTokens e hasAnyTokens. Essas funções também podem ser combinadas com os operadores AND, OR e NOT. A cláusula WHERE também pode conter filtros adicionais que não sejam funções de pesquisa de texto (para colunas de texto ou outras colunas) — nesse caso, a otimização de leitura direta ainda será usada, mas será menos eficaz (ela se aplica apenas às funções de pesquisa de texto compatíveis). Para verificar se uma consulta usa leitura direta, execute a consulta com EXPLAIN PLAN actions = 1. Como exemplo, uma consulta com a leitura direta desabilitada
retorna
enquanto a mesma consulta é executada com query_plan_direct_read_from_text_index = 1
retorna
A segunda saída de EXPLAIN PLAN contém uma coluna virtual __text_index_<index_name>_<function_name>_<id>. Se essa coluna estiver presente, a leitura direta estará sendo usada.

Exemplo: conjunto de dados do Hacker News

Vamos analisar os ganhos de desempenho dos índices de texto em um grande conjunto de dados com muito conteúdo textual. Usaremos 28,7 milhões de linhas de comentários do popular site Hacker News. Aqui está a tabela sem índice de texto:
As 28,7 milhões de linhas estão em um arquivo Parquet no S3 — vamos inseri-las na tabela hackernews:
Usaremos ALTER TABLE para adicionar um índice de texto à coluna comment e, em seguida, materializá-lo:
Agora, vamos executar consultas usando as funções hasToken, hasAnyTokens e hasAllTokens. Os exemplos a seguir mostrarão a grande diferença de desempenho entre uma varredura de índice padrão e a otimização de leitura direta.

1. Usando hasToken

hasToken verifica se o texto contém um token específico. Vamos procurar pelo token sensível a maiúsculas e minúsculas ‘ClickHouse’. Leitura direta desabilitada (varredura padrão) Por padrão, o ClickHouse usa o skip index para filtrar grânulos e, em seguida, lê os dados da coluna desses grânulos. Podemos simular esse comportamento desabilitando a leitura direta.
Leitura direta ativada (leitura rápida do índice) Agora executamos a mesma consulta com a leitura direta ativada (comportamento padrão).
A consulta usando leitura direta é mais de 45 vezes mais rápida (0,362s vs 0,008s) e processa significativamente menos dados (9,51 GB vs 3,15 MB) ao ler apenas o índice.

2. Usando hasAnyTokens

hasAnyTokens verifica se o texto contém pelo menos um dos tokens informados. Vamos procurar comentários que contenham ‘love’ ou ‘ClickHouse’. Leitura direta desativada (varredura padrão)
Leitura direta habilitada (leitura rápida pelo índice)
O ganho de velocidade é ainda mais expressivo para esta busca comum com “OR”. A consulta é quase 89 vezes mais rápida (1.329s vs 0.015s) ao evitar a varredura completa da coluna.

3. Usando hasAllTokens

hasAllTokens verifica se o texto contém todos os tokens informados. Vamos buscar comentários que contenham tanto ‘love’ quanto ‘ClickHouse’. Leitura direta desativada (varredura padrão) Mesmo com a leitura direta desativada, o skip index padrão continua eficaz. Ele reduz as 28.7M linhas para apenas 147.46K, mas ainda precisa ler 57.03 MB da coluna.
Leitura direta ativada (Leitura rápida do índice) A leitura direta responde à consulta com base nos dados do índice, lendo apenas 147.46 KB.
Para esta pesquisa “AND”, a otimização de leitura direta é mais de 26 vezes mais rápida (0.184s vs 0.007s) do que a varredura padrão com skip index.

4. Busca composta: OR, AND, NOT, …

A otimização de leitura direta também se aplica a expressões booleanas compostas. Aqui, faremos uma busca sem diferenciar maiúsculas de minúsculas por ‘ClickHouse’ OR ‘clickhouse’. Leitura direta desabilitada (varredura padrão)
Leitura direta ativada (Leitura rápida do índice)
Ao combinar os resultados do índice, a consulta com leitura direta fica 34 vezes mais rápida (0,450s vs 0,013s) e evita a leitura de 9,58 GB de dados das colunas. Para este caso específico, hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) seria a sintaxe preferida e mais eficiente.

Ajuste do índice de texto

Atualmente, há caches para os blocos de dicionário desserializados, os cabeçalhos e as listas de postings do índice de texto, para reduzir a E/S. Eles podem ser habilitados por meio das configurações use_text_index_dictionary_cache, use_text_index_header_cache e use_text_index_postings_cache, respectivamente. Por padrão, eles ficam desabilitados. Consulte as configurações de servidor a seguir para configurar o cache.

Configurações do servidor

Configurações de cache de blocos do dicionário do índice de texto

Configurações do cache de cabeçalhos

Configurações do cache das listas de postings

Última modificação em 25 de junho de 2026