> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-mintlify-fbfa8bee.mintlify.site/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é](/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)]
                                -- 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](/fr/reference/data-types/string) et [FixedString](/fr/reference/data-types/fixedstring),
* [Array(String)](/fr/reference/data-types/array) et [Array(FixedString)](/fr/reference/data-types/array),
* [Map](/fr/reference/data-types/map) (à l’aide des fonctions [mapKeys](/fr/reference/functions/regular-functions/tuple-map-functions#mapKeys) et [mapValues](/fr/reference/functions/regular-functions/tuple-map-functions#mapValues)), et
* [JSON](/fr/reference/data-types/newjson) (à l’aide des fonctions [JSONAllPaths](/fr/reference/functions/regular-functions/json-functions#JSONAllPaths) et [`JSONAllValues`](/fr/reference/functions/regular-functions/json-functions#JSONAllValues)).

Les colonnes de type [Nullable(T)](/fr/reference/data-types/nullable) et [LowCardinality()](/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)]
                                -- 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 table existantes (sinon, la recherche sur les parts sans index reviendra à des scans 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](/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](/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](/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](/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](/fr/reference/functions/regular-functions/array-functions#array)).

Tous les tokenizers disponibles sont répertoriés dans [system.tokenizers](/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](/fr/reference/functions/regular-functions/splitting-merging-functions#tokens) et [tokensForLikePattern](/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.
:::

**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](/fr/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/fr/reference/functions/regular-functions/string-functions#lowerUTF8), [caseFoldUTF8](/fr/reference/functions/regular-functions/string-functions#caseFoldUTF8).
2. Normalisation UTF-8, par ex. [normalizeUTF8NFC](/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFC), [normalizeUTF8NFD](/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFD), [normalizeUTF8NFKC](/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFKC), [normalizeUTF8NFKD](/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFKD), [normalizeUTF8NFKCCasefold](/fr/reference/functions/regular-functions/string-functions#normalizeUTF8NFKCCasefold), [toValidUTF8](/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](/fr/reference/functions/regular-functions/string-functions#extractTextFromHTML), [substring](/fr/reference/functions/regular-functions/string-functions#substring), [idnaEncode](/fr/reference/functions/regular-functions/string-functions#idnaEncode), [translate](/fr/reference/functions/regular-functions/string-replace-functions#translate), [removeDiacriticsUTF8](/fr/reference/functions/regular-functions/string-functions#removeDiacriticsUTF8).

L'expression de préprocesseur doit transformer une valeur d'entrée de type [String](/fr/reference/data-types/string) ou [FixedString](/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'utilisation de fonctions non déterministes est interdite.

<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](/fr/reference/functions/regular-functions/string-search-functions#hasToken), [hasAllTokens](/fr/reference/functions/regular-functions/string-search-functions#hasAllTokens), [hasAnyTokens](/fr/reference/functions/regular-functions/string-search-functions#hasAnyTokens) et [hasPhrase](/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](/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');
```

**Autres arguments (facultatifs)**.

<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`](/fr/reference/settings/merge-tree-settings#text_index_dictionary_block_size), [`text_index_dictionary_block_frontcoding_compression`](/fr/reference/settings/merge-tree-settings#text_index_dictionary_block_frontcoding_compression), [`text_index_posting_list_block_size`](/fr/reference/settings/merge-tree-settings#text_index_posting_list_block_size) et [`text_index_posting_list_codec`](/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 table parts.
  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](/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 partie de 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 de préprocesseur possibles.
  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).
</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](/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](/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](/fr/reference/functions/regular-functions/string-search-functions#like)) et la fonction [match](/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](/fr/reference/functions/regular-functions/string-search-functions#multiSearchAny) et sa variante UTF-8 [multiSearchAnyUTF8](/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](/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](/fr/reference/functions/regular-functions/string-functions#startsWith) et [endsWith](/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.

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-hastokenornull">
  #### `hasToken` et `hasTokenOrNull`
</div>

<Note>
  La fonction `hasToken` semble simple à utiliser, mais elle comporte certains pièges avec les tokenizers non par défaut et les expressions de prétraitement.
  Nous recommandons plutôt d'utiliser les fonctions `hasAnyTokens` et `hasAllTokens`.
</Note>

Les fonctions [hasToken](/fr/reference/functions/regular-functions/string-search-functions#hasToken) et [hasTokenOrNull](/fr/reference/functions/regular-functions/string-search-functions#hasTokenOrNull) effectuent une correspondance avec un seul token donné.

Contrairement aux fonctions mentionnées précédemment, elles ne tokenisent pas le terme recherché (elles supposent 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](/fr/reference/functions/regular-functions/string-search-functions#hasAnyTokens) et [hasAllTokens](/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](/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.
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](/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](/fr/reference/functions/regular-functions/array-functions#hasAny) et [hasAll](/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](/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](/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](/fr/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike) et [mapContainsValueLike](/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\[\]](/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](/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](/fr/reference/data-types/map).
Utilisez [mapKeys](/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](/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](/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](/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](/fr/reference/data-types/newjson) à l’aide de [`JSONAllPaths`](/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](/fr/reference/data-types/newjson) via la fonction [`JSONAllValues`](/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’expression
</div>

L’index de texte prend en charge la recherche d’expression via la fonction `hasPhrase`.
Tous les tokens de l’expression doivent apparaître de façon consécutive et dans le même ordre dans le document.

L’index de texte accélère la recherche d’expression en croisant les posting lists de tous les tokens de l’expression afin d’identifier les granules candidates.
Au sein de ces granules, ClickHouse vérifie ensuite l’adjacence exacte des tokens.

`hasPhrase` est pris en charge avec les tokenizers `splitByNonAlpha`, `splitByString`, `ngrams` et `asciiCJK`.

La chaîne de l’expression est tokenisée à l’aide du tokenizer configuré pour l’index.
Les caractères séparateurs du tokenizer dans l’expression sont ignorés : `hasPhrase(text, 'quick+brown')` est équivalent à `hasPhrase(text, 'quick brown')` pour le tokenizer `splitByNonAlpha`.

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

```sql title="Query" 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](/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](/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](/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](/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](/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](/fr/reference/settings/session-settings#text_index_like_min_pattern_length)
* [text\_index\_like\_max\_postings\_to\_read](/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>

Différents caches sont disponibles 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.
Ils peuvent être activés via les paramètres [use\_text\_index\_header\_cache](/fr/reference/settings/session-settings#use_text_index_header_cache), [use\_text\_index\_tokens\_cache](/fr/reference/settings/session-settings#use_text_index_tokens_cache) et [use\_text\_index\_postings\_cache](/fr/reference/settings/session-settings#use_text_index_postings_cache).

Par défaut, tous les caches sont désactivés.
Pour vider les caches, utilisez l’instruction [SYSTEM CLEAR TEXT INDEX CACHES](/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](/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](/fr/reference/settings/server-settings/settings#text_index_tokens_cache_size)                | Taille maximale du cache en octets.                                                                               |
| [text\_index\_tokens\_cache\_max\_entries](/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](/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](/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](/fr/reference/settings/server-settings/settings#text_index_header_cache_size)                | Taille maximale du cache en octets.                                                                                           |
| [text\_index\_header\_cache\_max\_entries](/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](/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](/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](/fr/reference/settings/server-settings/settings#text_index_postings_cache_size)                | Taille maximale du cache en octets.                                                                                           |
| [text\_index\_postings\_cache\_max\_entries](/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](/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](/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](/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.

**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](/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](/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)
