> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> Trouvez rapidement des termes de recherche dans du texte.

# Recherche en texte intégral avec des index de texte

Les index de texte (également appelés [index inversés](https://en.wikipedia.org/wiki/Inverted_index)) permettent d'effectuer rapidement des recherches en texte intégral dans des données textuelles.
Un index de texte stocke une association entre les tokens et les numéros de ligne qui contiennent chaque token.
Les tokens sont générés par un processus appelé tokenisation.
Par exemple, le tokenizer par défaut de ClickHouse convertit la phrase anglaise "The cat likes mice." en tokens \["The", "cat", "likes", "mice"].

Par exemple, supposons une table avec une seule colonne et trois lignes

```result theme={null}
1: The cat likes mice.
2: Mice are afraid of dogs.
3: I have two dogs and a cat.
```

Les tokens correspondants sont :

```result theme={null}
1: The, cat, likes, mice
2: Mice, are, afraid, of, dogs
3: I, have, two, dogs, and, a, cat
```

En général, nous préférons effectuer des recherches sans distinction entre majuscules et minuscules, c'est pourquoi nous mettons les tokens en minuscules :

```result theme={null}
1: the, cat, likes, mice
2: mice, are, afraid, of, dogs
3: i, have, two, dogs, and, a, cat
```

Nous supprimerons également les mots vides tels que "I", "the" et "and", car ils apparaissent dans presque toutes les lignes :

```result theme={null}
1: cat, likes, mice
2: mice, afraid, dogs
3: have, two, dogs, cat
```

Un index de texte contient alors (en théorie) les informations suivantes :

```result theme={null}
afraid : [2]
cat    : [1, 3]
dogs   : [2, 3]
have   : [3]
likes  : [1]
mice   : [1]
two    : [3]
```

À partir d’un token de recherche, cette structure d’index permet de retrouver rapidement toutes les lignes correspondantes.

<div id="creating-a-text-index">
  ## Création d’un index de texte
</div>

Les index de texte sont disponibles de façon générale (GA) à partir de ClickHouse version 26.2.
Dans ces versions, aucun paramètre particulier n’est nécessaire pour utiliser l’index de texte.
Nous recommandons vivement d’utiliser ClickHouse version 26.2 ou ultérieure pour les cas d’usage en production.

<Note>
  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é](/docs/fr/reference/settings/session-settings#compatibility).
</Note>

Pour créer un index de texte, utilisez la syntaxe suivante :

```sql title="Query" theme={null}
CREATE TABLE table
(
    key UInt64,
    str String,
    INDEX text_idx str TYPE text(
                                -- Mandatory parameters:
                                tokenizer = splitByNonAlpha
                                            | splitByString[(S)]
                                            | asciiCJK
                                            | ngrams[(N)]
                                            | sparseGrams[(min_length[, max_length[, min_cutoff_length]])]
                                            | array
                                -- Optional parameters:
                                [, preprocessor = expression(str)]
                                [, postprocessor = expression(str)]
                                [, support_phrase_search = 0 | 1 ] -- experimental
                                -- Optional advanced parameters:
                                [, dictionary_block_size = D]
                                [, dictionary_block_frontcoding_compression = B]
                                [, posting_list_block_size = C]
                                [, posting_list_codec = 'none' | 'bitpacking' ]
                            )
)
ENGINE = MergeTree
ORDER BY key
```

Les index de texte peuvent être définis sur des colonnes des types suivants :

* [String](/docs/fr/reference/data-types/string) et [FixedString](/docs/fr/reference/data-types/fixedstring),
* [Array(String)](/docs/fr/reference/data-types/array) et [Array(FixedString)](/docs/fr/reference/data-types/array),
* [Map](/docs/fr/reference/data-types/map) (à l’aide des fonctions [mapKeys](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapKeys) et [mapValues](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapValues)), et
* [JSON](/docs/fr/reference/data-types/newjson) (à l’aide des fonctions [JSONAllPaths](/docs/fr/reference/functions/regular-functions/json-functions#JSONAllPaths) et [`JSONAllValues`](/docs/fr/reference/functions/regular-functions/json-functions#JSONAllValues)).

Les colonnes de type [Nullable(T)](/docs/fr/reference/data-types/nullable) et [LowCardinality()](/docs/fr/reference/data-types/lowcardinality) sont également prises en charge, y compris `Array(Nullable(String or FixedString))`.

Autrement, pour ajouter un index de texte à une table existante :

```sql title="Query" theme={null}
ALTER TABLE table
    ADD INDEX text_idx str TYPE text(
                                -- Mandatory parameters:
                                tokenizer = splitByNonAlpha
                                            | splitByString[(S)]
                                            | asciiCJK
                                            | ngrams[(N)]
                                            | sparseGrams[(min_length[, max_length[, min_cutoff_length]])]
                                            | array
                                -- Optional parameters:
                                [, preprocessor = expression(str)]
                                [, postprocessor = expression(str)]
                                [, support_phrase_search = 0 | 1 ] -- experimental
                                -- Optional advanced parameters:
                                [, dictionary_block_size = D]
                                [, dictionary_block_frontcoding_compression = B]
                                [, posting_list_block_size = C]
                                [, posting_list_codec = 'none' | 'bitpacking' ]
                            )

```

Si vous ajoutez un index à une table existante, nous vous recommandons de matérialiser l’index pour les parts de la table existantes (sinon, la recherche sur les parts sans index reviendra à des balayages exhaustifs lents).

```sql title="Query" theme={null}
ALTER TABLE table MATERIALIZE INDEX text_idx SETTINGS mutations_sync = 2;
```

Pour supprimer un index de texte, exécutez

```sql title="Query" theme={null}
ALTER TABLE table DROP INDEX text_idx;
```

**Argument du tokenizer (obligatoire)**. L’argument `tokenizer` précise le tokenizer :

* `splitByNonAlpha` divise les chaînes selon les caractères ASCII non alphanumériques (voir la fonction [splitByNonAlpha](/docs/fr/reference/functions/regular-functions/splitting-merging-functions#splitByNonAlpha)).
* `splitByString(S)` divise les chaînes à l'aide de certaines chaînes séparatrices `S` définies par l'utilisateur (voir la fonction [splitByString](/docs/fr/reference/functions/regular-functions/splitting-merging-functions#splitByString)).
  Les séparateurs peuvent être spécifiés à l'aide d'un paramètre facultatif, par exemple `tokenizer = 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 exemple `tokenizer = splitByString`), est un seul espace `[' ']`.
* `asciiCJK` divise les chaînes en tokens selon les règles de délimitation des mots Unicode (comme dans [Unicode Text Segmentation (UAX #29)](https://unicode.org/reports/tr29/)). 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](https://en.wikipedia.org/wiki/CJK_characters), deviennent des tokens d'un seul caractère.
* `ngrams(N)` divise les chaînes en `N`-grammes de taille identique (voir la fonction [ngrams](/docs/fr/reference/functions/regular-functions/splitting-merging-functions#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 exemple `tokenizer = ngrams(3)`.
  La taille des n-grammes par défaut, si elle n'est pas explicitement spécifiée (par exemple `tokenizer = ngrams`), est de 3.
* `sparseGrams(min_length, max_length, min_cutoff_length)` divise les chaînes en n-grammes de longueur variable d'au moins `min_length` et d'au plus `max_length` caractères (bornes incluses) (voir la fonction [sparseGrams](/docs/fr/reference/functions/regular-functions/string-functions#sparseGrams)).
  Sauf indication explicite, `min_length` et `max_length` valent par défaut 3 et 100.
  Si le paramètre `min_cutoff_length` est fourni, seuls les n-grammes dont la longueur est supérieure ou égale à `min_cutoff_length` sont renvoyés.
  Comparé à `ngrams(N)`, le tokenizer `sparseGrams` produit 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.
* `array` ne réalise aucune tokenisation, c.-à-d. que chaque valeur de ligne constitue un token (voir la fonction [array](/docs/fr/reference/functions/regular-functions/array-functions#array)).

Tous les tokenizers disponibles sont répertoriés dans [system.tokenizers](/docs/fr/reference/system-tables/tokenizers).

<Note>
  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](https://en.wikipedia.org/wiki/Prefix_code), elles peuvent être passées dans un ordre arbitraire.
</Note>

Pour comprendre comment un tokenizer découpe la chaîne d'entrée, vous pouvez utiliser les fonctions [tokens](/docs/fr/reference/functions/regular-functions/splitting-merging-functions#tokens) et [tokensForLikePattern](/docs/fr/reference/functions/regular-functions/splitting-merging-functions#tokensForLikePattern) :

Exemple :

```sql title="Query" theme={null}
SELECT tokens('abc def', 'ngrams', 3);
```

```result title="Response" theme={null}
['abc','bc ','c d',' de','def']
```

*Utilisation de données d’entrée non ASCII.*
Les index de texte peuvent être créés à partir de données textuelles dans n’importe quelle langue et avec n’importe quel jeu de caractères.
Pour le texte non ASCII, le tokenizer `asciiCJK` est recommandé, car il gère correctement les limites de mots Unicode, y compris pour les caractères CJK.

<a id="preprocessor-argument-optional" />**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

1. Conversion en minuscules/majuscules, ou normalisation de la casse pour permettre une correspondance insensible à la casse, par ex., [lower](/docs/fr/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/docs/fr/reference/functions/regular-functions/string-functions#lowerUTF8), [caseFoldUTF8](/docs/fr/reference/functions/regular-functions/string-functions#caseFoldUTF8).
2. Normalisation UTF-8, par ex. [normalizeUTF8NFC](/docs/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFC), [normalizeUTF8NFD](/docs/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFD), [normalizeUTF8NFKC](/docs/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFKC), [normalizeUTF8NFKD](/docs/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFKD), [normalizeUTF8NFKCCasefold](/docs/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFKCCasefold), [toValidUTF8](/docs/fr/reference/functions/regular-functions/string-functions#toValidUTF8).
3. Suppression ou transformation de caractères ou de sous-chaînes indésirables, comme les accents, par ex. [extractTextFromHTML](/docs/fr/reference/functions/regular-functions/string-functions#extractTextFromHTML), [substring](/docs/fr/reference/functions/regular-functions/string-functions#substring), [idnaEncode](/docs/fr/reference/functions/regular-functions/string-functions#idnaEncode), [translate](/docs/fr/reference/functions/regular-functions/string-replace-functions#translate), [removeDiacriticsUTF8](/docs/fr/reference/functions/regular-functions/string-functions#removeDiacriticsUTF8).

L'expression de préprocesseur doit transformer une valeur d'entrée de type [String](/docs/fr/reference/data-types/string) ou [FixedString](/docs/fr/reference/data-types/fixedstring) en une valeur du même type.
Si l'index de texte a été construit sur une colonne de type `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)))`

De plus, l'expression de préprocesseur doit uniquement référencer la colonne ou l'expression sur laquelle l'index de texte est défini.

Exemples :

* `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))`

L'usage de fonctions non déterministes est interdit.

<Note>
  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.
</Note>

Les fonctions [hasToken](/docs/fr/reference/functions/regular-functions/string-search-functions#hasToken), [hasAllTokens](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAllTokens), [hasAnyTokens](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAnyTokens) et [hasPhrase](/docs/fr/reference/functions/regular-functions/string-search-functions#hasPhrase) utilisent le préprocesseur pour d'abord transformer le terme de recherche avant de le découper en tokens.
Notez que, comme le préprocesseur n'est appliqué que sur le chemin de l'index de texte, les résultats de ces fonctions peuvent différer entre les requêtes qui utilisent l'index de texte et celles qui ne l'utilisent pas (par ex. `SETTINGS use_skip_indexes = 0`).

Par exemple,

```sql title="Query" theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx str TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(str))
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM table WHERE hasToken(str, 'Foo');
```

est équivalent à :

```sql title="Query" theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx lower(str) TYPE text(tokenizer = 'splitByNonAlpha')
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM table WHERE hasToken(str, lower('Foo'));
```

Dans ce cas, l’expression du préprocesseur transforme individuellement les éléments du tableau.

Exemple :

```sql title="Query" theme={null}
CREATE TABLE table
(
    arr Array(String),
    INDEX idx arr TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(arr))

    -- This is not legal:
    INDEX idx_illegal arr TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = arraySort(arr))
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM tab WHERE hasAllTokens(arr, 'foo');
```

Pour définir un préprocesseur dans un index de texte sur des colonnes de type [Map](/docs/fr/reference/data-types/map) à la création, les utilisateurs doivent déterminer si l’index est
créé sur les clés ou sur les valeurs du type Map.

Exemple :

```sql title="Query" theme={null}
CREATE TABLE table
(
    map Map(String, String),
    INDEX idx mapKeys(map)  TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(mapKeys(map)))
)
ENGINE = MergeTree
ORDER BY tuple();

SELECT count() FROM tab WHERE hasAllTokens(mapKeys(map), 'foo');
```

<a id="postprocessor-argument-optional" />**Argument `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 :

1. **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)`
2. **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 utilisez `parseDateTimeOrNull` pour 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, utilisez `parseDateTimeBestEffortOrNull(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é comme `ERROR` ou `INFO`).
3. **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](/docs/fr/reference/functions/regular-functions/nlp-functions#stem) intégrée pour plusieurs langues.
   Exemple : `stem(str, 'en')`
4. **Normalisation de la casse**. Conversion des tokens en minuscules ou en majuscules pour permettre une correspondance insensible à la casse, par exemple [lower](/docs/fr/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/docs/fr/reference/functions/regular-functions/string-functions#lowerUTF8).
   Pour la conversion en minuscules et en majuscules, nous recommandons un préprocesseur plutôt qu’un postprocesseur.

L'expression de posttraitement transforme des tokens de type [String](/docs/fr/reference/data-types/string) en tokens du même type.
De plus, l'expression de posttraitement ne doit référencer que la colonne ou l'expression sur laquelle le text index est défini.
Lorsque la colonne est de type `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`, `hasAnyTokens` et `hasPhrase` (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). Pour `hasPhrase`, 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 supprime `the`, `hasPhrase(col, 'see cat')` correspond à un document `see 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.

Exemples :

* Supprimez les stop words à l’aide d’une expression de posttraitement :

```sql theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx(str) TYPE text(
        tokenizer = 'splitByNonAlpha',
        postprocessor = if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str)
    )
)
ENGINE = MergeTree
ORDER BY tuple();
```

* Supprimez les horodatages à l’aide d’une expression de post-traitement :

```sql theme={null}
-- Log lines: '2024-01-15T10:23:45 ERROR connection failed'
-- The splitByString tokenizer (default: whitespace) keeps the full timestamp as one token.
-- parseDateTimeOrNull detects and drops it; non-timestamp words are kept.
CREATE TABLE logs
(
    id   UInt64,
    line String,
    INDEX idx(line) TYPE text(
        tokenizer    = 'splitByString',
        postprocessor = if(isNull(parseDateTimeOrNull(line, '%Y-%m-%dT%H:%i:%S')), line, '')
    )
)
ENGINE = MergeTree ORDER BY id;

-- Only message-level words are indexed; timestamp tokens are not stored.
SELECT count() FROM logs WHERE hasAllTokens(line, ['ERROR']);       -- fast index lookup
SELECT count() FROM logs WHERE hasAllTokens(line, ['2024-01-15T10:23:45']);  -- returns 0: token was never indexed
```

* Supprimez les horodatages à l’aide d’une expression de prétraitement :

```sql theme={null}
-- The preprocessor strips the ISO timestamp prefix before tokenization.
-- Any tokenizer can be used; timestamp characters are never seen by the tokenizer.
CREATE TABLE logs
(
    id   UInt64,
    line String,
    INDEX idx(line) TYPE text(
        tokenizer   = 'splitByNonAlpha',
        preprocessor = replaceRegexpAll(line, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')
    )
)
ENGINE = MergeTree ORDER BY id;
```

* Supprimez les horodatages au moyen d’une expression combinant un préprocesseur et un postprocesseur :

```sql theme={null}
-- Preprocessor strips the timestamp, then lowercases the remainder.
-- Postprocessor drops the severity word (error, info, warn, debug) after tokenization.
-- Result: only substantive message words are stored in the index.
CREATE TABLE logs
(
    id   UInt64,
    line String,
    INDEX idx(line) TYPE text(
        tokenizer    = 'splitByNonAlpha',
        preprocessor = lower(replaceRegexpAll(line, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')),
        postprocessor = if(line IN ('error', 'info', 'warn', 'warning', 'debug', 'critical'), '', line)
    )
)
ENGINE = MergeTree ORDER BY id;

-- Example log line: '2024-01-15T10:23:45 ERROR connection failed'
-- After preprocessor:  'error connection failed'
-- After tokenization:  ['error', 'connection', 'failed']
-- After postprocessor: ['connection', 'failed']   ← 'error' dropped as severity word
SELECT count() FROM logs WHERE hasAllTokens(line, ['connection']);
```

* Réduisez les tokens à leur racine à l’aide d’une expression de post-traitement :

```sql theme={null}
CREATE TABLE table
(
    str String,
    INDEX idx(str) TYPE text(
        tokenizer = 'splitByNonAlpha',
        postprocessor = stem(str, 'en')
    )
)
ENGINE = MergeTree
ORDER BY tuple();

-- The query token 'running' is stemmed to 'run' before the lookup,
-- matching rows that contain 'run', 'runs', 'ran', 'running', etc.
SELECT count() FROM table WHERE hasAllTokens(str, ['running']);
```

**Prise en charge des fonctions**.

Pour les prédicats qui consultent l’index de texte, le préprocesseur et le postprocesseur sont appliqués à la valeur recherchée avant la vérification au niveau du granule, afin que la recherche dans l’index utilise les mêmes tokens que ceux stockés lors de la création de l’index.
Pour la plupart des fonctions (`=`, `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.

| Fonction                                                                                                   | Prend en charge un préprocesseur                                    | Tokenizers compatibles                                   | Prend en charge un postprocesseur |
| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------- |
| `=`                                                                                                        | oui                                                                 | tous                                                     | oui                               |
| `IN`                                                                                                       | oui                                                                 | tous                                                     | oui                               |
| [hasToken](/docs/fr/reference/functions/regular-functions/string-search-functions#hasToken)                     | oui                                                                 | tous (conçu pour `splitByNonAlpha`)                      | oui                               |
| [hasAnyTokens(col, str)](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | oui                                                                 | tous                                                     | oui                               |
| [hasAllTokens(col, str)](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | oui                                                                 | tous                                                     | oui                               |
| [hasAnyTokens(col, arr)](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | non (les éléments du tableau sont utilisés tels quels comme tokens) | tous                                                     | oui                               |
| [hasAllTokens(col, arr)](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | non (les éléments du tableau sont utilisés tels quels comme tokens) | tous                                                     | oui                               |
| [hasPhrase](/docs/fr/reference/functions/regular-functions/string-search-functions#hasPhrase)                   | oui                                                                 | `splitByNonAlpha`, `splitByString`, `ngrams`, `asciiCJK` | oui                               |
| [startsWith](/docs/fr/reference/functions/regular-functions/string-functions#startsWith)                        | oui                                                                 | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | oui                               |
| [endsWith](/docs/fr/reference/functions/regular-functions/string-functions#endsWith)                            | oui                                                                 | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | oui                               |
| [like](/docs/fr/reference/functions/regular-functions/string-search-functions#like)                             | oui¹                                                                | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹  | oui¹                              |
| [match](/docs/fr/reference/functions/regular-functions/string-search-functions#match)                           | oui¹                                                                | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹  | oui¹                              |
| [ilike](/docs/fr/reference/functions/regular-functions/string-search-functions#like)                            | oui² (`lower`/`upper` uniquement)                                   | `splitByNonAlpha`, `array`²                              | non²                              |
| [mapContainsKey](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsKey)             | oui                                                                 | tous                                                     | oui                               |
| [mapContainsValue](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsValue)         | oui                                                                 | tous                                                     | oui                               |
| [mapContainsKeyLike](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike)     | oui                                                                 | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | oui                               |
| [mapContainsValueLike](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) | oui                                                                 | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | oui                               |
| [has](/docs/fr/reference/functions/regular-functions/array-functions#has)                                       | oui                                                                 | `array`                                                  | oui                               |
| [hasAny](/docs/fr/reference/functions/regular-functions/array-functions#hasAny)                                 | oui                                                                 | `array`                                                  | oui                               |
| [hasAll](/docs/fr/reference/functions/regular-functions/array-functions#hasAll)                                 | oui                                                                 | `array`                                                  | oui                               |

¹ `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`](#functions-example-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`](/docs/fr/reference/settings/merge-tree-settings#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.

<Warning>
  Cet argument est expérimental et ne doit être utilisé que pour des tests.
  Activez le paramètre MergeTree [`allow_experimental_text_index_phrase_search`](/docs/fr/reference/settings/merge-tree-settings#allow_experimental_text_index_phrase_search) pour autoriser le stockage des positions.
</Warning>

<details markdown="1">
  <summary>Paramètres avancés facultatifs</summary>

  Les valeurs par défaut des paramètres avancés suivants conviennent dans la quasi-totalité des cas.
  Nous ne recommandons pas de les modifier.

  Le paramètre facultatif `dictionary_block_size` (par défaut : 512) 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 `posting_list_block_size` (par défaut : 1048576) spécifie la taille des blocs de posting lists en lignes.

  Le paramètre facultatif `posting_list_codec` (par défaut : `none`) spécifie le codec de la posting list :

  * `none` - les posting lists sont stockées sans compression supplémentaire.
  * `bitpacking` - applique le [codage différentiel (delta)](https://en.wikipedia.org/wiki/Delta_encoding), suivi du [bit-packing](https://dev.to/madhav_baby_giraffe/bit-packing-the-secret-to-optimizing-data-storage-and-transmission-m70) (chacun dans des blocs de taille fixe). Cela ralentit les requêtes SELECT et n'est pas recommandé pour le moment.

  Les paramètres avancés ci-dessus peuvent également être définis au niveau de la table via les paramètres MergeTree correspondants : [`text_index_dictionary_block_size`](/docs/fr/reference/settings/merge-tree-settings#text_index_dictionary_block_size), [`text_index_dictionary_block_frontcoding_compression`](/docs/fr/reference/settings/merge-tree-settings#text_index_dictionary_block_frontcoding_compression), [`text_index_posting_list_block_size`](/docs/fr/reference/settings/merge-tree-settings#text_index_posting_list_block_size) et [`text_index_posting_list_codec`](/docs/fr/reference/settings/merge-tree-settings#text_index_posting_list_codec).
  Ils s'appliquent à chaque index de texte de la table qui ne spécifie pas explicitement le paramètre.

  Le principal cas d'usage des paramètres au niveau de la table est de modifier les paramètres d'index d'une table existante sans supprimer puis recréer l'index de texte sur toutes les parts de la table.
  La modification d'un paramètre au niveau de la table applique les nouveaux paramètres uniquement aux index de texte construits pour les nouvelles parts ; les parts existantes conservent leur layout actuel.

  Un argument donné dans la définition de l'index prévaut sur le paramètre de table, par exemple :

  ```sql theme={null}
  CREATE TABLE table(
      s String,
      -- Cet index utilise 'bitpacking', en remplaçant la valeur par défaut définie au niveau de la table ci-dessous :
      INDEX idx_a s TYPE text(tokenizer = 'splitByNonAlpha', posting_list_codec = 'bitpacking'),
      -- Cet index hérite de 'none' à partir du paramètre de table :
      INDEX idx_b lower(s) TYPE text(tokenizer = 'splitByNonAlpha'))
  ENGINE = MergeTree()
  ORDER BY tuple()
  SETTINGS text_index_posting_list_codec = 'none';
  ```
</details>

*Granularité de l'index.*
Les index de texte sont implémentés dans ClickHouse comme un type de [skip indexes](/docs/fr/reference/engines/table-engines/mergetree-family/mergetree#skip-index-types).
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 :

```sql title="Query" theme={null}
CREATE TABLE table(
    k UInt64,
    s String,
    INDEX idx s TYPE text(tokenizer = ngrams(2)))
ENGINE = MergeTree()
ORDER BY k;

SHOW CREATE TABLE table;
```

```result title="Response" theme={null}
┌─statement──────────────────────────────────────────────────────────────┐
│ CREATE TABLE default.table                                            ↴│
│↳(                                                                     ↴│
│↳    `k` UInt64,                                                       ↴│
│↳    `s` String,                                                       ↴│
│↳    INDEX idx s TYPE text(tokenizer = ngrams(2)) GRANULARITY 100000000↴│ <-- here
│↳)                                                                     ↴│
│↳ENGINE = MergeTree                                                    ↴│
│↳ORDER BY k                                                            ↴│
│↳SETTINGS index_granularity = 8192                                      │
└────────────────────────────────────────────────────────────────────────┘
```

La granularité d’index très élevée garantit que l’index de texte intégral est créé pour l’intégralité de la partie de données.
Toute granularité d’index explicitement spécifiée est ignorée.

<div id="using-a-text-index">
  ## Utiliser un index de texte
</div>

L'utilisation d'un index de texte dans les requêtes SELECT est simple, car les fonctions courantes de recherche dans les chaînes exploitent automatiquement l'index.
Si aucun index n'existe sur une colonne ou une part de la table, les fonctions de recherche dans les chaînes se rabattent sur de lents parcours exhaustifs.

<Note>
  Nous recommandons d'utiliser les fonctions `hasAnyTokens` et `hasAllTokens` pour interroger l'index de texte ; voir [ci-dessous](#functions-example-hasanytokens-hasalltokens).
  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).
</Note>

<div id="functions-support">
  ### Fonctions prises en charge
</div>

L’index de texte intégral peut être utilisé lorsque des fonctions textuelles sont employées dans la clause `WHERE` ou les clauses `PREWHERE` :

```sql theme={null}
SELECT [...]
FROM [...]
WHERE string_search_function(column_with_text_index)
```

<div id="functions-example-equals">
  #### `=`
</div>

`=` ([equals](/docs/fr/reference/functions/regular-functions/comparison-functions#equals)) correspond à l’intégralité du terme de recherche donné.

Exemple :

```sql theme={null}
SELECT * from table WHERE str = 'Hello';
```

<div id="functions-example-in">
  #### `IN`
</div>

`IN` ([in](/docs/fr/reference/functions/regular-functions/in-functions)) est similaire à `equals`, mais correspond à l’ensemble des termes de recherche.

Exemple :

```sql theme={null}
SELECT * from table WHERE str IN ('Hello', 'World');
```

<Note>
  `NOT IN` (`notIn`) n’est pas pris en charge par l’index de texte.
</Note>

<div id="functions-example-like-match">
  #### `LIKE` et `match`
</div>

<Note>
  Ces fonctions utilisent actuellement l’index de texte pour le filtrage uniquement si le tokenizer de l’index est `splitByNonAlpha`, `ngrams` ou `sparseGrams`.
</Note>

<Note>
  `NOT LIKE` (`notLike`) n’est pas pris en charge par l’index de texte.
</Note>

Pour utiliser `LIKE` ([like](/docs/fr/reference/functions/regular-functions/string-search-functions#like)) et la fonction [match](/docs/fr/reference/functions/regular-functions/string-search-functions#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` :

```sql theme={null}
SELECT count() FROM table WHERE comment LIKE 'support%';
```

`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 :

```sql theme={null}
SELECT count() FROM table WHERE comment LIKE ' support %'; -- or `% support %`
```

Les espaces à gauche et à droite de `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](#like-ilike-queries-perf) pour plus de détails.

<div id="functions-example-multisearchany-multimatchany">
  #### `multiSearchAny` and `multiMatchAny`
</div>

[multiSearchAny](/docs/fr/reference/functions/regular-functions/string-search-functions#multiSearchAny) et sa variante UTF-8 [multiSearchAnyUTF8](/docs/fr/reference/functions/regular-functions/string-search-functions#multiSearchAnyUTF8) vérifient si l’une de plusieurs sous-chaînes littérales est présente dans la chaîne à analyser, et [multiMatchAny](/docs/fr/reference/functions/regular-functions/string-search-functions#multiMatchAny) vérifie si l’une de plusieurs expressions régulières correspond.
Ces fonctions utilisent l’index de texte intégral dans les mêmes conditions que `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` :

```sql theme={null}
SELECT count() FROM table WHERE multiSearchAny(comment, ['clickhouse', 'support']);
```

Le tokenizer `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` :

```sql theme={null}
SELECT count() FROM table WHERE multiSearchAny(comment, [' clickhouse ', ' support ']);
```

<div id="functions-example-startswith-endswith">
  #### `startsWith` et `endsWith`
</div>

Comme `LIKE`, les fonctions [startsWith](/docs/fr/reference/functions/regular-functions/string-functions#startsWith) et [endsWith](/docs/fr/reference/functions/regular-functions/string-functions#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` :

```sql theme={null}
SELECT count() FROM table WHERE startsWith(comment, 'clickhouse support');
```

Dans cet exemple, seul `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 :

```sql theme={null}
startsWith(comment, 'clickhouse supports ')`
```

De même, `endsWith` doit être utilisé avec un espace au début :

```sql theme={null}
SELECT count() FROM table WHERE endsWith(comment, ' olap engine');
```

<div id="functions-example-hastoken">
  #### `hasToken`
</div>

<Note>
  `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`.
</Note>

La fonction [hasToken](/docs/fr/reference/functions/regular-functions/string-search-functions#hasToken) effectue une correspondance avec un seul token donné.

Contrairement aux fonctions mentionnées précédemment, elle ne tokenise pas le terme recherché (elle suppose que l'entrée correspond à un seul token).

Exemple :

```sql theme={null}
SELECT count() FROM table WHERE hasToken(comment, 'clickhouse');
```

<div id="functions-example-hasanytokens-hasalltokens">
  #### `hasAnyTokens` and `hasAllTokens`
</div>

Les fonctions [hasAnyTokens](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAnyTokens) et [hasAllTokens](/docs/fr/reference/functions/regular-functions/string-search-functions#hasAllTokens) établissent une correspondance avec un ou l’ensemble des tokens fournis.

Ces deux fonctions acceptent les tokens de recherche soit sous forme de chaîne, qui sera découpée en tokens à l’aide du même tokenizer que celui utilisé pour la colonne d’index, soit sous forme de tableau de tokens déjà traités, auquel aucune tokenization ne sera appliquée avant la recherche.
Consultez la documentation de la fonction pour en savoir plus.

Exemple :

```sql theme={null}
-- Search tokens passed as string argument
SELECT count() FROM table WHERE hasAnyTokens(comment, 'clickhouse olap');
SELECT count() FROM table WHERE hasAllTokens(comment, 'clickhouse olap');

-- Search tokens passed as Array(String)
SELECT count() FROM table WHERE hasAnyTokens(comment, ['clickhouse', 'olap']);
SELECT count() FROM table WHERE hasAllTokens(comment, ['clickhouse', 'olap']);
```

<div id="functions-example-hasphrase">
  #### `hasPhrase`
</div>

La fonction [hasPhrase](/docs/fr/reference/functions/regular-functions/string-search-functions#hasPhrase) vérifie la présence d’une expression : tous les tokens doivent apparaître de façon consécutive et dans le même ordre que dans la chaîne de recherche.

Contrairement à `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 :

```sql theme={null}
-- Matches: 'clickhouse' and 'olap' must appear consecutively in that order
SELECT count() FROM table WHERE hasPhrase(comment, 'clickhouse olap');

-- Does NOT match a row containing 'olap clickhouse' (wrong order)
-- Does NOT match a row containing 'clickhouse fast olap' (non-consecutive)
```

<div id="functions-example-has">
  #### `has`
</div>

La fonction de tableau [has](/docs/fr/reference/functions/regular-functions/array-functions#has) permet de rechercher un seul token dans un tableau de chaînes.

Exemple :

```sql theme={null}
SELECT count() FROM table WHERE has(array, 'clickhouse');
```

<div id="functions-example-hasany-hasall">
  #### `hasAny` et `hasAll`
</div>

Les fonctions sur les tableaux [hasAny](/docs/fr/reference/functions/regular-functions/array-functions#hasAny) et [hasAll](/docs/fr/reference/functions/regular-functions/array-functions#hasAll) vérifient si la colonne de tableau indexée contient une partie ou la totalité d’un ensemble constant de chaînes recherchées.

Exemple :

```sql theme={null}
SELECT count() FROM table WHERE hasAny(tags, ['clickhouse', 'olap']);
SELECT count() FROM table WHERE hasAll(tags, ['clickhouse', 'olap']);
```

<div id="functions-example-mapcontains">
  #### `mapContains`
</div>

La fonction [mapContains](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsKey) (alias de `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 :

```sql theme={null}
SELECT count() FROM table WHERE mapContainsKey(map, 'clickhouse');
-- OR
SELECT count() FROM table WHERE mapContains(map, 'clickhouse');
```

<div id="functions-example-mapcontainsvalue">
  #### `mapContainsValue`
</div>

La fonction [mapContainsValue](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsValue) établit une correspondance entre les tokens extraits de la chaîne recherchée et les valeurs d'une map.
Le comportement est similaire à celui de la fonction `equals` sur une colonne `String`.
L’index de texte n'est utilisé que s'il a été créé sur une expression `mapValues(map)`.

Exemple :

```sql theme={null}
SELECT count() FROM table WHERE mapContainsValue(map, 'clickhouse');
```

<div id="functions-example-mapcontainslike">
  #### `mapContainsKeyLike` et `mapContainsValueLike`
</div>

Les fonctions [mapContainsKeyLike](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike) et [mapContainsValueLike](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) appliquent un motif à toutes les clés ou à toutes les valeurs (respectivement) d’une map.

Exemple :

```sql theme={null}
SELECT count() FROM table WHERE mapContainsKeyLike(map, '% clickhouse %');
SELECT count() FROM table WHERE mapContainsValueLike(map, '% clickhouse %');
```

<div id="functions-example-access-operator">
  #### `operator[]`
</div>

L’[operator\[\]](/docs/fr/reference/operators/index#access-operators) d’accès peut être utilisé avec l’index de texte pour filtrer les clés et les valeurs. L’index de texte n’est utilisé que s’il est créé sur les expressions `mapKeys(map)` ou `mapValues(map)`, ou sur les deux.

Exemple :

```sql theme={null}
SELECT count() FROM table WHERE map['engine'] = 'clickhouse';
```

Voir les exemples suivants pour savoir comment utiliser des colonnes de type `Array(T)` et `Map(K, V)` avec l’index de texte intégral.

<div id="text-index-example-array">
  ### Indexation des colonnes Array(String)
</div>

Imaginez une plateforme de blog où les auteurs classent leurs articles à l’aide de mots-clés.
Nous voulons que les utilisateurs puissent découvrir des contenus connexes en recherchant des thèmes ou en cliquant dessus.

Considérez cette définition de table :

```sql theme={null}
CREATE TABLE posts
(
    post_id UInt64,
    title String,
    content String,
    keywords Array(String)
)
ENGINE = MergeTree
ORDER BY (post_id);
```

Sans index de texte, trouver des posts contenant un mot-clé donné (par ex. `clickhouse`) nécessite de parcourir toutes les lignes :

```sql theme={null}
SELECT count() FROM posts WHERE has(keywords, 'clickhouse'); -- slow full-table scan - checks every keyword in every post
```

À mesure que la plateforme grandit, cela devient de plus en plus lent, car la requête doit examiner le tableau `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` :

```sql theme={null}
ALTER TABLE posts ADD INDEX keywords_idx(keywords) TYPE text(tokenizer = splitByNonAlpha);
ALTER TABLE posts MATERIALIZE INDEX keywords_idx; -- Don't forget to rebuild the index for existing data
```

<div id="text-index-example-map">
  ### Indexation des colonnes de type Map
</div>

Dans de nombreux cas d’usage en observabilité, les messages de log sont découpés en "composants" et stockés dans les types de données appropriés, par exemple une date-heure pour le timestamp, un enum pour le niveau de log, etc.
Les champs de métriques sont de préférence stockés sous forme de paires clé-valeur.
Les équipes d’exploitation doivent pouvoir rechercher efficacement dans les logs à des fins de débogage, d’investigation d’incidents de sécurité et de supervision.

Considérez cette table de logs :

```sql theme={null}
CREATE TABLE logs
(
    id UInt64,
    timestamp DateTime,
    message String,
    attributes Map(String, String)
)
ENGINE = MergeTree
ORDER BY (timestamp);
```

Sans index de texte, la recherche dans des données [Map](/docs/fr/reference/data-types/map) nécessite de parcourir l’intégralité de la table :

```sql theme={null}
-- Finds all logs with rate limiting data:
SELECT * FROM logs WHERE has(mapKeys(attributes), 'rate_limit'); -- slow full-table scan

-- Finds all logs from a specific IP:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- slow full-table scan
```

À mesure que le volume de logs augmente, ces requêtes ralentissent.

La solution consiste à créer un index de texte intégral sur les clés et les valeurs de [Map](/docs/fr/reference/data-types/map).
Utilisez [mapKeys](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapKeys) pour créer un index de texte intégral lorsque vous devez retrouver des logs à partir des noms de champ ou des types d’attribut :

```sql theme={null}
ALTER TABLE logs ADD INDEX attributes_keys_idx mapKeys(attributes) TYPE text(tokenizer = array);
ALTER TABLE posts MATERIALIZE INDEX attributes_keys_idx;
```

Utilisez [mapValues](/docs/fr/reference/functions/regular-functions/tuple-map-functions#mapValues) pour créer un index de texte intégral lorsque vous devez rechercher dans le contenu même des attributs :

```sql theme={null}
ALTER TABLE logs ADD INDEX attributes_vals_idx mapValues(attributes) TYPE text(tokenizer = array);
ALTER TABLE posts MATERIALIZE INDEX attributes_vals_idx;
```

Exemples de requêtes :

```sql theme={null}
-- Find all rate-limited requests:
SELECT * FROM logs WHERE mapContainsKey(attributes, 'rate_limit'); -- fast

-- Finds all logs from a specific IP:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- fast

-- Finds all logs where any attribute includes an error:
SELECT * FROM logs WHERE mapContainsValueLike(attributes, '% error %'); -- fast
```

<div id="text-index-example-json">
  ### Indexation des colonnes JSON
</div>

Les index de texte peuvent être utilisés avec les colonnes `JSON` de trois façons :

1. **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.
2. **Index basés sur les chemins avec [JSONAllPaths](/docs/fr/reference/functions/regular-functions/json-functions#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`.
3. **Index basés sur les valeurs avec [JSONAllValues](/docs/fr/reference/functions/regular-functions/json-functions#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.

<div id="json-indexes-on-subcolumns">
  #### Index sur des sous-colonnes spécifiques
</div>

Vous pouvez créer un skip index sur n’importe quelle sous-colonne JSON en utilisant la même syntaxe que pour les colonnes classiques.

Il existe deux façons de référencer une sous-colonne JSON dans une expression d’index :

* **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`.

Exemple de définition d’index :

```sql title="Query" theme={null}
CREATE TABLE sensor_data
(
    data JSON(sensor_id String),
    INDEX idx_sensor data.sensor_id TYPE text(tokenizer = splitByNonAlpha),
    INDEX idx_location data.location::String TYPE text(tokenizer = splitByNonAlpha)
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS index_granularity = 1;

INSERT INTO sensor_data SELECT toJSONString(map('sensor_id', 'id_' || number , 'location', 'room_' || toString(number))) FROM numbers(4);
INSERT INTO sensor_data SELECT toJSONString(map('sensor_id', 'id_' || number, 'location', 'room_' || toString(number))) FROM numbers(4, 4);
```

Exemple de requête :

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM sensor_data WHERE data.sensor_id = 'id_5';
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx_sensor
        Description: text
        Condition: (mode: All; tokens: ["5", "id"])
        Parts: 1/2
        Granules: 1/8
```

Exemple de requête :

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM sensor_data WHERE data.location::String = 'room_5';
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx_location
        Description: text
        Condition: (mode: All; tokens: ["5", "room"])
        Parts: 1/2
        Granules: 1/8
```

<div id="json-indexes-jsonallpaths">
  #### Index basés sur les chemins avec JSONAllPaths
</div>

Comme pour les colonnes `Map`, des index de texte peuvent être créés sur des colonnes [JSON](/docs/fr/reference/data-types/newjson) à l’aide de [`JSONAllPaths`](/docs/fr/reference/functions/regular-functions/json-functions#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 :

```sql title="Query" theme={null}
CREATE TABLE events
(
    data JSON,
    INDEX idx JSONAllPaths(data) TYPE text(tokenizer = array)
)
ENGINE = MergeTree
ORDER BY tuple();

INSERT INTO events VALUES ('{"user": {"name": "Alice"}, "action": "login"}');
INSERT INTO events VALUES ('{"metric": {"cpu": 0.95}, "host": "srv1"}');
```

Vous pouvez utiliser `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 :

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM events WHERE data.user.name = 'Alice';
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx
        Description: text
        Condition: (mode: All; tokens: ["user.name"])
        Parts: 1/2
        Granules: 1/2
```

Lorsqu’un chemin n’existe dans aucune part, toutes les parts et toutes les granules sont ignorées.

Exemple :

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM events WHERE data.nonexistent = 1;
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx
        Description: text
        Condition: (mode: All; tokens: ["nonexistent"])
        Parts: 0/2
        Granules: 0/2
```

`IS NOT NULL` utilise également l’index — il ignore les granules où le chemin est absent (puisque la valeur serait `NULL`) :

Exemple :

```sql title="Query" theme={null}
EXPLAIN indexes = 1 SELECT * FROM events WHERE data.user.name IS NOT NULL;
```

```text title="Response" theme={null}
...
    Indexes:
      Skip
        Name: idx
        Description: text
        Condition: (mode: All; tokens: ["user.name"])
        Parts: 1/2
        Granules: 1/2
```

<div id="json-indexes-jsonallvalues">
  #### Index basés sur les valeurs avec JSONAllValues
</div>

Les index de texte peuvent être utilisés pour accélérer les recherches dans les colonnes [JSON](/docs/fr/reference/data-types/newjson) via la fonction [`JSONAllValues`](/docs/fr/reference/functions/regular-functions/json-functions#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.

<Note>
  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.
</Note>

<div id="json-all-values-creating-the-index">
  ##### Création de l’index
</div>

Exemple de définition d’un index :

```sql theme={null}
CREATE TABLE events
(
    id UInt64,
    data JSON,
    INDEX json_idx JSONAllValues(data) TYPE text(tokenizer = splitByNonAlpha)
)
ENGINE = MergeTree
ORDER BY id;
```

<div id="json-all-values-supported-query-patterns">
  ##### Types de requêtes pris en charge
</div>

Une fois l’index créé, il peut accélérer les requêtes sur les sous-colonnes JSON en utilisant les mêmes fonctions que pour les colonnes `String`, ainsi que la fonction `equals` pour toutes les colonnes.

Accès aux sous-colonnes :

```sql theme={null}
SELECT * FROM events WHERE data.user_name = 'alice';
SELECT * FROM events WHERE data.message LIKE '% error %';
SELECT * FROM events WHERE startsWith(data.status, 'fail');
SELECT * FROM events WHERE hasToken(data.title, 'clickhouse');
```

Accès à la sous-colonne via un `CAST` explicite :

```sql theme={null}
SELECT * FROM events WHERE hasAllTokens(data.message::String, 'connection timeout');
SELECT * FROM events WHERE data.status_code::UInt64 = 404;
SELECT * FROM events WHERE has(data.tags::Array(String), 'bug')
```

opérateur `IN` :

```sql theme={null}
SELECT * FROM events WHERE data.level IN ('error', 'critical');
```

<div id="text-index-phrase-search">
  ### Recherche d'expressions
</div>

Une recherche classique dans un index de texte, par exemple

```sql theme={null}
SELECT *
FROM tab
WHERE hasAllTokens(col, 'weather in Tokyo')
```

correspond à toutes les lignes qui contiennent les tokens donnés, dans n’importe quel ordre.
Dans l’exemple, la ligne `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,

```sql theme={null}
SELECT *
FROM tab
WHERE hasPhrase(col, 'weather in Tokyo')
```

correspond à toute ligne contenant la séquence de tokens `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.

<div id="text-index-phrase-search-example">
  #### Exemple
</div>

```sql theme={null}
CREATE TABLE tab (
    id UInt32,
    text String,
    INDEX idx text TYPE text(tokenizer = splitByNonAlpha)
)
ENGINE = MergeTree
ORDER BY id;

INSERT INTO tab VALUES
    (1, 'weather in New York'),
    (2, 'New weather in York'),
    (3, 'weather in New Orleans');
```

```sql title="Query" theme={null}
SELECT id, text FROM tab WHERE hasPhrase(text, 'weather in New York');
```

```result title="Response" theme={null}
   ┌─id─┬─text────────────────┐
1. │  1 │ weather in New York │
   └────┴─────────────────────┘
```

La ligne 2 (`'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'`.

<div id="performance-tuning">
  ## Optimisation des performances
</div>

<div id="direct-read">
  ### lecture directe
</div>

Certaines requêtes textuelles peuvent être considérablement accélérées grâce à une optimisation appelée "lecture directe".

Exemple :

```sql theme={null}
SELECT column_a, column_b, ...
FROM [...]
WHERE string_search_function(column_with_text_index)
```

L’optimisation de lecture directe répond à la requête en s’appuyant exclusivement sur l’index de texte (c’est-à-dire sur des consultations de l’index de texte), sans accéder à la colonne de texte sous-jacente.
Les consultations de l’index de texte lisent relativement peu de données et sont donc bien plus rapides que les skip indexes habituels dans ClickHouse (qui effectuent une consultation du skip index, puis chargent et filtrent les granules restantes).

La lecture directe est contrôlée par deux paramètres :

* Le paramètre [query\_plan\_direct\_read\_from\_text\_index](/docs/fr/reference/settings/session-settings#query_plan_direct_read_from_text_index) (`true` par 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](/docs/fr/reference/settings/session-settings#use_skip_indexes_on_data_read) était un prérequis pour la lecture directe dans les versions de ClickHouse \< 26.4.

**Fonctions prises en charge**

L’optimisation de lecture directe prend en charge les fonctions `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

```sql theme={null}
EXPLAIN PLAN actions = 1
SELECT count()
FROM table
WHERE hasToken(col, 'some_token')
SETTINGS query_plan_direct_read_from_text_index = 0, -- disable direct read
```

renvoie

```text theme={null}
[...]
Filter ((WHERE + Change column names to column identifiers))
Filter column: hasToken(__table1.col, 'some_token'_String) (removed)
Actions: INPUT : 0 -> col String : 0
         COLUMN Const(String) -> 'some_token'_String String : 1
         FUNCTION hasToken(col :: 0, 'some_token'_String :: 1) -> hasToken(__table1.col, 'some_token'_String) UInt8 : 2
[...]
```

alors que la même requête, exécutée avec `query_plan_direct_read_from_text_index = 1`

```sql theme={null}
EXPLAIN PLAN actions = 1
SELECT count()
FROM table
WHERE hasToken(col, 'some_token')
SETTINGS query_plan_direct_read_from_text_index = 1, -- enable direct read
```

renvoie

```text theme={null}
[...]
Expression (Before GROUP BY)
Positions:
  Filter
  Filter column: __text_index_idx_hasToken_94cc2a813036b453d84b6fb344a63ad3 (removed)
  Actions: INPUT :: 0 -> __text_index_idx_hasToken_94cc2a813036b453d84b6fb344a63ad3 UInt8 : 0
[...]
```

La seconde sortie d’EXPLAIN PLAN contient une colonne virtuelle `__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](/docs/fr/reference/settings/session-settings#query_plan_text_index_add_hint) (activé par défaut).

Exemple de requête sans indice :

```sql theme={null}
EXPLAIN actions = 1
SELECT count()
FROM table
WHERE (col LIKE '%some-token%') AND (d >= today())
SETTINGS query_plan_text_index_add_hint = 0
FORMAT TSV
```

renvoie

```text theme={null}
[...]
Prewhere filter column: and(like(__table1.col, \'%some-token%\'_String), greaterOrEquals(__table1.d, _CAST(20440_Date, \'Date\'_String))) (removed)
[...]
```

alors que la même requête est exécutée avec `query_plan_text_index_add_hint = 1`

```sql theme={null}
EXPLAIN actions = 1
SELECT count()
FROM table
WHERE col LIKE '%some-token%'
SETTINGS query_plan_text_index_add_hint = 1
```

renvoie

```text theme={null}
[...]
Prewhere filter column: and(__text_index_idx_col_like_d306f7c9c95238594618ac23eb7a3f74, like(__table1.col, \'%some-token%\'_String), greaterOrEquals(__table1.d, _CAST(20440_Date, \'Date\'_String))) (removed)
[...]
```

Dans la sortie du second EXPLAIN PLAN, vous pouvez voir qu’une conjonction supplémentaire (`__text_index_...`) a été ajoutée à la condition de filtrage.
Grâce à l’optimisation [PREWHERE](/docs/fr/reference/statements/select/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.

<div id="like-ilike-queries-perf">
  ### Requêtes LIKE/ILIKE
</div>

Lorsque le motif d’une requête LIKE/ILIKE est `%<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 :

* [use\_text\_index\_like\_evaluation\_by\_dictionary\_scan](/docs/fr/reference/settings/session-settings#use_text_index_like_evaluation_by_dictionary_scan)

Le mécanisme de repli est contrôlé par deux paramètres :

* [text\_index\_like\_min\_pattern\_length](/docs/fr/reference/settings/session-settings#text_index_like_min_pattern_length)
* [text\_index\_like\_max\_postings\_to\_read](/docs/fr/reference/settings/session-settings#text_index_like_max_postings_to_read)

Cette optimisation ne prend en charge que les fonctions `like` et `ilike`.

<div id="caching">
  ### Mise en cache
</div>

Il existe différents caches à l’échelle du serveur pour conserver en mémoire certaines parties de l’index de texte (voir la section [Détails d’implémentation](#implementation)) :
À l’heure actuelle, il existe des caches pour les en-têtes désérialisés, les tokens et les listes de postings de l’index de texte afin de réduire les opérations d’E/S.
Utilisez les paramètres [use\_text\_index\_header\_cache](/docs/fr/reference/settings/session-settings#use_text_index_header_cache), [use\_text\_index\_tokens\_cache](/docs/fr/reference/settings/session-settings#use_text_index_tokens_cache) et [use\_text\_index\_postings\_cache](/docs/fr/reference/settings/session-settings#use_text_index_postings_cache) pour désactiver, pour les requêtes, la lecture et l’écriture dans les caches individuels.

Pour vider les caches, utilisez l’instruction [SYSTEM CLEAR TEXT INDEX CACHES](/docs/fr/reference/statements/system#drop-text-index-caches)

Reportez-vous aux paramètres du serveur ci-dessous pour configurer les caches.

<div id="caching-tokens">
  #### Paramètres du cache des jetons
</div>

| Paramètre                                                                                                                       | Description                                                                                                       |
| ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| [text\_index\_tokens\_cache\_policy](/docs/fr/reference/settings/server-settings/settings#text_index_tokens_cache_policy)            | Nom de la politique du cache des jetons de l’index de texte.                                                      |
| [text\_index\_tokens\_cache\_size](/docs/fr/reference/settings/server-settings/settings#text_index_tokens_cache_size)                | Taille maximale du cache en octets.                                                                               |
| [text\_index\_tokens\_cache\_max\_entries](/docs/fr/reference/settings/server-settings/settings#text_index_tokens_cache_max_entries) | Nombre maximal de jetons désérialisés dans le cache.                                                              |
| [text\_index\_tokens\_cache\_size\_ratio](/docs/fr/reference/settings/server-settings/settings#text_index_tokens_cache_size_ratio)   | Taille de la file protégée dans le cache des jetons de l’index de texte, par rapport à la taille totale du cache. |

<div id="caching-header">
  #### Paramètres du cache des en-têtes
</div>

| Paramètre                                                                                                                       | Description                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [text\_index\_header\_cache\_policy](/docs/fr/reference/settings/server-settings/settings#text_index_header_cache_policy)            | Nom de la politique du cache des en-têtes de l'index de texte.                                                                |
| [text\_index\_header\_cache\_size](/docs/fr/reference/settings/server-settings/settings#text_index_header_cache_size)                | Taille maximale du cache en octets.                                                                                           |
| [text\_index\_header\_cache\_max\_entries](/docs/fr/reference/settings/server-settings/settings#text_index_header_cache_max_entries) | Nombre maximal d'en-têtes désérialisés dans le cache.                                                                         |
| [text\_index\_header\_cache\_size\_ratio](/docs/fr/reference/settings/server-settings/settings#text_index_header_cache_size_ratio)   | Taille de la file d'attente protégée dans le cache des en-têtes de l'index de texte, par rapport à la taille totale du cache. |

<div id="caching-posting-lists">
  #### Paramètres du cache des listes de postings
</div>

| Setting                                                                                                                             | Description                                                                                                                   |
| ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [text\_index\_postings\_cache\_policy](/docs/fr/reference/settings/server-settings/settings#text_index_postings_cache_policy)            | Nom de la politique de cache des listes de postings de l'index de texte.                                                      |
| [text\_index\_postings\_cache\_size](/docs/fr/reference/settings/server-settings/settings#text_index_postings_cache_size)                | Taille maximale du cache en octets.                                                                                           |
| [text\_index\_postings\_cache\_max\_entries](/docs/fr/reference/settings/server-settings/settings#text_index_postings_cache_max_entries) | Nombre maximal de listes de postings désérialisées dans le cache.                                                             |
| [text\_index\_postings\_cache\_size\_ratio](/docs/fr/reference/settings/server-settings/settings#text_index_postings_cache_size_ratio)   | Taille de la file protégée dans le cache des listes de postings de l'index de texte, par rapport à la taille totale du cache. |

<div id="limitations">
  ## Limites
</div>

L’index de texte présente actuellement les limitations suivantes :

* 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).

<div id="text-index-vs-bloom-filter-indexes">
  ## Index de texte vs index basés sur des filtres de Bloom
</div>

Les prédicats String peuvent être accélérés à l’aide d’index de texte et d’index basés sur des filtres de Bloom (types d’index `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](/docs/fr/reference/engines/table-engines/mergetree-family/mergetree#n-gram-bloom-filter) pour un exemple).
* Sont relativement compacts (quelques kilo-octets ou mégaoctets par part).

**Index de texte**

* 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).

Les index basés sur des filtres de Bloom ne prennent en charge la recherche en texte intégral qu’en tant qu’« effet secondaire » :

* 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é.

Les index de texte, en revanche, sont conçus spécifiquement pour la recherche en texte intégral :

* Ils fournissent la tokenisation et le prétraitement
* Ils prennent efficacement en charge `hasAllTokens`, `LIKE`, `match` et des fonctions de recherche textuelle similaires.
* Ils offrent une bien meilleure capacité de passage à l’échelle pour les grands corpus textuels.

<div id="implementation">
  ## Détails d’implémentation
</div>

Chaque index de texte se compose de deux structures de données (abstraites) :

* 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.

L’index de texte est construit pour l’ensemble de la part.
Contrairement aux autres skip indexes, l’index de texte peut être fusionné au lieu d’être reconstruit lors de la fusion des data parts (voir ci-dessous).

Lors de la création de l’index, trois fichiers sont créés (par part) :

**Fichier des blocs de dictionnaire (.dct)**

Les tokens de l’index de texte sont triés et stockés dans des blocs de dictionnaire de 512 tokens chacun (la taille du bloc est configurable via le paramètre `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](/docs/fr/guides/clickhouse/data-modelling/sparse-primary-indexes)) 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](https://roaringbitmap.org/).
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](/docs/fr/reference/statements/alter/projection#projection-indexes) 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](/docs/fr/reference/functions/table-functions/mergeTreeTextIndex) peut être utilisée pour inspecter les index de texte.

<div id="hacker-news-dataset">
  ## Exemple : jeu de données Hacker News
</div>

Examinons les gains de performances des index de texte sur un vaste jeu de données contenant beaucoup de texte.
Nous utiliserons 28,7 M de lignes de commentaires du site populaire Hacker News.
Voici la table sans index de texte :

```sql theme={null}
CREATE TABLE hackernews (
    id UInt64,
    deleted UInt8,
    type String,
    author String,
    timestamp DateTime,
    comment String,
    dead UInt8,
    parent UInt64,
    poll UInt64,
    children Array(UInt32),
    url String,
    score UInt32,
    title String,
    parts Array(UInt32),
    descendants UInt32
)
ENGINE = MergeTree
ORDER BY (type, author);
```

Les 28,7 M de lignes se trouvent dans un fichier Parquet sur S3 - insérons-les dans la table `hackernews` :

```sql theme={null}
INSERT INTO hackernews
    SELECT * FROM s3Cluster(
        'default',
        'https://datasets-documentation.s3.eu-west-3.amazonaws.com/hackernews/hacknernews.parquet',
        'Parquet',
        '
    id UInt64,
    deleted UInt8,
    type String,
    by String,
    time DateTime,
    text String,
    dead UInt8,
    parent UInt64,
    poll UInt64,
    kids Array(UInt32),
    url String,
    score UInt32,
    title String,
    parts Array(UInt32),
    descendants UInt32');
```

Nous utiliserons `ALTER TABLE` pour ajouter un index de texte sur la colonne comment, puis le matérialiser :

```sql theme={null}
-- Add the index
ALTER TABLE hackernews ADD INDEX comment_idx comment TYPE text(tokenizer = splitByNonAlpha);

-- Materialize the index for existing data
ALTER TABLE hackernews MATERIALIZE INDEX comment_idx SETTINGS mutations_sync = 2;
```

Maintenant, exécutons des requêtes à l’aide des fonctions `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.

<div id="using-hasToken">
  ### 1. Utilisation de `hasToken`
</div>

`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.

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│     516 │
└─────────┘

1 row in set. Elapsed: 0.362 sec. Processed 24.90 million rows, 9.51 GB
```

**Lecture directe activée (lecture rapide de l’index)**
Nous exécutons maintenant la même requête avec la lecture directe activée (par défaut).

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│     516 │
└─────────┘

1 row in set. Elapsed: 0.008 sec. Processed 3.15 million rows, 3.15 MB
```

La requête `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.

<div id="using-hasAnyTokens">
  ### 2. Utilisation de `hasAnyTokens`
</div>

`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)**

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAnyTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│  408426 │
└─────────┘

1 row in set. Elapsed: 1.329 sec. Processed 28.74 million rows, 9.72 GB
```

**Lecture directe activée (lecture rapide depuis l’index)**

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAnyTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│  408426 │
└─────────┘

1 row in set. Elapsed: 0.015 sec. Processed 27.99 million rows, 27.99 MB
```

L’accélération est encore plus spectaculaire pour cette recherche courante avec l’opérateur "OR".
La requête est près de 89 fois plus rapide (1.329s vs 0.015s) en évitant le parcours complet de la colonne.

<div id="using-hasAllTokens">
  ### 3. Utilisation de `hasAllTokens`
</div>

`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.

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAllTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│      11 │
└─────────┘

1 row in set. Elapsed: 0.184 sec. Processed 147.46 thousand rows, 57.03 MB
```

**Lecture directe activée (lecture rapide de l’index)**
La lecture directe répond à la requête en s’appuyant sur les données de l’index et ne lit que 147.46 KB.

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasAllTokens(comment, 'love ClickHouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│      11 │
└─────────┘

1 row in set. Elapsed: 0.007 sec. Processed 147.46 thousand rows, 147.46 KB
```

Pour cette recherche "AND", l’optimisation de lecture directe est plus de 26 fois plus rapide (0.184s contre 0.007s) que le scan standard du skip index.

<div id="compound-search">
  ### 4. Recherche composée : OR, AND, NOT, ...
</div>

L’optimisation de lecture directe s’applique également aux expressions booléennes composées.
Ici, nous allons effectuer une recherche insensible à la casse de 'ClickHouse' OR 'clickhouse'.

**Lecture directe désactivée (Standard scan)**

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse') OR hasToken(comment, 'clickhouse')
SETTINGS query_plan_direct_read_from_text_index = 0;

┌─count()─┐
│     769 │
└─────────┘

1 row in set. Elapsed: 0.450 sec. Processed 25.87 million rows, 9.58 GB
```

**Lecture directe activée (lecture rapide de l’index)**

```sql theme={null}
SELECT count()
FROM hackernews
WHERE hasToken(comment, 'ClickHouse') OR hasToken(comment, 'clickhouse')
SETTINGS query_plan_direct_read_from_text_index = 1;

┌─count()─┐
│     769 │
└─────────┘

1 row in set. Elapsed: 0.013 sec. Processed 25.87 million rows, 51.73 MB
```

En combinant les résultats de l’index, la requête en lecture directe est 34 fois plus rapide (0,450 s contre 0,013 s) et évite de lire 9,58 Go de données de colonne.
Pour ce cas précis, `hasAnyTokens(comment, ['ClickHouse', 'clickhouse'])` serait la syntaxe à privilégier, car plus efficace.

<div id="related-content">
  ## Contenu connexe
</div>

* Blog : [Annonce de la disponibilité générale de la recherche en texte intégral dans ClickHouse](https://clickhouse.com/blog/full-text-search-ga-release)
* Blog : [Concevoir une recherche en texte intégral haute performance pour le stockage objet](https://clickhouse.com/blog/clickhouse-full-text-search-object-storage)
* Vidéo : [Introduction à la recherche en texte intégral dans ClickHouse](https://www.youtube.com/watch?v=9zPmf1a_heU)
* Vidéo : [Sous le capot : la recherche en texte intégral à l’échelle et à la vitesse de ClickHouse](https://www.youtube.com/watch?v=8JbqE_ubfkU)
* Présentation : [Dans les coulisses de la recherche en texte intégral de ClickHouse : rapide, native et columnaire](https://github.com/ClickHouse/clickhouse-presentations/blob/master/2025-tumuchdata-munich/ClickHouse_%20full-text%20search%20-%2011.11.2025%20Munich%20Database%20Meetup.pdf)
* Présentation : [Index inversés de base de données : pourquoi, quoi et comment, FOSDEM 2026](https://presentations.clickhouse.com/2026-fosdem-inverted-index/Inverted_indexes_the_what_the_why_the_how.pdf)

**Contenu obsolète**

* Blog : [Présentation des index inversés dans ClickHouse](https://clickhouse.com/blog/clickhouse-search-with-inverted-indices)
* Blog : [Dans les coulisses de la recherche en texte intégral de ClickHouse : rapide, native et columnaire](https://clickhouse.com/blog/clickhouse-full-text-search)
* Vidéo : [Index en texte intégral : conception et expérimentations](https://www.youtube.com/watch?v=O_MnyUkrIq8)
