Criando um índice de texto
Os índices de texto podem ser usados com qualquer versão do ClickHouse >= 26.2, independentemente da configuração de compatibilidade.
Query
- String e FixedString,
- Array(String) e Array(FixedString),
- Map (por meio das funções mapKeys e mapValues), e
- JSON (por meio das funções JSONAllPaths e
JSONAllValues).
Array(Nullable(String or FixedString)).
Como alternativa, para adicionar um índice de texto a uma tabela existente:
Query
Query
Query
tokenizer especifica o tokenizador:
splitByNonAlphadivide strings em caracteres ASCII não alfanuméricos (consulte a função splitByNonAlpha).splitByString(S)divide strings usando determinadas strings separadorasSdefinidas pelo usuário (consulte 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[' '].asciiCJKdivide strings em tokens usando regras de limite de palavras do Unicode (semelhantes a Unicode Text Segmentation (UAX #29)). Caracteres ASCII alfanuméricos e sublinhados formam tokens com conectores (ASCII:para letras,.e'para caracteres do mesmo tipo). Caracteres Unicode não ASCII, incluindo caracteres CJK, tornam-se tokens de um único caractere.ngrams(N)divide strings em n-grams de tamanho fixoN(consulte a função ngrams). O comprimento do ngram pode ser especificado usando um parâmetro inteiro opcional entre 1 e 8; por exemplo,tokenizer = ngrams(3). O tamanho padrão do ngram, se não for especificado explicitamente (por exemplo,tokenizer = ngrams), é 3.sparseGrams(min_length, max_length, min_cutoff_length)divide strings em n-grams de comprimento variável com no mínimomin_lengthe no máximomax_lengthcaracteres (inclusive) (consulte a função sparseGrams). A menos que sejam especificados explicitamente,min_lengthemax_lengthassumem, por padrão, os valores 3 e 100. Se o parâmetromin_cutoff_lengthfor fornecido, somente n-grams com comprimento maior ou igual amin_cutoff_lengthserão retornados. Em comparação comngrams(N), o tokenizadorsparseGramsproduz N-grams de comprimento variável, permitindo uma representação mais flexível do texto original. Por exemplo,tokenizer = sparseGrams(3, 5, 4)gera internamente 3-, 4- e 5-grams a partir da string de entrada, mas apenas os 4- e 5-grams são retornados.arraynão realiza tokenização, ou seja, cada valor da linha é um token (consulte a função array).
O tokenizador
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 as duas strings separadoras para ['%', '%21'] produzirá ['21abc'].
Na maioria dos casos, convém que a correspondência dê preferência primeiro aos separadores mais longos.
Em geral, isso pode ser feito passando as strings separadoras em ordem decrescente de comprimento.
Se as strings separadoras formarem um prefix code, elas poderão ser passadas em qualquer ordem.Query
Response
asciiCJK, pois ele lida corretamente com os limites de palavras em Unicode, incluindo caracteres CJK.
:::
Argumento do preprocessador (opcional). O preprocessador refere-se a uma expressão aplicada à string de entrada antes da tokenização.
Casos de uso típicos do argumento do preprocessador incluem
- Conversão para minúsculas/maiúsculas, ou case folding para permitir correspondência sem diferenciar maiúsculas de minúsculas, por exemplo, lower, lowerUTF8, caseFoldUTF8.
- Normalização UTF-8, por exemplo, normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, normalizeUTF8NFKCCasefold, toValidUTF8.
- Remoção ou transformação de caracteres ou substrings indesejados, como acentos, por exemplo, extractTextFromHTML, substring, idnaEncode, translate, removeDiacriticsUTF8.
Nullable(T) ou LowCardinality(T), então a expressão do pré-processador deve aceitar valores anuláveis ou de baixa cardinalidade (ou seja, sem lançar uma exceção).
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)))INDEX idx(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = removeDiacriticsUTF8(caseFoldUTF8(col)))
INDEX idx(lower(col)) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = upper(lower(col)))INDEX idx(lower(col)) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(lower(col), lower(col)))- Não é permitido:
INDEX idx(lower(col)) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(col, col))
Query
Query
Query
Query
Query
Response
Usando um índice de texto
Recomendamos usar as funções
hasAnyTokens e hasAllTokens para pesquisar no índice de texto; consulte abaixo.
Essas funções funcionam com todos os tokenizadores disponíveis e todas as expressões de pré-processamento possíveis.
Como as outras funções compatíveis surgiram historicamente antes do índice de texto, elas precisaram manter seu comportamento legado em muitos casos (por exemplo, sem suporte a pré-processamento).Funções suportadas
WHERE ou nas cláusulas PREWHERE:
=
= (equals) corresponde ao termo de busca informado por completo.
Exemplo:
IN
IN (in) é semelhante a equals, mas faz correspondência com todos os termos pesquisados.
Exemplo:
NOT IN (notIn) não é suportado pelo índice de texto.LIKE and match
Atualmente, essas funções usam o índice de texto para filtragem apenas se o tokenizador do índice for
splitByNonAlpha, ngrams ou sparseGrams.NOT LIKE (notLike) não é compatível com o índice de texto.LIKE (like) e a função match com índices de texto, o ClickHouse precisa conseguir extrair tokens completos do termo de busca.
No caso do índice com o tokenizador ngrams, isso acontece se o comprimento das strings pesquisadas entre caracteres curinga for igual ou maior que o comprimento do ngram.
Exemplo de índice de texto com o tokenizador splitByNonAlpha:
support no exemplo pode corresponder a support, supports, supporting etc.
Esse tipo de consulta é uma consulta de substring e não pode ser acelerada com um índice de texto.
Para usar um índice de texto em consultas LIKE, o padrão LIKE deve ser reescrito da seguinte forma:
support garantem que o termo possa ser extraído como um token.
Felizmente, há um caso especial em que o ClickHouse pode aproveitar o índice invertido para acelerar significativamente consultas LIKE.
Consulte a seção sobre otimização de desempenho de LIKE/ILIKE para mais detalhes.
startsWith and endsWith
LIKE, as funções startsWith e endsWith só podem usar um índice de texto se for possível extrair tokens completos do termo de busca.
No índice com o tokenizer ngrams, isso acontece quando o comprimento das Strings pesquisadas entre caracteres curinga é igual ou maior que o comprimento do ngram.
Exemplo de índice de texto com o tokenizer splitByNonAlpha:
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 busca com um espaço no final:
endsWith deve ser usado com um espaço no início:
hasToken e hasTokenOrNull
A função
hasToken parece simples de usar, mas tem algumas limitações com tokenizadores não padrão e expressões de pré-processamento.
Recomendamos usar as funções hasAnyTokens e hasAllTokens.hasAnyTokens and hasAllTokens
hasPhrase
hasAllTokens, que exige apenas que todos os tokens estejam presentes em algum ponto, hasPhrase exige que eles apareçam em sequência.
A frase de busca é tokenizada usando o mesmo tokenizador configurado para a coluna de índice.
Observe que a função requer um dos tokenizadores splitByNonAlpha, splitByString, ngrams ou asciiCJK.
Exemplo:
has
hasAny e hasAll
mapContains
mapContainsKey) faz a correspondência com os tokens extraídos da string pesquisada nas chaves de um map.
O comportamento é semelhante ao da função equals com uma coluna String.
O índice de texto é usado apenas se tiver sido criado em uma expressão mapKeys(map).
Exemplo:
mapContainsValue
equals com uma coluna String.
O índice de texto só é usado se tiver sido criado em uma expressão mapValues(map).
Exemplo:
mapContainsKeyLike and mapContainsValueLike
operator[]
mapKeys(map) ou mapValues(map), ou em ambas.
Exemplo:
Array(T) e Map(K, V) com o índice de texto.
Indexação de colunas Array(String)
clickhouse) exige a varredura de todos os registros:
keywords em cada linha.
Para contornar esse problema de desempenho, definimos um índice de texto para a coluna keywords:
Indexação de colunas Map
Indexação de colunas JSON
JSON de três maneiras:
- Índices em subcolunas específicas — crie um índice de texto em um caminho JSON conhecido, assim como em uma coluna comum. Isso indexa os valores nesse caminho.
- Índices baseados em caminhos com JSONAllPaths — indexam todos os caminhos presentes em cada grânulo para ignorar grânulos que não podem conter o caminho consultado. Semelhante ao que ocorre com colunas
Map. - Índices baseados em valores com JSONAllValues — indexam todos os valores em todos os caminhos JSON para acelerar a busca de texto completo em qualquer subcoluna JSON com um único índice.
Índices em subcolunas específicas
- Caminho tipado declarado no type hint de JSON — acesse-o diretamente pelo nome:
json.a. - Caminho dinâmico com cast explícito — use a sintaxe de cast
:::json.b::String.
Query
Query
Response
Query
Response
Índices baseados em caminhos com JSONAllPaths
Map, é possível criar índices de texto em colunas JSON usando JSONAllPaths.
O índice armazena o conjunto de caminhos JSON presentes em cada grânulo e os utiliza para ignorar grânulos nos quais o caminho consultado está ausente.
Definição de exemplo do índice:
Query
EXPLAIN indexes = 1 para verificar se o skip index está sendo usado.
Quando um caminho existe apenas em uma parte, o índice ignora a outra parte.
Exemplo:
Query
Response
Query
Response
IS NOT NULL também usa o índice — ele ignora os grânulos em que o caminho não existe (já que o valor seria NULL):
Exemplo:
Query
Response
Índices baseados em valores com JSONAllValues
JSONAllValues.
JSONAllValues retorna todos os valores de uma coluna JSON como Array(String).
Valores de tipos de dados que não são string (por exemplo, inteiros e arrays) são convertidos para sua representação em texto.
Um índice de texto criado com JSONAllValues indexa essas representações textuais em todos os caminhos JSON de cada linha.
Esse índice pode então acelerar consultas que filtram subcolunas JSON específicas.
Quando uma consulta filtra uma subcoluna específica (por exemplo, data.user_name = 'alice'), o índice de texto pode rapidamente ignorar linhas (e grânulos) que não contêm os tokens pesquisados em nenhum de seus valores JSON.
O índice pode gerar falsos positivos quando caminhos JSON diferentes contêm os mesmos tokens.
Por exemplo, se a linha 1 tiver
{"a": "hello", "b": "world"} e uma consulta procurar por data.a = 'world', o índice de texto não consegue distinguir que world pertence ao caminho b, e não a a.
Nesses casos, o índice não ignorará a linha, e o filtro nos dados reais da coluna fará a avaliação final.
Esse é o mesmo comportamento de outros casos de uso de índice de texto, em que o índice atua como um pré-filtro rápido.Criando o índice
Padrões de consulta suportados
String e a função equals para todas as colunas.
Acesso à subcoluna:
CAST explícito:
IN:
Busca por frase
hasPhrase.
Todos os tokens da frase devem aparecer de forma consecutiva e na mesma ordem no documento.
O índice de texto acelera a busca por frase ao intersectar as lista de postings de todos os tokens da frase para identificar grânulos candidatos.
Dentro desses grânulos, o ClickHouse então verifica a adjacência exata entre os tokens.
hasPhrase tem suporte com os tokenizadores splitByNonAlpha, splitByString, ngrams e asciiCJK.
A frase é tokenizada usando o tokenizador configurado no índice.
Os caracteres separadores do tokenizador na frase são ignorados: hasPhrase(text, 'quick+brown') é equivalente a hasPhrase(text, 'quick brown') para o tokenizador splitByNonAlpha.
Exemplo
Query
Query
Response
'New weather in York') não corresponde porque os tokens estão na ordem incorreta.
A linha 3 ('weather in New Orleans') não corresponde porque não contém o token 'York'.
Otimização de desempenho
Leitura direta
- A configuração query_plan_direct_read_from_text_index (
truepor padrão) especifica se a leitura direta está habilitada de modo geral. - A configuração use_skip_indexes_on_data_read era um pré-requisito para a leitura direta em versões do ClickHouse < 26.4.
hasToken, hasAllTokens e hasAnyTokens.
Se o índice de texto for definido com um tokenizer array, a leitura direta também terá suporte para as funções equals, has, hasAny, hasAll, mapContainsKey e mapContainsValue.
Essas funções também podem ser combinadas com os operadores AND, OR e NOT.
As cláusulas WHERE ou PREWHERE também podem conter filtros adicionais de funções que não sejam de busca 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 busca de texto compatíveis).
Para verificar se uma consulta utiliza leitura direta, execute a consulta com EXPLAIN PLAN actions = 1.
Como exemplo, uma consulta com a leitura direta desabilitada
query_plan_direct_read_from_text_index = 1
__text_index_<index_name>_<function_name>_<id>.
Se essa coluna estiver presente, a leitura direta será usada.
Se a cláusula de filtro WHERE contiver apenas funções de busca de texto, a consulta poderá evitar completamente a leitura dos dados da coluna e obter o maior ganho de desempenho com a leitura direta.
No entanto, mesmo que a coluna de texto seja acessada em outra parte da consulta, a leitura direta ainda proporcionará melhoria de desempenho.
Leitura direta como hint
A leitura direta como hint se baseia nos mesmos princípios da leitura direta normal, mas adiciona um filtro extra construído a partir dos dados do índice de texto, sem remover a coluna de texto subjacente.
Ela é usada para funções em que ler apenas do índice de texto produziria falsos positivos.
As funções compatíveis são: like, startsWith, endsWith, equals, has, hasPhrase, mapContainsKey e mapContainsValue.
O filtro adicional pode oferecer seletividade extra para restringir ainda mais o conjunto de resultados em combinação com outros filtros, ajudando a reduzir a quantidade de dados lidos de outras colunas.
A leitura direta como hint é controlada pela configuração query_plan_text_index_add_hint (ativada por padrão).
Exemplo de consulta sem hint:
query_plan_text_index_add_hint = 1
__text_index_...) foi adicionado à condição de filtro.
Graças à otimização PREWHERE, a condição de filtro é dividida em três termos separados, aplicados em ordem crescente de complexidade computacional.
Para esta consulta, a ordem de aplicação é __text_index_..., depois greaterOrEquals(...) e, por fim, like(...).
Essa ordenação permite ignorar ainda mais grânulos de dados do que os já ignorados pelo índice de texto e pelo filtro original, antes da leitura das colunas pesadas usadas na consulta após a cláusula WHERE, reduzindo ainda mais a quantidade de dados a ser lida.
Consultas LIKE/ILIKE
%<caracteres-alfanuméricos-sem-espaços>% e o tokenizer do índice de texto é splitByNonAlpha ou array, o ClickHouse usa o índice invertido para acelerar significativamente as consultas LIKE/ILIKE. Para isso, o ClickHouse examina o dicionário do índice invertido em vez de fazer uma varredura completa da tabela para encontrar o padrão correspondente.
Quando a otimização está habilitada, as consultas LIKE/ILIKE devem ser significativamente mais rápidas do que uma varredura completa da tabela. No entanto, quando o padrão corresponde à maioria dos tokens do dicionário, o desempenho pode ser pior do que em uma varredura completa da tabela. Felizmente, há um mecanismo de fallback para evitar isso.
A otimização é controlada por uma configuração:
O mecanismo de fallback é controlado por duas configurações:
Essa otimização é compatível apenas com as funções like e ilike.
Cache
Configurações do cache de tokens
| Configuração | Descrição |
|---|---|
| text_index_tokens_cache_policy | Nome da política do cache de tokens do índice de texto. |
| text_index_tokens_cache_size | Tamanho máximo do cache em bytes. |
| text_index_tokens_cache_max_entries | Número máximo de tokens desserializados no cache. |
| text_index_tokens_cache_size_ratio | Tamanho da fila protegida no cache de tokens do índice de texto em relação ao tamanho total do cache. |
Configurações de cache do cabeçalho
| Configuração | Descrição |
|---|---|
| text_index_header_cache_policy | Nome da política do cache do cabeçalho do índice de texto. |
| text_index_header_cache_size | Tamanho máximo do cache em bytes. |
| text_index_header_cache_max_entries | Número máximo de cabeçalhos desserializados no cache. |
| text_index_header_cache_size_ratio | Tamanho da fila protegida no cache do cabeçalho do índice de texto em relação ao tamanho total do cache. |
Configurações do cache de listas de postings
| Configuração | Descrição |
|---|---|
| text_index_postings_cache_policy | Nome da política do cache de postings do índice de texto. |
| text_index_postings_cache_size | Tamanho máximo do cache em bytes. |
| text_index_postings_cache_max_entries | Número máximo de postings desserializados no cache. |
| text_index_postings_cache_size_ratio | Tamanho da fila protegida no cache de postings do índice de texto em relação ao tamanho total do cache. |
Limitações
- A materialização de índices de texto com um grande número de tokens (por exemplo, 10 bilhões de tokens) pode consumir quantidades significativas de memória. A
materialização de índices de texto pode ocorrer diretamente (
ALTER TABLE <table> MATERIALIZE INDEX <index>) ou indiretamente durante mesclagens de partes. - Não é possível materializar índices de texto em partes com mais de 4.294.967.296 (= 2^32 = aprox. 4,2 bilhões) linhas. Sem um índice de texto materializado, as consultas recorrem a uma busca lenta por força bruta dentro da parte. Como estimativa de pior caso, suponha que uma parte contenha uma única coluna do tipo String e que a configuração do MergeTree
max_bytes_to_merge_at_max_space_in_pool(padrão: 150 GB) não tenha sido alterada. Nesse caso, isso ocorre se a coluna contiver, em média, menos de 29,5 caracteres por linha. Na prática, as tabelas também contêm outras colunas, e esse limite é várias vezes menor do que isso (dependendo do número, tipo e tamanho das outras colunas).
Índices de texto vs. índices baseados em filtro de Bloom
bloom_filter, ngrambf_v1, tokenbf_v1, sparse_grams), mas eles diferem fundamentalmente em seu design e nos casos de uso a que se destinam:
Índices de filtro de Bloom
- Baseiam-se em estruturas de dados probabilísticas que podem produzir falsos positivos.
- Só conseguem responder a perguntas de pertinência a conjuntos, ou seja: a coluna pode conter o token X vs. definitivamente não contém X.
- Armazenam informações no nível de grânulo, o que permite ignorar intervalos mais amplos durante a execução da consulta.
- São difíceis de ajustar corretamente (veja aqui um exemplo).
- São relativamente compactos (alguns quilobytes ou megabytes por parte).
- Constroem um índice invertido determinístico sobre tokens. O próprio índice não pode gerar falsos positivos.
- São especificamente otimizados para cargas de trabalho de pesquisa de texto.
- Armazenam informações no nível da linha, o que permite a busca eficiente de termos.
- São relativamente grandes (de dezenas a centenas de megabytes por parte).
- Eles não oferecem suporte a tokenização e preprocessamento avançados.
- Eles não oferecem suporte à pesquisa por múltiplos tokens.
- Eles não fornecem as características de desempenho esperadas de um índice invertido.
- Eles oferecem tokenização e preprocessamento
- Eles oferecem suporte eficiente a
hasAllTokens,LIKE,matche funções semelhantes de pesquisa de texto. - Eles têm escalabilidade significativamente melhor para grandes corpora de texto.
Detalhes de implementação
- um dicionário que mapeia cada token para uma lista de postings, e
- um conjunto de listas de postings, cada uma representando um conjunto de números de linha.
dictionary_block_size).
Um arquivo de blocos do dicionário (.dct) consiste em todos os blocos de dicionário de todos os grânulos de índice em uma parte.
Arquivo de cabeçalho do índice (.idx)
O arquivo de cabeçalho do índice contém, para cada bloco de dicionário, o primeiro token do bloco e seu deslocamento relativo no arquivo de blocos do dicionário.
Essa estrutura de índice esparso é semelhante ao índice primário esparso) do ClickHouse.
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 lista de postings for maior que posting_list_block_size, ela será dividida em vários blocos, que são armazenados sequencialmente no arquivo de listas de postings.
Mesclagem de índices de texto
Quando partes de dados são mescladas, o índice de texto não precisa ser reconstruído do zero; em vez disso, ele pode ser mesclado com eficiência em uma etapa separada do processo de mesclagem.
Durante essa etapa, os dicionários ordenados dos índices de texto de cada parte de entrada são lidos e combinados em um novo dicionário unificado.
Os números de linha nas listas de postings também são recalculados para refletir suas novas posições na parte de dados mesclada, usando um mapeamento de números de linha antigos para novos criado durante a fase inicial da mesclagem.
Esse método de mesclar índices de texto é semelhante à forma como projeções com a coluna _part_offset são mescladas.
Se o índice não estiver materializado na parte de origem, ele será construído, gravado em um arquivo temporário e depois mesclado com os índices das outras partes e de outros arquivos de índice temporários.
Depuração
A função de tabela mergeTreeTextIndex pode ser usada para inspecionar índices de texto.
Exemplo: conjunto de dados do Hacker News
hackernews:
ALTER TABLE e adicionaremos um índice de texto à coluna comment e, em seguida, o materializaremos:
hasToken, hasAnyTokens e hasAllTokens.
Os exemplos a seguir mostram 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 buscar o token que diferencia maiúsculas de minúsculas ‘ClickHouse’.
Leitura direta desabilitada (varredura padrão)
Por padrão, o ClickHouse usa o skip index para filtrar grânulos e depois lê os dados da coluna desses grânulos.
Podemos simular esse comportamento desabilitando a leitura direta.
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)
3. Usando hasAllTokens
hasAllTokens verifica se o texto contém todos os tokens fornecidos.
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 linhas, mas ainda precisa ler 57.03 MB da coluna.
4. Busca composta: OR, AND, NOT, …
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) seria a sintaxe mais indicada e mais eficiente.
- Apresentação: https://github.com/ClickHouse/clickhouse-presentations/blob/master/2025-tumuchdata-munich/ClickHouse_%20full-text%20search%20-%2011.11.2025%20Munich%20Database%20Meetup.pdf
- Apresentação: https://presentations.clickhouse.com/2026-fosdem-inverted-index/Inverted_indexes_the_what_the_why_the_how.pdf