Création d’un index de texte
Les index de texte peuvent être utilisés avec n’importe quelle version de ClickHouse >= 26.2, quel que soit le paramètre de compatibilité.
Query
- String et FixedString,
- Array(String) et Array(FixedString),
- Map (à l’aide des fonctions mapKeys et mapValues), et
- JSON (à l’aide des fonctions JSONAllPaths et
JSONAllValues).
Array(Nullable(String or FixedString)).
Autrement, pour ajouter un index de texte à une table existante :
Query
Query
Query
tokenizer précise le tokenizer :
splitByNonAlphadivise les chaînes selon les caractères ASCII non alphanumériques (voir la fonction splitByNonAlpha).splitByString(S)divise les chaînes à l’aide de certaines chaînes séparatricesSdéfinies par l’utilisateur (voir la fonction splitByString). Les séparateurs peuvent être spécifiés à l’aide d’un paramètre facultatif, par exempletokenizer = splitByString([', ', '; ', '\n', '\\']). Notez que chaque chaîne peut être composée de plusieurs caractères (', 'dans l’exemple). La liste de séparateurs par défaut, si elle n’est pas explicitement spécifiée (par exempletokenizer = splitByString), est un seul espace[' '].asciiCJKdivise les chaînes en tokens selon les règles de délimitation des mots Unicode (comme dans Unicode Text Segmentation (UAX #29)). Les caractères ASCII alphanumériques et les traits de soulignement forment des tokens avec des connecteurs (ASCII:pour les lettres,.et'pour les caractères de même type). Les caractères Unicode non ASCII, y compris les caractères CJK, deviennent des tokens d’un seul caractère.ngrams(N)divise les chaînes enN-grammes de taille identique (voir la fonction ngrams). La longueur des n-grammes peut être spécifiée à l’aide d’un paramètre entier facultatif compris entre 1 et 8, par exempletokenizer = ngrams(3). La taille des n-grammes par défaut, si elle n’est pas explicitement spécifiée (par exempletokenizer = ngrams), est de 3.sparseGrams(min_length, max_length, min_cutoff_length)divise les chaînes en n-grammes de longueur variable d’au moinsmin_lengthet d’au plusmax_lengthcaractères (bornes incluses) (voir la fonction sparseGrams). Sauf indication explicite,min_lengthetmax_lengthvalent par défaut 3 et 100. Si le paramètremin_cutoff_lengthest fourni, seuls les n-grammes dont la longueur est supérieure ou égale àmin_cutoff_lengthsont renvoyés. Comparé àngrams(N), le tokenizersparseGramsproduit des N-grammes de longueur variable, ce qui permet une représentation plus souple du texte d’origine. Par exemple,tokenizer = sparseGrams(3, 5, 4)génère en interne des 3-, 4- et 5-grammes à partir de la chaîne d’entrée, mais seuls les 4- et 5-grammes sont renvoyés.arrayne réalise aucune tokenisation, c.-à-d. que chaque valeur de ligne constitue un token (voir la fonction array).
Le tokenizer
splitByString applique les séparateurs de découpage de gauche à droite.
Cela peut créer des ambiguïtés.
Par exemple, les chaînes séparatrices ['%21', '%'] feront que %21abc sera tokenisé en ['abc'], tandis qu’en inversant l’ordre des deux chaînes séparatrices ['%', '%21'], on obtiendra ['21abc'].
Dans la plupart des cas, vous souhaiterez que la correspondance privilégie d’abord les séparateurs les plus longs.
Cela peut généralement se faire en passant les chaînes séparatrices par ordre décroissant de longueur.
Si les chaînes séparatrices forment un prefix code, elles peuvent être passées dans un ordre arbitraire.Query
Response
asciiCJK est recommandé, car il gère correctement les limites de mots Unicode, y compris pour les caractères CJK.
Argument de préprocesseur (facultatif). Le préprocesseur correspond à une expression appliquée à la chaîne d’entrée avant la tokenisation.
Les cas d’usage typiques de l’argument de préprocesseur incluent
- Conversion en minuscules/majuscules, ou normalisation de la casse pour permettre une correspondance insensible à la casse, par ex., lower, lowerUTF8, caseFoldUTF8.
- Normalisation UTF-8, par ex. normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, normalizeUTF8NFKCCasefold, toValidUTF8.
- Suppression ou transformation de caractères ou de sous-chaînes indésirables, comme les accents, par ex. extractTextFromHTML, substring, idnaEncode, translate, removeDiacriticsUTF8.
Nullable(T) ou LowCardinality(T), alors l’expression de préprocesseur doit accepter des valeurs nullables ou à faible cardinalité (c.-à-d. ne pas lever d’exception).
Exemples :
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)))- Non autorisé :
INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(col, col))
Les préprocesseurs sont en principe équivalents à l’encapsulation de la colonne ou de l’expression indexée dans l’expression de préprocesseur.
Par exemple, le préprocesseur
lower dans INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col)) peut être émulé par INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha').
Cette dernière forme présente l’inconvénient que le préprocesseur émulé n’est appliqué que s’il correspond à la condition de filtrage dans la clause WHERE.
Par exemple, WHERE hasAllTokens(lower(col), [...]) correspond, tandis que WHERE hasAllTokens(col, [...]) ne correspond pas.
Pour une expérience utilisateur optimale, nous recommandons donc d’utiliser des expressions de préprocesseur.SETTINGS use_skip_indexes = 0).
Par exemple,
Query
Query
Query
Query
postprocessor (facultatif). Le postprocesseur désigne une expression appliquée à chaque token de sortie après la tokenisation.
Contrairement au préprocesseur, qui transforme l’intégralité de la chaîne d’entrée avant que le tokenizer ne la découpe en tokens, le postprocesseur agit directement sur les tokens, un par un.
C’est l’endroit idéal pour les transformations qui s’appliquent intrinsèquement au niveau du token.
Les cas d’usage typiques de l’argument postprocessor incluent :
- Filtrage des stop words (tokens extrêmement fréquents). Les tokens très courants tels que “the”, “a” et “is” ont peu de pertinence pour la recherche et alourdissent l’index.
Vous pouvez utiliser le postprocesseur pour les éliminer en les convertissant en tokens vides — les tokens vides sont ignorés, c’est-à-dire qu’ils ne sont pas ajoutés à l’index.
Exemple :
if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str) - Suppression des timestamps. Les lignes de log commencent souvent par un timestamp structuré tel que
2024-01-15T10:23:45, ou en contiennent un. L’indexation des tokens de timestamp gonfle l’index avec des chaînes qui n’ont aucune pertinence pour la recherche. Il existe deux approches complémentaires pour ignorer les timestamps :- Approche avec postprocesseur : utilisez le tokenizer
splitByString(découpage par espaces) afin que le timestamp entier devienne un seul token, puis utilisezparseDateTimeOrNullpour le détecter et le supprimer. Exemple :if(isNull(parseDateTimeOrNull(str, '%Y-%m-%dT%H:%i:%S')), str, '')Pour les timestamps avec décalages de fuseau horaire ou secondes fractionnaires, utilisezparseDateTimeBestEffortOrNull(str)sans chaîne de format explicite. - Approche avec préprocesseur : retirez le timestamp de la ligne de log complète avant la tokenisation à l’aide d’une regular expression.
Exemple :
replaceRegexpAll(str, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')Cela fonctionne avec n’importe quel tokenizer et est plus efficace, car les caractères du timestamp ne sont jamais tokenized. Les deux approches peuvent être combinées : le préprocesseur retire le timestamp tandis que le postprocesseur normalise ou filtre les tokens restants (par exemple, passage en minuscules + suppression des mots de sévérité commeERRORouINFO).
- Approche avec postprocesseur : utilisez le tokenizer
- Racinisation. Ramener chaque token à sa racine améliore le rappel de recherche en faisant correspondre des variantes morphologiques qui partagent la même racine.
Par exemple, avec la racinisation anglaise, “running”, “runs” et “run” sont tous ramenés à “run”, de sorte qu’une query sur l’une de ces variantes correspond à toutes.
ClickHouse fournit une fonction stem intégrée pour plusieurs langues.
Exemple :
stem(str, 'en') - Normalisation de la casse. Conversion des tokens en minuscules ou en majuscules pour permettre une correspondance insensible à la casse, par exemple lower, lowerUTF8. Pour la conversion en minuscules et en majuscules, nous recommandons un préprocesseur plutôt qu’un postprocesseur.
Array(String), le postprocesseur continue d’opérer sur les tokens individuels en tant que valeurs String simples.
L’utilisation de fonctions non déterministes est interdite.
Le postprocesseur est appliqué à chaque token généré lors de la construction de l’index (pour le tokenizer array, chaque élément du tableau est un token). Au moment de l’exécution de la requête, le comportement dépend de la fonction :
- Pour
hasToken,hasAllTokens,hasAnyTokensethasPhrase(avec n’importe quel tokenizer pris en charge) : le postprocesseur est appliqué à la fois aux tokens du haystack et au needle de recherche, ce qui permet une correspondance entièrement normalisée (par ex. une recherche insensible à la casse). PourhasPhrase, les tokens posttraités sont positionnés de façon compacte : ainsi, si le postprocesseur supprime un token, cela ne crée aucun écart de position et l’expression continue de correspondre malgré tout — par ex., avec un postprocesseur de stop words qui supprimethe,hasPhrase(col, 'see cat')correspond à un documentsee the cat. - Pour toutes les autres fonctions (
=,IN,has,hasAny,hasAll,mapContains*) : seul le needle de recherche est posttraité pour la recherche avec indication d’index ; le prédicat au niveau des lignes continue, lui, de comparer les valeurs d’origine de la colonne.
- Supprimez les stop words à l’aide d’une expression de posttraitement :
- Supprimez les horodatages à l’aide d’une expression de post-traitement :
- Supprimez les horodatages à l’aide d’une expression de prétraitement :
- Supprimez les horodatages au moyen d’une expression combinant un préprocesseur et un postprocesseur :
- Réduisez les tokens à leur racine à l’aide d’une expression de post-traitement :
=, IN, startsWith, endsWith, LIKE, mapContains*), l’index de texte sert uniquement à ignorer les blocs de données non pertinents ; ClickHouse vérifie ensuite chaque ligne conservée à l’aide du prédicat d’origine sur les données de la colonne d’origine.
Pour les fonctions de recherche de tokens (hasToken, hasAllTokens, hasAnyTokens), l’index de texte constitue le principal mécanisme d’évaluation : ClickHouse normalise le needle à l’aide du même préprocesseur, tokenizer et postprocesseur que ceux appliqués lors de la création de l’index, puis utilise cette forme normalisée aussi bien pour les parts de la table indexées que non indexées. En présence d’un postprocesseur, les tokens du haystack sont également normalisés au moment de la requête (pour n’importe quel tokenizer, et pas seulement array), de sorte que les deux côtés de la comparaison sont transformés de manière cohérente et que le résultat ne dépend ni d’une lecture directe de l’index (paramètre query_plan_direct_read_from_text_index), ni du fait qu’une part donnée dispose d’un index matérialisé — par exemple, pour activer une correspondance insensible à la casse pour hasAllTokens(col, ['FOO']) avec un postprocesseur lower.
Sans support_phrase_search, hasPhrase utilise l’index uniquement comme indication et vérifie chaque ligne conservée à l’aide du prédicat d’origine ; un postprocesseur normalise en outre la phrase et les tokens du haystack de la même manière, afin que le résultat soit indépendant du chemin de lecture, et les tokens supprimés par le postprocesseur ne rompent pas l’adjacence de la phrase. Avec support_phrase_search = 1, hasPhrase utilise des lectures directes exactes (tout en appliquant le postprocesseur, le cas échéant).
Les tokens de recherche que le postprocesseur transforme en chaîne vide sont ignorés, c’est-à-dire traités comme absents de la phrase de recherche.
¹
LIKE et match utilisent la lecture directe comme indice pour les tokenizers listés ; sinon, ils reviennent à un balayage exhaustif.
LIKE prend en outre en charge une lecture directe (sans indice) (activée via use_text_index_like_evaluation_by_dictionary_scan) pour les tokenizers splitByNonAlpha et array, sans préprocesseur ni postprocesseur.
² ILIKE est uniquement pris en charge via la lecture directe (sans indice) (use_text_index_like_evaluation_by_dictionary_scan = 1, tokenizer splitByNonAlpha ou array).
Il n’y a pas de solution de repli consistant à utiliser l’index comme indice : si le paramètre est désactivé ou si le tokenizer ne fait pas partie de l’ensemble pris en charge, l’index n’est pas utilisé pour ILIKE.
Le préprocesseur, s’il est présent, doit être lower ou upper ; les postprocesseurs ne sont pas pris en charge.
Expérimental : argument de prise en charge de la recherche de phrase (facultatif).
Le paramètre expérimental support_phrase_search (par défaut : 0) contrôle si l’index stocke les positions des tokens.
Lorsqu’il est défini sur 1, l’index stocke également des données de position (dans un fichier .pos), ce qui permet une correspondance exacte des expressions via des lectures directes pour la fonction hasPhrase.
Le stockage des positions augmente la taille de l’index sur disque et le coût d’écriture ; il est donc optionnel.
Le format sur disque n’est pas encore stable ; ce paramètre est donc expérimental et pourrait changer dans une prochaine version.
La création d’un index avec support_phrase_search = 1 nécessite donc que le paramètre MergeTree allow_experimental_text_index_phrase_search soit activé.
Définissez support_phrase_search = 0 (la valeur par défaut) pour conserver un stockage reposant uniquement sur des posting lists ; les index de texte créés sans cet argument restent sans positions.
Granularité de l’index.
Les index de texte sont implémentés dans ClickHouse comme un type de skip indexes.
Cependant, contrairement aux autres skip indexes, les index de texte utilisent une granularité infinie (100 millions).
Cela est visible dans la définition de table d’un index de texte.
Exemple :
Query
Response
Utiliser un index de texte
Nous recommandons d’utiliser les fonctions
hasAnyTokens et hasAllTokens pour interroger l’index de texte ; voir ci-dessous.
Ces fonctions fonctionnent avec tous les tokenizers disponibles et toutes les expressions possibles de préprocesseur et de postprocesseur.
Comme les autres fonctions prises en charge sont apparues avant l’index de texte, elles ont dû conserver leur comportement historique dans de nombreux cas (par exemple, sans prise en charge du préprocesseur ou du postprocesseur).Fonctions prises en charge
WHERE ou les clauses PREWHERE :
=
= (equals) correspond à l’intégralité du terme de recherche donné.
Exemple :
IN
IN (in) est similaire à equals, mais correspond à l’ensemble des termes de recherche.
Exemple :
NOT IN (notIn) n’est pas pris en charge par l’index de texte.LIKE et match
Ces fonctions utilisent actuellement l’index de texte pour le filtrage uniquement si le tokenizer de l’index est
splitByNonAlpha, ngrams ou sparseGrams.NOT LIKE (notLike) n’est pas pris en charge par l’index de texte.LIKE (like) et la fonction match avec des index de texte, ClickHouse doit pouvoir extraire des tokens complets à partir du terme recherché.
Pour un index utilisant le tokenizer ngrams, c’est le cas si la longueur des chaînes recherchées entre les jokers est égale ou supérieure à la longueur du ngram.
Exemple pour l’index de texte avec le tokenizer splitByNonAlpha :
support dans l’exemple pourrait correspondre à support, supports, supporting, etc.
Ce type de requête est une recherche par sous-chaîne et ne peut pas être accéléré par un index de texte.
Pour qu’un index de texte puisse être utilisé avec des requêtes LIKE, le motif LIKE doit être réécrit comme suit :
support garantissent que le terme peut être extrait en tant que token.
Heureusement, il existe un cas particulier où ClickHouse peut exploiter l’index inversé pour accélérer considérablement les requêtes LIKE.
Consultez la section sur l’optimisation des performances des requêtes LIKE/ILIKE pour plus de détails.
multiSearchAny and multiMatchAny
LIKE et match (voir ci-dessus) : ClickHouse doit pouvoir extraire des tokens complets de chaque motif recherché, et la liste des motifs doit être constante.
Une granule est lue si l’un des motifs peut y être présent.
Pour multiMatchAny, si un seul motif ne peut pas être ramené à une contrainte sur les tokens (par exemple .*, qui correspond à n’importe quel document), l’index de texte intégral ne peut pas être utilisé et la requête bascule sur une analyse complète.
Comme pour LIKE et match, la recherche par sous-chaîne et par expression régulière fonctionne mieux avec les tokenizers ngrams et sparseGrams.
Ces tokenizers indexent des n-grams de caractères qui se chevauchent, de sorte qu’un motif recherché est décomposé en n-grams présents dans l’index partout où il apparaît comme sous-chaîne, qu’il commence ou se termine au milieu d’un mot ou non.
Un motif recherché peut donc être utilisé tel quel, à condition qu’il soit au moins aussi long que la taille du n-gram.
Example pour l’index de texte intégral avec le tokenizer ngrams :
splitByNonAlpha, en revanche, n’indexe que des tokens complets (des mots entiers).
Comme un motif peut commencer ou se terminer au milieu d’un mot, ClickHouse supprime les tokens de tête et de fin de chaque motif, de sorte que l’index ne puisse écarter des granules qu’en s’appuyant sur des tokens complets.
Pour que la recherche par sous-chaîne et par expression régulière utilise l’index avec splitByNonAlpha, entourez chaque motif de caractères séparateurs (comme des espaces) afin qu’il forme un ou plusieurs tokens complets.
Exemple d’index de texte avec le tokenizer splitByNonAlpha :
startsWith et endsWith
LIKE, les fonctions startsWith et endsWith ne peuvent utiliser un index de texte que si des tokens complets peuvent être extraits du terme recherché.
Pour un index avec le tokenizer ngrams, c’est le cas si la longueur des chaînes recherchées entre les wildcards est égale ou supérieure à celle du ngram.
Lorsqu’un index de texte utilise un postprocesseur, ces fonctions peuvent toujours utiliser l’index en mode Hint si les tokens d’indice extraits restent non vides après normalisation. Si la normalisation supprime tous les tokens d’indice, l’index n’est pas utilisé pour ce prédicat.
Exemple d’index de texte avec le tokenizer splitByNonAlpha :
clickhouse est considéré comme un token.
support n’est pas un token, car il peut correspondre à support, supports, supporting, etc.
Pour trouver toutes les rows qui commencent par clickhouse supports, veuillez terminer le motif de recherche par un espace à la fin :
endsWith doit être utilisé avec un espace au début :
hasToken
hasToken comporte certains pièges lorsqu’elle est utilisée pour des recherches dans des index de texte avec des tokenizers autres que splitByNonAlpha et/ou des expressions de prétraitement/post-traitement.
Nous recommandons plutôt d’utiliser hasAnyTokens et hasAllTokens.Les variantes insensibles à la casse hasTokenCaseInsensitive et hasTokenCaseInsensitiveOrNull ne tiennent pas compte des index de texte — elles s’exécutent toujours comme une analyse complète de la table, même sur des colonnes indexées en texte. Pour une correspondance insensible à la casse, utilisez un préprocesseur ou postprocesseur lower(...) et combinez-le avec hasToken / hasAllTokens / hasAnyTokens.hasAnyTokens and hasAllTokens
hasPhrase
hasAllTokens, qui exige seulement que tous les tokens soient présents quelque part, hasPhrase exige qu’ils apparaissent sous la forme d’une séquence consécutive.
L’expression de recherche est tokenisée à l’aide du même tokenizer configuré pour la colonne indexée.
Lorsque l’index de texte utilise un postprocesseur, l’expression de recherche est également normalisée avant la recherche dans l’index.
Notez que la fonction nécessite l’un des tokenizers splitByNonAlpha, splitByString, ngrams ou asciiCJK.
Exemple :
has
hasAny et hasAll
mapContains
mapContainsKey) fait correspondre aux clés d’une map les tokens extraits de la chaîne recherchée.
Le comportement est similaire à celui de la fonction equals avec une colonne String.
L’index textuel n’est utilisé que s’il a été créé sur une expression mapKeys(map).
Exemple :
mapContainsValue
equals sur une colonne String.
L’index de texte n’est utilisé que s’il a été créé sur une expression mapValues(map).
Exemple :
mapContainsKeyLike et mapContainsValueLike
operator[]
mapKeys(map) ou mapValues(map), ou sur les deux.
Exemple :
Array(T) et Map(K, V) avec l’index de texte intégral.
Indexation des colonnes Array(String)
clickhouse) nécessite de parcourir toutes les lignes :
keywords de chaque ligne.
Pour remédier à ce problème de performances, nous définissons un index de texte intégral pour la colonne keywords :
Indexation des colonnes de type Map
Indexation des colonnes JSON
JSON de trois façons :
- Index sur des sous-colonnes spécifiques — créez un index de texte sur un chemin JSON connu, comme pour une colonne classique. Cela indexe les valeurs de ce chemin.
- Index basés sur les chemins avec JSONAllPaths — indexent tous les chemins présents dans chaque granule afin d’ignorer les granules qui ne peuvent pas contenir le chemin recherché. Comme pour les colonnes
Map. - Index basés sur les valeurs avec JSONAllValues — indexent toutes les valeurs de tous les chemins JSON afin d’accélérer la recherche en texte intégral sur n’importe quelle sous-colonne JSON avec un seul index.
Index sur des sous-colonnes spécifiques
- Chemin typé déclaré dans l’indication de type JSON — accès direct par son nom :
json.a. - Chemin dynamique avec conversion de type explicite — utilisez la syntaxe de cast
:::json.b::String.
Query
Query
Response
Query
Response
Index basés sur les chemins avec JSONAllPaths
Map, des index de texte peuvent être créés sur des colonnes JSON à l’aide de JSONAllPaths.
L’index stocke l’ensemble des chemins JSON présents dans chaque granule et les utilise pour sauter les granules où le chemin recherché est absent.
Exemple de définition d’index :
Query
EXPLAIN indexes = 1 pour vérifier que le skip index est utilisé.
Lorsqu’un chemin n’existe que dans une seule part, l’index permet d’ignorer l’autre part.
Exemple :
Query
Response
Query
Response
IS NOT NULL utilise également l’index — il ignore les granules où le chemin est absent (puisque la valeur serait NULL) :
Exemple :
Query
Response
Index basés sur les valeurs avec JSONAllValues
JSONAllValues.
JSONAllValues renvoie toutes les valeurs d’une colonne JSON sous forme de Array(String).
Les valeurs de types de données non textuels (par exemple, les entiers et les tableaux) sont converties en représentation textuelle.
Un index de texte construit avec JSONAllValues indexe ces représentations textuelles sur tous les chemins JSON de chaque ligne.
Cet index peut ensuite accélérer les requêtes qui filtrent sur des sous-colonnes JSON individuelles.
Lorsqu’une requête filtre sur une sous-colonne spécifique (par exemple, data.user_name = 'alice'), l’index de texte peut rapidement ignorer les lignes (et les granules) qui ne contiennent pas les tokens recherchés dans leurs valeurs JSON.
L’index peut produire des faux positifs lorsque différents chemins JSON contiennent les mêmes tokens.
Par exemple, si la ligne 1 contient
{"a": "hello", "b": "world"} et qu’une requête recherche data.a = 'world', l’index de texte ne peut pas distinguer que world appartient au chemin b et non à a.
Dans ce cas, l’index n’ignorera pas la ligne, et le filtre sur les données réelles de la colonne se chargera de l’évaluation finale.
Le comportement est le même que dans les autres cas d’usage des index de texte, où l’index sert de préfiltre rapide.Création de l’index
Types de requêtes pris en charge
String, ainsi que la fonction equals pour toutes les colonnes.
Accès aux sous-colonnes :
CAST explicite :
IN :
Recherche d’expressions
While she stayed in Tokyo, the weather was great. correspond au filtre.
À l’inverse, une recherche d’expression consiste à faire correspondre les tokens dans l’ordre indiqué.
Par exemple,
weather in Tokyo, comme How is the weather in Tokyo? ?
L’index de texte accélère la recherche d’expressions en faisant l’intersection des posting lists de tous les tokens de l’expression afin d’identifier les granules candidates.
Dans ces granules, ClickHouse vérifie ensuite que les tokens sont exactement adjacents.
Ce processus est relativement coûteux et plus lent que les requêtes de recherche textuelle classiques.
Pour accélérer les requêtes de recherche d’expressions, veuillez activer le stockage des positions dans l’index de texte (voir Optional parameters ci-dessus).
hasPhrase peut être utilisé avec les tokenizers splitByNonAlpha, splitByString, ngrams et asciiCJK.
La chaîne correspondant à l’expression fournie est tokenisée à l’aide du tokenizer de l’index.
Les caractères séparateurs de l’expression sont ignorés : hasPhrase(text, 'quick+brown') équivaut à hasPhrase(text, 'quick brown'), à condition que splitByNonAlpha soit utilisé comme tokenizer.
Exemple
Query
Response
'New weather in York') ne correspond pas, car les tokens ne sont pas dans le bon ordre.
La ligne 3 ('weather in New Orleans') ne correspond pas, car elle ne contient pas le token 'York'.
Optimisation des performances
lecture directe
- Le paramètre query_plan_direct_read_from_text_index (
truepar défaut) indique si la lecture directe est activée de manière générale. - Le paramètre use_skip_indexes_on_data_read était un prérequis pour la lecture directe dans les versions de ClickHouse < 26.4.
hasToken, hasAllTokens et hasAnyTokens.
Si l’index de texte est défini avec un tokenizer array, la lecture directe est également prise en charge pour les fonctions equals, has, hasAny, hasAll, mapContainsKey et mapContainsValue.
Ces fonctions peuvent également être combinées avec les opérateurs AND, OR et NOT.
Les clauses WHERE ou PREWHERE peuvent également contenir des filtres supplémentaires autres que les fonctions de recherche textuelle (sur des colonnes de texte ou d’autres colonnes) - dans ce cas, l’optimisation de lecture directe sera tout de même utilisée, mais sera moins efficace (elle s’applique uniquement aux fonctions de recherche textuelle prises en charge).
Pour vérifier qu’une requête utilise la lecture directe, exécutez-la avec EXPLAIN PLAN actions = 1.
À titre d’exemple, une requête avec la lecture directe désactivée
query_plan_direct_read_from_text_index = 1
__text_index_<index_name>_<function_name>_<id>.
Si cette colonne est présente, la lecture directe est utilisée.
Si la clause WHERE ne contient que des fonctions de recherche textuelle, la requête peut éviter complètement de lire les données de la colonne et tirer le plus grand bénéfice en termes de performances de la lecture directe.
Cependant, même si la colonne de texte est utilisée ailleurs dans la requête, la lecture directe apportera tout de même un gain de performances.
Lecture directe comme indice
La lecture directe comme indice repose sur les mêmes principes que la lecture directe normale, mais ajoute en plus un filtre supplémentaire construit à partir des données de l’index de texte, sans éliminer la colonne de texte sous-jacente.
Elle est utilisée pour les fonctions pour lesquelles une lecture uniquement depuis l’index de texte produirait des faux positifs.
Les fonctions prises en charge sont : like, startsWith, endsWith, equals, has, hasPhrase, mapContainsKey et mapContainsValue.
Le filtre supplémentaire peut apporter une sélectivité supplémentaire pour restreindre davantage le jeu de résultats en combinaison avec d’autres filtres, ce qui aide à réduire la quantité de données lues depuis d’autres colonnes.
La lecture directe comme indice est contrôlée par le paramètre query_plan_text_index_add_hint (activé par défaut).
Exemple de requête sans indice :
query_plan_text_index_add_hint = 1
__text_index_...) a été ajoutée à la condition de filtrage.
Grâce à l’optimisation PREWHERE, la condition de filtrage est décomposée en trois conjonctions distinctes, appliquées par ordre croissant de complexité de calcul.
Pour cette requête, l’ordre d’application est __text_index_..., puis greaterOrEquals(...), et enfin like(...).
Cet ordre permet d’ignorer encore plus de granules de données que l’index de texte et le filtre d’origine, avant de lire les colonnes volumineuses utilisées dans la requête après la clause WHERE, ce qui réduit encore la quantité de données à lire.
Requêtes LIKE/ILIKE
%<alpha-numeric-characters-without-spaces>% et que le tokenizer de l’index de texte est splitByNonAlpha ou array, ClickHouse exploite l’index inversé pour accélérer considérablement les requêtes LIKE/ILIKE. Pour cela, ClickHouse parcourt le dictionnaire de l’index inversé au lieu d’effectuer un scan complet de la table afin de trouver le motif correspondant.
Lorsque l’optimisation est activée, les requêtes LIKE/ILIKE devraient être nettement plus rapides qu’un scan complet de la table. Cependant, si le motif correspond à la majorité des tokens du dictionnaire, les performances peuvent être moins bonnes qu’avec un scan complet de la table. Heureusement, un mécanisme de repli permet d’éviter cela.
L’optimisation est contrôlée par un paramètre :
Le mécanisme de repli est contrôlé par deux paramètres :
Cette optimisation ne prend en charge que les fonctions like et ilike.
Mise en cache
Paramètres du cache des jetons
Paramètres du cache des en-têtes
Paramètres du cache des listes de postings
Limites
- La matérialisation d’index de texte comportant un grand nombre de tokens (par ex. 10 milliards de tokens) peut consommer une quantité importante de mémoire. La
matérialisation d’un index de texte peut se produire directement (
ALTER TABLE <table> MATERIALIZE INDEX <index>) ou indirectement lors des fusions de parts. - Il n’est pas possible de matérialiser des index de texte sur des parts de plus de 4.294.967.296 (= 2^32 = env. 4,2 milliards) lignes. Sans index de texte matérialisé, les requêtes reviennent à une recherche brute-force lente dans la part. Dans le pire des cas, supposons qu’une part contienne une seule colonne de type String et que le paramètre MergeTree
max_bytes_to_merge_at_max_space_in_pool(par défaut : 150 GB) n’ait pas été modifié. Dans ce cas, cela se produit si la colonne contient en moyenne moins de 29,5 caractères par ligne. En pratique, les tables contiennent aussi d’autres colonnes et le seuil est alors plusieurs fois plus faible (selon le nombre, le type et la taille des autres colonnes).
Index de texte vs index basés sur des filtres de Bloom
bloom_filter, ngrambf_v1, tokenbf_v1, sparse_grams), mais ces deux types d’index diffèrent fondamentalement par leur conception et leurs cas d’usage visés :
Index à filtre de Bloom
- Reposent sur des structures de données probabilistes qui peuvent produire des faux positifs.
- Peuvent uniquement répondre à des questions d’appartenance à un ensemble, c.-à-d. déterminer si la colonne peut contenir le token X ou si elle ne contient certainement pas X.
- Stockent des informations au niveau des granules afin de permettre d’ignorer de larges plages lors de l’exécution d’une requête.
- Sont difficiles à paramétrer correctement (voir ici pour un exemple).
- Sont relativement compacts (quelques kilo-octets ou mégaoctets par part).
- Construisent un index inversé déterministe sur des tokens. L’index lui-même ne peut pas produire de faux positifs.
- Sont spécifiquement optimisés pour les charges de travail de recherche textuelle.
- Stockent des informations au niveau des lignes, ce qui permet une recherche de termes efficace.
- Sont relativement volumineux (de dizaines à des centaines de mégaoctets par part).
- Ils ne prennent pas en charge la tokenisation ni le prétraitement avancés.
- Ils ne prennent pas en charge la recherche sur plusieurs tokens.
- Ils n’offrent pas les performances attendues d’un index inversé.
- Ils fournissent la tokenisation et le prétraitement
- Ils prennent efficacement en charge
hasAllTokens,LIKE,matchet des fonctions de recherche textuelle similaires. - Ils offrent une bien meilleure capacité de passage à l’échelle pour les grands corpus textuels.
Détails d’implémentation
- un dictionnaire qui associe chaque token à une liste de postings, et
- un ensemble de listes de postings, chacune représentant un ensemble de numéros de ligne.
dictionary_block_size).
Un fichier de blocs de dictionnaire (.dct) contient tous les blocs de dictionnaire de toutes les index granules d’une part.
Fichier d’en-tête d’index (.idx)
Le fichier d’en-tête d’index contient, pour chaque bloc de dictionnaire, le premier token du bloc et son décalage relatif dans le fichier des blocs de dictionnaire.
Cette structure d’index sparse est similaire à l’index de clé primaire sparse) de ClickHouse.
Fichier des listes de postings (.pst)
Les listes de postings de tous les tokens sont disposées séquentiellement dans le fichier des listes de postings.
Pour économiser de l’espace tout en permettant des opérations rapides d’intersection et d’union, les listes de postings sont stockées sous forme de bitmaps Roaring.
Si la liste de postings dépasse posting_list_block_size, elle est divisée en plusieurs blocs, stockés séquentiellement dans le fichier des listes de postings.
Fichier des positions (.pos)
Facultatif, uniquement si l’argument d’index support_phrase_search = 1.
Il stocke les positions des tokens dans les lignes correspondantes.
Fusion des index de texte
Lorsque des data parts sont fusionnées, l’index de texte n’a pas besoin d’être reconstruit à partir de zéro ; il peut au contraire être fusionné efficacement dans une étape distincte du processus de fusion.
Au cours de cette étape, les dictionnaires triés des index de texte de chaque part d’entrée sont lus et combinés en un nouveau dictionnaire unifié.
Les numéros de ligne dans les listes de postings sont également recalculés afin de refléter leurs nouvelles positions dans la data part fusionnée, à l’aide d’une correspondance entre anciens et nouveaux numéros de ligne créée pendant la phase initiale de fusion.
Cette méthode de fusion des index de texte est similaire à la manière dont les projections avec la colonne _part_offset sont fusionnées.
Si l’index n’est pas matérialisé dans la part source, il est construit, écrit dans un fichier temporaire, puis fusionné avec les index des autres parts et ceux des autres fichiers d’index temporaires.
Débogage
La table function mergeTreeTextIndex peut être utilisée pour inspecter les index de texte.
Exemple : jeu de données Hacker News
hackernews :
ALTER TABLE pour ajouter un index de texte sur la colonne comment, puis le matérialiser :
hasToken, hasAnyTokens et hasAllTokens.
Les exemples suivants illustrent l’écart de performances spectaculaire entre un balayage d’index standard et l’optimisation de lecture directe.
1. Utilisation de hasToken
hasToken vérifie si le texte contient un token précis.
Nous rechercherons le token sensible à la casse ‘ClickHouse’.
Lecture directe désactivée (scan standard)
Par défaut, ClickHouse utilise le skip index pour filtrer les granules, puis lit les données de colonne de ces granules.
Nous pouvons simuler ce comportement en désactivant la lecture directe.
lecture directe est plus de 45 fois plus rapide (0.362s contre 0.008s) et traite nettement moins de données (9.51 GB contre 3.15 MB) en lisant uniquement l’index.
2. Utilisation de hasAnyTokens
hasAnyTokens vérifie si le texte contient au moins un des tokens fournis.
Nous allons rechercher des commentaires contenant soit ‘love’, soit ‘ClickHouse’.
Lecture directe désactivée (Standard scan)
3. Utilisation de hasAllTokens
hasAllTokens vérifie si le texte contient tous les tokens fournis.
Nous allons rechercher des commentaires contenant à la fois ‘love’ et ‘ClickHouse’.
Lecture directe désactivée (scan standard)
Même avec la lecture directe désactivée, le skip index standard reste efficace.
Il ramène les 28.7M lignes à seulement 147.46K lignes, mais il doit toujours lire 57.03 MB dans la colonne.
4. Recherche composée : OR, AND, NOT, …
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) serait la syntaxe à privilégier, car plus efficace.
- Blog : Annonce de la disponibilité générale de la recherche en texte intégral dans ClickHouse
- Blog : Concevoir une recherche en texte intégral haute performance pour le stockage objet
- Vidéo : Introduction à la recherche en texte intégral dans ClickHouse
- Vidéo : Sous le capot : la recherche en texte intégral à l’échelle et à la vitesse de ClickHouse
- Présentation : Dans les coulisses de la recherche en texte intégral de ClickHouse : rapide, native et columnaire
- Présentation : Index inversés de base de données : pourquoi, quoi et comment, FOSDEM 2026