Criando um índice de texto
Os índices de texto estão disponíveis de forma geral (GA) no ClickHouse 26.2 e versões mais recentes. Nessas versões, não é necessário configurar nenhuma opção especial para usar o índice de texto. Recomendamos fortemente usar versões do ClickHouse >= 26.2 em produção.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, seja na coluna
Mapusando o tokenizadorkeyValuePairs, ou apenas nas chaves ou nos valores do map usando a função mapKeys ou 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[' '].splitByRegexp(regexp[, match_tokens])divide strings de acordo com uma expressão regularregexpdefinida pelo usuário. O argumentoregexpé obrigatório; por exemplo,tokenizer = splitByRegexp('[a-zA-Z]+[0-9]'). O argumento opcionalmatch_tokens(falsepor padrão;0/1também são aceitos) controla o queregexprepresenta:- com
match_tokens = false(o padrão),regexprepresenta separadores: os tokens são os trechos de texto (não vazios) entre correspondências sucessivas (mesma semântica da função splitByRegexp). - com
match_tokens = true,regexpé correspondido diretamente: cada correspondência contribui com no máximo um token — seu primeiro grupo de captura ou a correspondência inteira, casoregexpnão tenha grupo de captura — e tudo fora das correspondências é descartado. Por exemplo,tokenizer = splitByRegexp('tag:(\w+)', true)indexahelloeworlda partir detag:hello tag:world, removendo o prefixotag:. A correspondência usa RE2, o mesmo engine utilizado por todas as funções baseadas em regex no ClickHouse; apenas o primeiro grupo de captura é usado, de modo que grupos adicionais apenas restringem o que é correspondido. Um grupo de captura que não participou da correspondência, ou que correspondeu a uma string vazia, não contribui com nenhum token, e a varredura sempre é retomada após a correspondência completa, e não após o span capturado (portanto, as correspondências nunca se sobrepõem). Commatch_tokens = true, observe que os termos buscados nas funções de busca textual são tokenizados com o próprio tokenizador do índice; assim, um termo buscado em texto simples que não corresponda aoregexpnão gera tokens e, portanto, nenhum resultado. Termos buscados passados como arrays para funções de busca textual não são tokenizados e, por isso, são recomendados para uso com tokenizadores regexp.
- com
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.chinese[(granularity)]segmenta texto chinês em palavras usando um dicionário e um modelo oculto de Markov (o algoritmo segue jieba; o dicionário e os dados do modelo embutidos são derivados de cppjieba). Diferentemente deasciiCJK, que trata cada caractere não ASCII como um token de um único caractere,chineseagrupa caracteres chineses consecutivos em palavras (por exemplo,北京大学se torna um token北京大学, em vez de quatro tokens de um único caractere). Isso gera tokens mais significativos para texto chinês e maior qualidade de busca. Para texto geral/misto, useasciiCJK; para texto somente em chinês, usechinese. O argumento opcionalgranularityé'coarse_grained'(o padrão, se não for especificado) ou'fine_grained'. Este último também enumera subpalavras sobrepostas; por exemplo,北京邮电大学também gera北京,邮电,大学. A tokenização granular melhora o recall, ao custo de um índice maior. Pesquise um índice de textochinesecom hasAnyTokens / hasAllTokens (que tokenizam o termo buscado com o tokenizadorchinese), e não comhasToken(que divide apenas em separadores ASCII).icu(locale)divide strings em tokens de palavras usando a segmentação de palavras Unicode (UAX #29) da biblioteca ICU. Para sistemas de escrita que não usam espaços em branco entre palavras (por exemplo, chinês, japonês e tailandês), a ICU aplica segmentação baseada em dicionário; portanto — diferentemente deasciiCJK— esse texto é dividido em palavras significativas com vários caracteres, em vez de caracteres únicos. Aqui, “dicionário” refere-se às listas de palavras incluídas na ICU para esses sistemas de escrita (consultebrkitr/dictionaries); a ICU escolhe a divisão mais provável entre essas palavras.localeé a localidade da ICU passada ao segmentador; a segmentação é orientada principalmente pelo sistema de escrita e pelo dicionário, e a localidade seleciona as personalizações específicas da localidade na ICU. É um parâmetro obrigatório; por exemplo,tokenizer = icu('ja')outokenizer = icu('zh'). As localidades disponíveis podem ser listadas comSELECT * FROM system.collations.japanesedivide texto em japonês em palavras usando o analisador morfológico MeCab. Diferentemente deasciiCJK, que gera tokens de um único caractere para entradas CJK, este tokenizador realiza a segmentação adequada de palavras. Ele requer um dicionário carregado em tempo de execução a partir da configuração do servidor (consulte Dicionário do tokenizador japonês).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). Para compatibilidade com outros sistemas,keywordestá disponível como um alias dearray.keyValuePairscombina os pares chave-valor de uma colunaMapem um único token. Isso ajuda a realizar lookups combinados de chave-valor, comoWHERE map['key'] = 'value'(consulte o tokenizadorkeyValuePairs).
Dicionário do tokenizador japonês
O tokenizadorjapanese requer um dicionário MeCab, que não é fornecido com o ClickHouse. Forneça um na configuração do servidor:
dictionary_locationé o local de um arquivo compactado contendo um dicionário MeCab compilado. Qualquer dicionário oficial funciona, como IPADIC ou UniDic. O local deve terminar em uma extensão de arquivo compactado compatível (por exemplo,.tar.gz,.tar.zstou.zip) — o tipo de arquivo compactado é detectado por ela —, portanto, uma URL sem essa extensão (por exemplo,https://example.com/download) é rejeitada. Locais compatíveis:- um caminho local
file://; - uma URL
http(s)://, obtida por download direto (use-a para um objeto público ou pré-assinado); - um armazenamento de objetos compatível com S3 — AWS S3, GCS, MinIO, local etc. (não apenas AWS) — acessado como
s3:///gs:///oss://ou como uma URL completahttp(s)://endpoint/bucket/key. Para um bucket privado, forneça as credenciais do S3 como elementos filhos de<japanese>(consulte o exemplo abaixo).
- um caminho local
dictionary_shaé o SHA-256 desse arquivo compactado. Ele é verificado antes de o dicionário ser carregado; em caso de divergência, o dicionário não é carregado e um erro é gerado.
dictionary_sha deve ser configurado em todas as réplicas.
Para ler de um bucket privado compatível com S3, adicione as configurações do S3 como elementos filhos de <japanese>, ao lado de dictionary_location e dictionary_sha:
access_key_id, secret_access_key, region, no_sign_request, use_environment_credentials, …) são as mesmas configurações de autenticação do S3 usadas em outras partes do ClickHouse. A presença delas também faz com que uma URL http(s):// seja obtida por meio do cliente S3 (com assinatura de solicitações), em vez de um simples download.
Depois que o Dicionário estiver configurado, o tokenizador japanese poderá ser usado da seguinte forma:
Query
Response
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 Unicode, incluindo caracteres CJK.
Para idiomas que não separam palavras por espaços em branco (por exemplo, chinês, japonês ou tailandês), o tokenizador icu(locale) produz tokens de palavras significativos com vários caracteres por meio da segmentação de palavras baseada em dicionário do ICU.
Especificamente para o japonês, o tokenizador japanese (MeCab) segmenta o texto em palavras, em vez de caracteres individuais, e geralmente fornece melhores resultados de busca.
Especificamente para o chinês, o tokenizador chinese (jieba) segmenta o texto em palavras, em vez de caracteres individuais, e geralmente fornece melhores resultados de busca.
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 para o 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))
Em princípio, os preprocessadores são equivalentes a envolver a coluna ou expressão do índice com a expressão do pré-processador.
Por exemplo, o preprocessador
lower em INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col)) pode ser emulado por INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha').
Esta última forma tem a desvantagem de que o preprocessador emulado só é aplicado se corresponder à condição de filtro na cláusula WHERE.
Por exemplo, WHERE hasAllTokens(lower(col), [...]) corresponde, enquanto WHERE hasAllTokens(col, [...]) não.
Portanto, para uma experiência ideal do usuário, recomendamos usar expressões do pré-processador.SETTINGS use_skip_indexes = 0).
Por exemplo,
Query
Query
Query
Query
- Filtragem de stop words (tokens extremamente frequentes). Tokens muito comuns, como “the”, “a” e “is”, têm pouca relevância para busca e aumentam o índice.
Você pode usar o pós-processador para descartá-los convertendo-os em tokens vazios — tokens vazios são ignorados, isto é, não são adicionados ao índice.
Exemplo:
if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str) - Remoção de timestamp. Linhas de log frequentemente começam com ou contêm um timestamp estruturado, como
2024-01-15T10:23:45. O indexamento de tokens de timestamp infla o índice com strings que não têm relevância para busca. Há duas abordagens complementares para ignorar timestamps:- Abordagem com pós-processador: use o tokenizador
splitByString(divisão por espaços em branco) para que o timestamp inteiro se torne um único token e, em seguida, useparseDateTimeOrNullpara detectá-lo e descartá-lo. Exemplo:if(isNull(parseDateTimeOrNull(str, '%Y-%m-%dT%H:%i:%S')), str, '')Para timestamps com offsets de timezone ou segundos fracionários, useparseDateTimeBestEffortOrNull(str)sem uma format string explícita. - Abordagem com pré-processador: remova o timestamp da linha de log completa antes da tokenização com uma expressão regular.
Exemplo:
replaceRegexpAll(str, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')Isso funciona com qualquer tokenizador e é mais eficiente, pois os caracteres do timestamp nunca são tokenizados. As duas abordagens podem ser combinadas: o pré-processador remove o timestamp, enquanto o pós-processador normaliza ou filtra os tokens restantes (por exemplo, lowercase + remoção de palavras de severidade comoERRORouINFO).
- Abordagem com pós-processador: use o tokenizador
- Stemming. Mapear cada token para seu radical melhora a abrangência da busca ao corresponder variantes morfológicas que compartilham a mesma raiz.
Por exemplo, com stemming em inglês, “running”, “runs” e “run” são todos reduzidos a “run”, de modo que uma consulta por qualquer uma dessas variantes corresponde a todas elas.
O ClickHouse fornece uma função interna stem para vários idiomas.
Exemplo:
stem(str, 'en') - Normalização de maiúsculas/minúsculas. Converter tokens para minúsculas ou maiúsculas para permitir correspondência sem diferenciar maiúsculas de minúsculas, por exemplo lower, lowerUTF8. Para conversão para minúsculas e maiúsculas, recomendamos um pré-processador em vez de um pós-processador.
Array(String), o pós-processador ainda opera em tokens individuais como valores simples String.
O uso de funções não determinísticas não é permitido.
O pós-processador é aplicado a cada token gerado durante a construção do índice (para o tokenizador array, cada elemento do array é um token). Durante a consulta, o comportamento depende da função:
- Para
hasToken,hasAllTokens,hasAnyTokensehasPhrase(com qualquer tokenizador compatível): o pós-processador é aplicado tanto aos tokens do haystack quanto ao needle de busca, permitindo correspondência totalmente normalizada (por exemplo, busca sem diferenciar maiúsculas de minúsculas). ParahasPhrase, os tokens pós-processados são posicionados de forma densa, portanto, um token que o pós-processador descarta não deixa nenhuma lacuna posicional e a frase ainda corresponde através dele — por exemplo, com um pós-processador de stop words que descartathe,hasPhrase(col, 'see cat')corresponde a um documentosee the cat. A única exceção éhasPhraseem um índicesplitByRegexp, que não oferece suporte a um pós-processador (a combinação é rejeitada com uma exceção). - Para todas as outras funções (
=,IN,has,hasAny,hasAll,mapContains*): apenas o needle de busca é pós-processado para a busca por dica de índice; o predicado no nível da linha ainda compara com os valores originais da coluna.
- Remova stop words usando uma expressão de pós-processador:
- Remova timestamps usando uma expressão de pós-processador:
- Remova os timestamps usando uma expressão de preprocessador:
- Remova os timestamps usando uma expressão combinada de preprocessador e pós-processamento:
- Reduza os tokens aos seus radicais usando uma expressão de pós-processamento:
=, IN, startsWith, endsWith, LIKE, mapContains*), o índice de texto é usado apenas para ignorar blocos de dados irrelevantes; o ClickHouse ainda verifica cada linha restante usando o predicado original sobre os dados originais da coluna.
Para funções de busca por token (hasToken, hasAllTokens, hasAnyTokens), o índice de texto é o principal caminho de avaliação: o ClickHouse normaliza o termo buscado por meio do mesmo pré-processador, tokenizador e pós-processador aplicados na construção do índice, e usa essa forma normalizada tanto para partes de tabela indexadas quanto não indexadas. Com um pós-processador, os tokens do texto pesquisado também são normalizados em tempo de consulta (para qualquer tokenizador, não apenas array), para que ambos os lados da comparação sejam transformados de forma consistente e o resultado não dependa de o índice ser lido diretamente (configuração query_plan_direct_read_from_text_index) nem de uma determinada parte ter um índice materializado — por exemplo, habilitando correspondência sem diferenciar maiúsculas de minúsculas para hasAllTokens(col, ['FOO']) com um pós-processador lower.
Sem support_phrase_search, hasPhrase usa o índice apenas como uma dica e verifica cada linha restante com o predicado original; um pós-processador também normaliza a frase e os tokens do texto pesquisado da mesma forma, para que o resultado seja independente do caminho de leitura, e os tokens descartados pelo pós-processador não quebrem a adjacência da frase. Com support_phrase_search = 1, hasPhrase usa leituras diretas exatas (ainda aplicando o pós-processador, se houver). Esse suporte ao pós-processador não se estende ao tokenizador splitByRegexp: hasPhrase em um índice splitByRegexp combinado com um pós-processador é rejeitado (consulte a nota de rodapé ³ abaixo).
Os tokens de busca que o pós-processador mapeia para uma string vazia são ignorados, ou seja, tratados como ausentes da frase de busca.
¹
LIKE e match usam leitura direta como dica para os tokenizadores listados; caso contrário, recorrem a uma varredura por força bruta.
Além disso, LIKE oferece suporte à avaliação por varredura de dicionário (habilitada por use_text_index_like_evaluation_by_dictionary_scan) para os tokenizadores splitByNonAlpha e array, sem pré-processador nem pós-processador; consulte a seção consultas LIKE/ILIKE abaixo.
² ILIKE é compatível apenas por meio da avaliação por varredura de dicionário (use_text_index_like_evaluation_by_dictionary_scan = 1, tokenizador splitByNonAlpha ou array).
Não há fallback para usar o índice como dica nos padrões que a varredura de dicionário não suporta: se a configuração estiver desabilitada ou o tokenizador não estiver no conjunto compatível, o índice não será usado para ILIKE.
O pré-processador, se presente, deve ser lower ou upper; pós-processadores não são compatíveis.
Alguns needles também não são elegíveis; consulte consultas LIKE/ILIKE.
³ hasPhrase em um índice de texto splitByRegexp não oferece suporte a pós-processador: a combinação é rejeitada com uma exceção, porque a reescrita em nível de linha do pós-processador pressupõe tokens no estilo splitByNonAlpha, divididos por espaços em branco. Sem um pós-processador, splitByRegexp é totalmente suportado por hasPhrase.
⁴ startsWith e endsWith pesquisam os tokens completos da needle de busca, e o token na extremidade aberta da needle de busca está incompleto porque o valor continua ali: startsWith(col, 'ClickHouse is') pesquisa o token ClickHouse, enquanto startsWith(col, 'ClickHouse') não tem nenhum token completo para pesquisar.
Este último é avaliado por uma varredura de dicionário (use_text_index_like_evaluation_by_dictionary_scan = 1, tokenizador splitByNonAlpha ou array, sem pré-processador ou pós-processador), que também é o caminho adotado por col LIKE 'ClickHouse%', porque a etapa do analyzer optimize_rewrite_like_perfect_affix o reescreve como startsWith.
Consulte Consultas LIKE/ILIKE.
⁵ A coluna tokenizadores compatíveis ignora o tokenizador keyValuePairs, que é um tokenizador especializado para colunas Map.
Experimental: argumento de suporte a busca por frase (opcional).
O parâmetro experimental support_phrase_search (padrão: 0) controla se o índice armazena as posições dos tokens.
Quando definido como 1, o índice também armazena dados posicionais (em um arquivo .pos), o que permite correspondência exata de frases por meio de leituras diretas com a função hasPhrase.
Armazenar posições aumenta o tamanho do índice em disco e o custo de escrita, portanto esse recurso é opcional.
O formato em disco ainda não é estável, portanto este parâmetro é experimental e pode mudar em um lançamento futuro.
Criar um índice com support_phrase_search = 1 exige, portanto, que a configuração do MergeTree allow_experimental_text_index_phrase_search esteja habilitada.
Defina support_phrase_search = 0 (o padrão) para manter o armazenamento somente com posting list; os índices de texto criados sem esse argumento continuam sem posições.
Granularidade do índice.
Os índices de texto são implementados no ClickHouse como um tipo de skip indexes.
No entanto, diferentemente de outros skip indexes, os índices de texto usam granularidade infinita (100 milhões).
Isso pode ser observado na definição da tabela de um índice de texto.
Exemplo:
Query
Response
Usando um índice de texto
Usar um índice de texto em consultas SELECT é simples, pois as funções comuns de busca em strings usarão o índice automaticamente. Se não houver um índice em uma coluna ou parte da tabela, as funções de busca em strings recorrerão a varreduras lentas por força bruta.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 possíveis de preprocessador e pós-processamento.
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 preprocessador ou pós-processamento).Funções suportadas
O índice de texto pode ser usado com funções de texto na cláusulaWHERE 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.
multiSearchAny and multiMatchAny
multiSearchAny e sua variante UTF-8 multiSearchAnyUTF8 testam se alguma entre várias substrings literais ocorre no texto de entrada, e multiMatchAny testa se alguma entre várias expressões regulares encontra correspondência.
Essas funções usam o índice de texto nas mesmas condições que LIKE e match (veja acima): o ClickHouse precisa conseguir extrair tokens completos de cada needle, e a lista de needles precisa ser constante.
Um grânulo é lido se algum needle puder estar presente nele.
Para multiMatchAny, se um único padrão não puder ser reduzido a um requisito de token (por exemplo, .*, que corresponde a qualquer documento), o índice de texto não poderá ser usado, e a consulta recorrerá a uma varredura completa.
Assim como em LIKE e match, a busca por substring e por expressão regular funciona melhor com os tokenizadores ngrams e sparseGrams.
Esses tokenizadores indexam n-grams de caracteres sobrepostos, de modo que um needle é decomposto em n-grams presentes no índice em qualquer lugar em que o needle ocorra como substring, independentemente de começar ou terminar no meio de uma palavra.
Portanto, um needle pode ser usado como está, desde que tenha pelo menos o tamanho do n-gram.
Exemplo de índice de texto com o tokenizador ngrams:
splitByNonAlpha, em contrapartida, indexa apenas tokens completos (palavras inteiras).
Como um padrão pode começar ou terminar no meio de uma palavra, o ClickHouse descarta os tokens do início e do fim de cada padrão, de modo que o índice só possa podar grânulos usando tokens completos.
Para que a busca por substring e por expressão regular use o índice com splitByNonAlpha, envolva cada padrão com caracteres separadores (como espaços), para que ele forme um ou mais tokens completos.
Exemplo de índice de texto com o tokenizador splitByNonAlpha:
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 busca.
No índice com o tokenizador ngrams, isso acontece quando o comprimento das Strings pesquisadas entre caracteres curinga é igual ou maior que o comprimento do ngram.
Quando um índice de texto usa um pós-processador, essas funções ainda podem usar o índice no modo Hint se os tokens de dica extraídos continuarem não vazios após a normalização. Se a normalização remover todos os tokens de dica, o índice não será usado para esse predicado.
Exemplo de índice de texto com o tokenizador 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
A função
hasToken parece simples de usar, mas tem algumas limitações quando usada para lookups em índices de texto com tokenizadores diferentes de splitByNonAlpha e/ou expressões de pré-processamento/pós-processamento.
Recomendamos usar as funções hasAnyTokens e hasAllTokens.As variantes sem distinção entre maiúsculas e minúsculas hasTokenCaseInsensitive e hasTokenCaseInsensitiveOrNull não reconhecem índices de texto — elas sempre executam uma varredura completa da linha, mesmo em colunas com índice de texto. Para correspondência sem distinção entre maiúsculas e minúsculas, use um pré-processador ou pós-processador lower(...) e combine-o com hasToken / hasAllTokens / hasAnyTokens.hasAnyTokens e hasAllTokens, ela não tokeniza o termo de busca (parte do pressuposto de que a entrada é um único token).
Exemplo:
hasAnyTokens and hasAllTokens
As funções hasAnyTokens e hasAllTokens fazem a 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 tokenizador da coluna de índice, ou como um array de tokens já processados, aos quais nenhuma tokenização será aplicada antes da busca.
Consulte a documentação da função para mais informações.
Exemplo:
hasPhrase
A função hasPhrase faz correspondência com uma frase: todos os tokens devem aparecer de forma consecutiva e na mesma ordem da string de busca.
Ao contrário de 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.
Quando o índice de texto usa um pós-processador, a frase de busca também é normalizada antes da consulta ao índice.
Observe que a função requer um dos tokenizadores splitByNonAlpha, splitByString, splitByRegexp, ngrams, asciiCJK ou icu.
Exemplo:
has
A função Array has verifica a correspondência de um único token em um array de strings.
Exemplo:
hasAny e hasAll
As funções de array hasAny e hasAll verificam se a coluna de array indexada contém alguma ou todas as strings procuradas de um conjunto constante.
Exemplo:
mapContains
A função mapContains (um alias de 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
A função mapContainsValue faz correspondência com os tokens extraídos da string pesquisada nos valores de um map.
O comportamento é semelhante ao da função 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
As funções mapContainsKeyLike e mapContainsValueLike comparam um padrão com todas as chaves ou valores (respectivamente) de um map.
Exemplo:
operator[]
O operador de acesso operator[] pode ser usado com o índice de texto para filtrar as chaves e os valores.
O índice de texto é utilizado se usar um tokenizador keyValuePairs sobre a coluna Map, ou se for construído sobre uma expressão de índice mapKeys(map) ou mapValues(map).
No primeiro caso (tokenizador keyValuePairs), pode ser usada a direct read; caso contrário, direct read com indicação.
Exemplo:
Array(T) e Map(K, V) com o índice de texto.
Indexação de colunas Array(String)
Imagine uma plataforma de blogs, em que os autores categorizam suas publicações usando palavras-chave. Queremos que os usuários descubram conteúdo relacionado pesquisando tópicos ou clicando neles. Considere esta definição de tabela: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
Em muitos casos de uso de observabilidade, as mensagens de log são divididas em “componentes” e armazenadas nos tipos de dados apropriados, por exemplo, data e hora para o timestamp, enum para o nível de log etc. Os campos de métricas são mais bem armazenados como pares chave-valor. As equipes de operações precisam pesquisar nos logs com eficiência para depuração, incidentes de segurança e monitoramento. Considere esta tabela de logs:Lookups combinados de chave-valor com o tokenizer keyValuePairs
Use o tokenizer keyValuePairs na própria coluna Map quando precisar buscar uma chave específica que contenha um valor específico:
(key, value) de uma linha em um único token, de modo que o índice saiba qual valor pertence a qual chave.
Os índices mapKeys e mapValues descritos abaixo não conseguem responder a esse tipo de consulta, porque indexam chaves e valores de forma independente: eles conseguem dizer que algum atributo de uma linha tem o valor error, mas não que se tratava do atributo level.
Um par é armazenado como o token key ‖ value ‖ length(key).
O comprimento ao final mantém inequívoca a fronteira entre a chave e o valor, de modo que ambos podem conter bytes arbitrários: diferentemente de uni-los com um separador como key=value, uma chave ou um valor que contenha o separador não pode gerar uma correspondência falsa.
A função de tabela mergeTreeTextIndex retorna as partes decodificadas de cada token nas colunas token_key e token_value.
Aplicam-se as seguintes restrições:
- O índice deve ser criado sobre um
Mapcujas chaves e valores sejam do tipoStringouLowCardinality(String).FixedStringé rejeitado porque a coluna armazena os bytes de preenchimento enquanto a constante pesquisada não, o que faria com que linhas fossem silenciosamente ignoradas.Nullableé rejeitado porque a codificação não consegue distinguir um valor vazio deNULL. - Os argumentos
preprocessor,postprocessoresupport_phrase_searchsão rejeitados na criação da tabela. - Até o momento, apenas
=sobre um elemento do map é respondido a partir do índice. Outras buscas em map, comomapContainsKey,mapContainsValue, suas variantes*LikeeIN, recorrem a uma varredura por força bruta. map['key']retorna o valor padrão do tipo do valor quando a chave está ausente, portantomap['key'] = ''também é verdadeiro para linhas que não contêm a chave e, por isso, não possuem token. Um predicado desse tipo recorre a uma varredura por força bruta.- Se uma linha contiver a mesma chave mais de uma vez,
map['key']é o valor de sua primeira ocorrência, e o índice corresponde a essa ocorrência.
Busca por chave e valor com mapKeys e mapValues
Use mapKeys para criar um índice de texto quando precisar encontrar logs por nomes de campos ou tipos de atributos:
Indexação de colunas JSON
Índices de texto podem ser usados com colunasJSON 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
Você pode criar um skip index em qualquer subcoluna JSON usando a mesma sintaxe das colunas comuns. Há duas maneiras de referenciar uma subcoluna JSON em uma expressão de índice:- 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
Assim como nas colunasMap, é 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
Índices de texto podem ser usados para acelerar pesquisas em colunas JSON por meio da funçãoJSONAllValues.
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
Exemplo de definição de índice:Padrões de consulta suportados
Depois de criado, o índice pode acelerar consultas em subcolunas JSON usando as mesmas funções usadas com colunasString e a função equals para todas as colunas.
Acesso à subcoluna:
CAST explícito:
IN:
Busca por frase
Uma busca comum em um índice de texto, por exemploWhile she stayed in Tokyo, the weather was great. corresponde ao filtro.
Em contraste, a busca por frase consiste em corresponder aos tokens na ordem fornecida.
Por exemplo,
weather in Tokyo, como em How is the weather in Tokyo??
O índice de texto acelera a busca por frase fazendo a interseção das listas 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 dos tokens.
Esse processo é relativamente custoso e mais lento do que consultas comuns de busca textual.
Para acelerar as consultas de busca por frase, habilite o armazenamento das posições no índice de texto (consulte Optional parameters acima).
hasPhrase pode ser usado em conjunto com os tokenizers splitByNonAlpha, splitByString, splitByRegexp, ngrams, asciiCJK e icu.
A string da frase fornecida é tokenizada usando o tokenizer do índice.
Os caracteres separadores na frase são ignorados: hasPhrase(text, 'quick+brown') é equivalente a hasPhrase(text, 'quick brown'), supondo que splitByNonAlpha seja usado como tokenizer.
Exemplo
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
Certos tipos de consultas de texto podem ser acelerados significativamente por uma otimização chamada “leitura direta”. Exemplo:- 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.
Se o índice de texto for definido com um tokenizer keyValuePairs, a leitura direta terá suporte para equals em um elemento de map (map['key'] = 'value').
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
Quando o pattern de uma consulta LIKE/ILIKE é%<alpha-numeric-characters-without-spaces>%, <alpha-numeric-characters-without-spaces>% ou %<alpha-numeric-characters-without-spaces> e o tokenizer do índice de texto é splitByNonAlpha ou array, o ClickHouse aproveita o inverted index para acelerar significativamente as consultas LIKE/ILIKE. Para isso, em vez de fazer um full-table scan, o ClickHouse percorre o Dicionário do inverted index para encontrar o pattern correspondente.
A forma como o resultado do Dicionário scan é usado depende de onde a needle está ancorada:
%value%corresponde a uma linha se e somente se corresponder a um dos tokens da linha, portanto o índice decide a consulta sozinho: uma leitura direta (sem hint) que remove o predicado original.value%e%valueancoram a needle no valor inteiro, enquanto o Dicionário scan só consegue ancorá-la em um token; assim, o scan retorna um superconjunto das linhas correspondentes e é usado como uma leitura direta como hint.
startsWith(col, 'value') e endsWith(col, 'value') quando a needle não possui um token completo para pesquisar.
Esse é o caminho que a maioria dos patterns value% e %value realmente segue, pois a passagem do analyzer optimize_rewrite_like_perfect_affix (habilitada por padrão) reescreve col LIKE 'value%' como startsWith(col, 'value') e col LIKE '%value' como endsWith(col, 'value').
Needles que abrangem vários tokens continuam usando os tokens completos da needle e dispensam o Dicionário scan.
Como caso especial, se o tokenizer do índice for array, então qualquer pattern se qualifica para a otimização: âncoras, pontuação, wildcards _, várias needles separadas por % e metacaracteres escapados.
Quando a otimização está habilitada, as consultas LIKE/ILIKE tendem a ser significativamente mais rápidas que um full-table scan. No entanto, quando o pattern corresponde à maioria dos tokens do Dicionário, o desempenho pode ser pior do que o de um full-table scan. Felizmente, existe um mecanismo de fallback para evitar isso.
O ganho de velocidade de um pattern value% ou %value vem do skipping de grânulos e, portanto, depende de como as linhas correspondentes estão distribuídas. Needles que correspondem a linhas em todos os grânulos não eliminam nada, e a consulta acaba pagando pelo Dicionário scan além do full scan que teria feito de qualquer forma. Desabilite use_text_index_like_evaluation_by_dictionary_scan para workloads desse tipo.
A otimização é controlada por uma configuração:
O mecanismo de fallback é controlado por duas configurações:
Essa otimização suporta apenas as funções like, ilike, startsWith e endsWith.
De modo geral, ela exige um índice sem pré-processador ou pós-processador (ilike também aceita lower ou upper como função de pré-processador).
Para ilike, a busca recorre a um full-table scan em dois casos especiais, tanto para o tokenizer splitByNonAlpha quanto para o array:
- A needle contém a letra
k. OiliketrataU+212A KELVIN SIGNcomo umk, mas o Dicionário scan compara bytes e deixaria passar uma linha escrita dessa forma.U+212Aé o único caractere que oilikeconverte para uma letra ou dígito ASCII, portantoké o único caractere de needle afetado. - A expressão do pré-processador do índice contém
lowerUTF8ouupperUTF8. Essas funções reescrevem caracteres não ASCII como letras ASCII (ßcomoSS,ſcomoS), o que faria o índice reportar linhas que oilikenão corresponde.
Consultas triviais de contagem
Uma consulta que apenas conta as linhas que correspondem a um predicado de busca textualhasAnyTokens) ou intersectá-los (hasAllTokens). Como um índice de texto abrange toda a parte, a contagem é exata e continua rápida mesmo em partes muito grandes.
A otimização se aplica a um count() simples filtrado por um único predicado hasToken, hasAnyTokens ou hasAllTokens (ou um equivalente de Array/Map). Predicados combinados com AND/OR/NOT, um filtro adicional (por exemplo, ... AND id > 10), um predicado no modo de indicação, como m['key'] = 'value', selecionar qualquer coisa além da contagem ou uma busca por frase, LIKE ou padrão fazem com que a consulta leia as linhas. Partes sem um índice materializado ainda são contadas corretamente mediante a leitura de suas linhas.
Para confirmar que a otimização está sendo aplicada, verifique se há ReadFromTextIndexCount no plano de consulta:
Cache
Há diferentes caches de servidor disponíveis para armazenar em buffer, na memória, partes do índice de texto (consulte a seção Detalhes de implementação): Atualmente, há caches para os cabeçalhos desserializados, tokens e lista de postings do índice de texto, para reduzir a E/S. Use as configurações use_text_index_header_cache, use_text_index_tokens_cache e use_text_index_postings_cache para desabilitar a leitura e a gravação nos caches individuais pelas consultas. O armazenamento em cache de tokens ausentes de uma parte de dados é habilitado por padrão e pode ser controlado independentemente comuse_text_index_negative_tokens_cache.
Para limpar os caches, use a instrução SYSTEM CLEAR TEXT INDEX CACHES
Consulte as configurações de servidor a seguir para configurar os caches.
Configurações do cache de tokens
Configurações de cache do cabeçalho
Configurações do cache de listas de postings
Limitações
No momento, o índice de texto tem as seguintes 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).
Notas de upgrade
A versão do formato em disco dos índices de texto é controlada pela configuração de tabelatext_index_serialization_version (padrão: v2_with_positions).
A configuração é uma preferência, não uma restrição rígida: se a versão configurada não puder representar um índice, uma versão mais recente capaz de representá-lo será escolhida automaticamente. Portanto, a gravação de um índice de texto nunca falhará devido a essa configuração.
Durante uma atualização gradual, fixe o formato com a configuração compatibility nos servidores já atualizados: quando definida como uma versão anterior à que introduziu o formato correspondente, text_index_serialization_version reverte automaticamente para um valor mais antigo, e os servidores mais recentes continuam gravando em um formato que os servidores mais antigos ainda conseguem ler.
O codec da lista de postings não é regido por essa versão: uma parte gravada com posting_list_codec = 'pfor' (ou com a configuração text_index_posting_list_codec) não pode ser lida por servidores anteriores ao codec, e nem text_index_serialization_version nem compatibility impedem seu uso, porque um codec indicado no índice tem precedência sobre a preferência de versão, da mesma forma que support_phrase_search. Não habilite pfor até que todos os servidores que possam ler a tabela tenham passado por upgrade.
Índices de texto vs. índices baseados em filtro de Bloom
Predicados sobre strings podem ser acelerados com índices de texto e índices baseados em filtro de Bloom (tipos de índicebloom_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
Cada índice de texto consiste em duas estruturas de dados (abstratas):- 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.
Arquivo de posições (.pos)
Opcional, somente se o argumento do índice support_phrase_search = 1.
Armazena as posições dos tokens dentro das linhas correspondentes.
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
Vamos analisar as melhorias 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: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, …
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 desativada (varredura padrão)hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) seria a sintaxe mais indicada e mais eficiente.
Conteúdo relacionado
- Blog: Anunciando a disponibilidade geral da pesquisa de texto completo do ClickHouse
- Blog: Criando pesquisa de texto completo de alto desempenho para armazenamento de objetos
- Vídeo: Introdução à pesquisa de texto completo no ClickHouse
- Vídeo: Nos bastidores: pesquisa de texto completo no ClickHouse em escala e com alta velocidade
- Apresentação: Por dentro da pesquisa de texto completo no ClickHouse: rápida, nativa e colunar
- Apresentação: Índices invertidos em bancos de dados: o porquê, o quê e como, FOSDEM 2026