Création d’un index de texte intégral
tokenizer. L’argument tokenizer spécifie le tokenizer :
splitByNonAlphadécoupe les chaînes sur les caractères ASCII non alphanumériques (voir aussi la fonction splitByNonAlpha).splitByString(S)découpe les chaînes à l’aide de certaines chaînes séparatricesSdéfinies par l’utilisateur (voir aussi la fonction splitByString). Les séparateurs peuvent être indiqués à l’aide d’un paramètre optionnel, 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 indiquée (par exemple,tokenizer = splitByString), est un espace unique[' '].ngrams(N)découpe les chaînes enN-grammes de taille identique (voir aussi la fonction ngrams). La longueur des ngrammes peut être indiquée à l’aide d’un paramètre entier optionnel compris entre 2 et 8, par exempletokenizer = ngrams(3). La taille de ngramme par défaut, si elle n’est pas explicitement indiquée (par exemple,tokenizer = ngrams), est 3.arrayn’effectue aucune tokenisation, c’est-à-dire que chaque valeur de ligne constitue un token (voir aussi la fonction array).sparseGrams(min_length, max_length, min_cutoff_length)— utilise le même algorithme que la fonction sparseGrams pour découper une chaîne en tous les ngrammes de longueurmin_lengthainsi qu’en plusieurs ngrammes de plus grande taille jusqu’àmax_length, inclus. Simin_cutoff_lengthest spécifié, seuls les N-grammes dont la longueur est supérieure ou égale àmin_cutoff_lengthsont enregistrés dans l’index. Contrairement àngrams(N), qui ne génère que des N-grammes de longueur fixe,sparseGramsproduit un ensemble de N-grammes de longueur variable dans la plage indiquée, ce qui permet une représentation plus souple du contexte textuel. Par exemple,tokenizer = sparseGrams(3, 5, 4)générera des 3-, 4- et 5-grammes à partir de la chaîne d’entrée et n’enregistrera dans l’index que les 4- et 5-grammes.
Le tokenizer
splitByString applique les séparateurs 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 en ['%', '%21'], la sortie sera ['21abc'].
Dans la plupart des cas, vous voudrez que la correspondance privilégie d’abord les séparateurs les plus longs.
Cela peut généralement être obtenu en passant les chaînes séparatrices par ordre décroissant de longueur.
Si les chaînes séparatrices forment un code préfixe, elles peuvent être passées dans n’importe quel ordre.preprocessor. L’argument facultatif preprocessor est une expression qui transforme la chaîne d’entrée avant la tokenization.
Les cas d’usage typiques de l’argument preprocessor incluent :
- La conversion des chaînes d’entrée en minuscules (ou en majuscules) pour permettre une correspondance insensible à la casse, par ex. lower, lowerUTF8 ; voir le premier exemple ci-dessous.
- La normalisation UTF-8, par ex. normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, toValidUTF8.
- La suppression ou la transformation de caractères ou de sous-chaînes indésirables, par ex. extractTextFromHTML, substring, idnaEncode.
preprocessor doit transformer une valeur d’entrée de type String ou FixedString en une valeur du même type.
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))
preprocessor ne doit référencer que la colonne sur laquelle le text index est défini.
L’utilisation de fonctions non déterministes n’est pas autorisée.
Les fonctions hasToken, hasAllTokens et hasAnyTokens utilisent le preprocessor pour transformer d’abord le terme de recherche avant de le tokeniser.
Par exemple :
Paramètres avancés facultatifs
Paramètres avancés facultatifs
Les valeurs par défaut des paramètres avancés suivants conviennent dans la quasi-totalité des situations.
Nous ne recommandons pas de les modifier.Le paramètre facultatif
dictionary_block_size (par défaut : 128) spécifie la taille des blocs du dictionnaire en lignes.Le paramètre facultatif dictionary_block_frontcoding_compression (par défaut : 1) indique si les blocs du dictionnaire utilisent le front coding comme méthode de compression.Le paramètre facultatif max_cardinality_for_embedded_postings (par défaut : 16) spécifie le seuil de cardinalité en dessous duquel les listes de postings doivent être intégrées aux blocs du dictionnaire.Le paramètre facultatif bloom_filter_false_positive_rate (par défaut : 0.1) spécifie le taux de faux positifs du filtre de Bloom du dictionnaire.Utilisation d’un index de texte
Fonctions prises en charge
WHERE d’une requête SELECT :
= and !=
= (equals) and != (notEquals ) correspondent exactement au terme de recherche indiqué.
Exemple :
= et !=, mais les recherches d’égalité et d’inégalité n’ont de sens qu’avec le tokenizer array (auquel cas l’index stocke les valeurs complètes des lignes).
IN et NOT IN
IN (in) et NOT IN (notIn) sont similaires aux fonctions equals et notEquals, mais permettent de faire correspondre tous (IN) ou aucun (NOT IN) des termes de recherche.
Exemple :
= et != s’appliquent, c’est-à-dire que IN et NOT IN n’ont de sens qu’en conjonction avec le tokenizer array.
LIKE, NOT LIKE et match
Ces fonctions utilisent actuellement l’index de texte pour le filtrage uniquement si le tokenizer de l’index est
splitByNonAlpha ou ngrams.LIKE like, NOT LIKE (notLike) et la fonction match avec des index de texte, ClickHouse doit pouvoir extraire des tokens complets à partir du terme recherché.
Exemple :
support dans l’exemple pourrait correspondre à support, supports, supporting, etc.
Ce type de requête est une requête de sous-chaîne et ne peut pas être accélérée par un index de texte intégral.
Pour tirer parti d’un index de texte intégral pour les requêtes LIKE, le motif LIKE doit être réécrit de la manière suivante :
support garantissent que le terme peut être extrait comme token.
startsWith and endsWith
LIKE, les fonctions startsWith et endsWith ne peuvent utiliser un index de texte que si des tokens complets peuvent être extraits du terme de recherche.
Exemple :
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 lignes qui commencent par clickhouse supports, veuillez terminer le motif de recherche par un espace final :
endsWith doit être utilisé avec une espace initiale :
hasToken and hasTokenOrNull
hasToken et hasTokenOrNull sont celles qui offrent les meilleures performances avec l’index text.
hasAnyTokens and hasAllTokens
has
mapContains
mapContainsKey) vérifie la présence d’un seul token parmi les clés d’une map.
Exemple :
operator[]
Array(T) et de Map(K, V) avec l’index de texte.
Exemples de prise en charge de Array et Map par l’index de texte intégral.
Indexation de Array(String)
clickhouse) nécessite de parcourir toutes les entrées :
keywords de chaque ligne.
Pour remédier à ce problème de performances, nous pouvons définir un index de texte sur keywords qui crée une structure optimisée pour la recherche, prétraite tous les mots-clés et permet des recherches instantanées :
Important : après avoir ajouté l’index de texte, vous devez le reconstruire pour les données déjà présentes :
Indexation des données de type Map
- Trouve tous les logs avec limitation de débit :
- Recherche tous les logs provenant d’une adresse IP donnée :
Important : après avoir ajouté l’index de texte intégral, vous devez le reconstruire pour les données existantes :
- Trouvez toutes les requêtes faisant l’objet d’une limitation de débit :
- Trouve tous les logs provenant d’une adresse IP spécifique :
Mise en œuvre
Structure de l’index
- 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 du dictionnaire (.dct) contient tous les blocs de dictionnaire de tous les granules d’index d’une part.
Fichier des granules d’index (.idx)
Le fichier des granules d’index contient, pour chaque bloc de dictionnaire, le premier token du bloc, son décalage relatif dans le fichier des blocs du dictionnaire, ainsi qu’un bloom filter pour tous les tokens du bloc.
Cette structure de sparse index est similaire à l’index primaire sparse de ClickHouse).
Le bloom filter permet d’ignorer rapidement les blocs de dictionnaire si le token recherché n’y est pas présent.
Fichier des listes de postings (.pst)
Les listes de postings de tous les tokens sont stockées séquentiellement dans le fichier des listes de postings.
Pour économiser de l’espace tout en permettant des opérations rapides d’intersect et de union, les listes de postings sont stockées sous forme de bitmaps Roaring.
Si la cardinalité d’une liste de postings est inférieure à 16 (configurable via le paramètre max_cardinality_for_embedded_postings), elle est intégrée au dictionnaire.
Lecture directe
- Le paramètre query_plan_direct_read_from_text_index (par défaut : 1), qui indique si la lecture directe est globalement activée.
- Le paramètre use_skip_indexes_on_data_read (par défaut : 1), qui constitue un autre prérequis pour la lecture directe. Notez que, sur les bases de données ClickHouse avec compatibility < 25.10,
use_skip_indexes_on_data_readest désactivé. Vous devez donc soit augmenter la valeur du paramètre compatibility, soit définir explicitementSET use_skip_indexes_on_data_read = 1.
ALTER TABLE ... MATERIALIZE INDEX pour cela).
Fonctions prises en charge
L’optimisation de lecture directe prend en charge les fonctions hasToken, hasAllTokens et hasAnyTokens.
Ces fonctions peuvent également être combinées avec les opérateurs AND, OR et NOT.
La clause WHERE peut également contenir des filtres supplémentaires autres que des 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 elle 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, cela signifie que direct read est utilisé.
Exemple : jeu de données Hackernews
hackernews :
ALTER TABLE pour ajouter un index de texte intégral sur la colonne comment, puis le matérialiser :
hasToken, hasAnyTokens et hasAllTokens.
Les exemples suivants montreront l’écart de performances spectaculaire entre un parcours d’index standard et l’optimisation de lecture directe.
1. Utilisation de hasToken
hasToken vérifie si le texte contient un token unique spécifique.
Nous allons rechercher le token sensible à la casse ‘ClickHouse’.
Lecture directe désactivée (scan standard)
Par défaut, ClickHouse utilise l’index de saut pour filtrer les granules, puis lit les données des colonnes de ces granules.
Nous pouvons simuler ce comportement en désactivant la lecture directe.
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 (scan standard)
3. Utilisation de hasAllTokens
hasAllTokens vérifie si le texte contient tous les jetons indiqués.
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, l’index de saut standard reste efficace.
Il filtre les 28,7 M de lignes pour n’en conserver que 147,46 K, mais il doit tout de même lire 57,03 Mo depuis la colonne.
4. Recherche composée : OR, AND, NOT, …
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) serait la syntaxe à privilégier, car plus efficace.