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

> Encuentre rápidamente términos de búsqueda en texto.

# Búsqueda de texto completo con índices de texto

Los índices de texto (también conocidos como [índices invertidos](https://en.wikipedia.org/wiki/Inverted_index)) permiten realizar búsquedas de texto completo rápidas en datos de texto.
Un índice de texto almacena una correspondencia entre tokens y los números de fila que contienen cada token.
Los tokens se generan mediante un proceso llamado tokenización.
Por ejemplo, el tokenizador predeterminado de ClickHouse convierte la frase en inglés "The cat likes mice." en los tokens \["The", "cat", "likes", "mice"].

Como ejemplo, suponga una tabla con una sola columna y tres filas

```result theme={null}
1: Al gato le gustan los ratones.
2: Los ratones le tienen miedo a los perros.
3: Tengo dos perros y un gato.
```

Los tokens correspondientes son:

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

Normalmente preferimos buscar sin distinguir entre mayúsculas y minúsculas, por lo que pasamos los tokens a minúsculas:

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

También eliminaremos palabras vacías como "I", "the" y "and", ya que aparecen en casi todas las filas:

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

Un índice de texto contiene, por tanto, la siguiente información:

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

Dado un token de búsqueda, esta estructura de índice permite encontrar rápidamente todas las filas que coinciden.

<div id="creating-a-text-index">
  ## Crear un índice de texto
</div>

Los índices de texto están disponibles de forma general (GA) en ClickHouse 26.2 y versiones posteriores.
En estas versiones, no es necesario configurar ninguna opción especial para usar el índice de texto.
Recomendamos encarecidamente usar ClickHouse >= 26.2 para casos de uso en producción.

<Note>
  Los índices de texto pueden usarse con cualquier versión de ClickHouse >= 26.2, independientemente de la configuración [compatibility](/docs/es/reference/settings/session-settings#compatibility).
</Note>

Para crear un índice de texto, use la siguiente sintaxis:

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

Los índices de texto pueden definirse en columnas de estos tipos:

* [String](/docs/es/reference/data-types/string) y [FixedString](/docs/es/reference/data-types/fixedstring),
* [Array(String)](/docs/es/reference/data-types/array) y [Array(FixedString)](/docs/es/reference/data-types/array),
* [Map](/docs/es/reference/data-types/map) (mediante las funciones [mapKeys](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapKeys) y [mapValues](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapValues)), y
* [JSON](/docs/es/reference/data-types/newjson) (mediante las funciones [JSONAllPaths](/docs/es/reference/functions/regular-functions/json-functions#JSONAllPaths) y [`JSONAllValues`](/docs/es/reference/functions/regular-functions/json-functions#JSONAllValues)).

También se admiten columnas de tipo [Nullable(T)](/docs/es/reference/data-types/nullable) y [LowCardinality()](/docs/es/reference/data-types/lowcardinality), incluido `Array(Nullable(String or FixedString))`.

Como alternativa, para añadir un índice de texto a una tabla existente:

```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 agrega un índice a una tabla existente, le recomendamos materializar el índice en las partes existentes de la tabla (de lo contrario, las búsquedas en las partes sin índice recurrirán a escaneos exhaustivos lentos).

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

Para eliminar un índice de texto, ejecute

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

**Argumento tokenizador (obligatorio)**. El argumento `tokenizer` especifica el tokenizador:

* `splitByNonAlpha` divide cadenas por caracteres ASCII no alfanuméricos (consulte la función [splitByNonAlpha](/docs/es/reference/functions/regular-functions/splitting-merging-functions#splitByNonAlpha)).
* `splitByString(S)` divide cadenas usando determinadas cadenas separadoras `S` definidas por el usuario (consulte la función [splitByString](/docs/es/reference/functions/regular-functions/splitting-merging-functions#splitByString)).
  Los separadores pueden especificarse mediante un parámetro opcional; por ejemplo, `tokenizer = splitByString([', ', '; ', '\n', '\\'])`.
  Tenga en cuenta que cada cadena puede constar de varios caracteres (`', '` en el ejemplo).
  La lista de separadores predeterminada, si no se especifica explícitamente (por ejemplo, `tokenizer = splitByString`), es un único espacio en blanco `[' ']`.
* `asciiCJK` divide cadenas en tokens usando reglas de límites de palabra de Unicode (similares a [Unicode Text Segmentation (UAX #29)](https://unicode.org/reports/tr29/)). Los caracteres ASCII alfanuméricos y los guiones bajos forman tokens con conectores (ASCII `:` para letras, `.` y `'` para caracteres del mismo tipo). Los caracteres Unicode no ASCII, incluidos los caracteres [CJK](https://en.wikipedia.org/wiki/CJK_characters), se convierten en tokens de un solo carácter.
* `ngrams(N)` divide cadenas en `N`-grams del mismo tamaño (consulte la función [ngrams](/docs/es/reference/functions/regular-functions/splitting-merging-functions#ngrams)).
  La longitud del ngram puede especificarse mediante un parámetro entero opcional entre 1 y 8; por ejemplo, `tokenizer = ngrams(3)`.
  El tamaño predeterminado del ngram, si no se especifica explícitamente (por ejemplo, `tokenizer = ngrams`), es 3.
* `sparseGrams(min_length, max_length, min_cutoff_length)` divide cadenas en n-grams de longitud variable de al menos `min_length` y como máximo `max_length` caracteres (inclusive) (consulte la función [sparseGrams](/docs/es/reference/functions/regular-functions/string-functions#sparseGrams)).
  A menos que se especifique explícitamente, `min_length` y `max_length` toman los valores predeterminados 3 y 100.
  Si se proporciona el parámetro `min_cutoff_length`, solo se devuelven n-grams con una longitud mayor o igual que `min_cutoff_length`.
  En comparación con `ngrams(N)`, el tokenizador `sparseGrams` produce N-grams de longitud variable, lo que permite una representación más flexible del texto original.
  Por ejemplo, `tokenizer = sparseGrams(3, 5, 4)` genera internamente 3-, 4- y 5-grams a partir de la cadena de entrada, pero solo se devuelven los 4- y 5-grams.
* `array` no realiza tokenización; es decir, cada valor de fila es un token (consulte la función [array](/docs/es/reference/functions/regular-functions/array-functions#array)).

Todos los tokenizadores disponibles se enumeran en [system.tokenizers](/docs/es/reference/system-tables/tokenizers).

<Note>
  El tokenizador `splitByString` aplica los separadores de división de izquierda a derecha.
  Esto puede crear ambigüedades.
  Por ejemplo, las cadenas separadoras `['%21', '%']` harán que `%21abc` se tokenice como `['abc']`, mientras que, si se intercambia el orden de ambas cadenas separadoras por `['%', '%21']`, la salida será `['21abc']`.
  En la mayoría de los casos, conviene que la coincidencia dé prioridad a los separadores más largos.
  Por lo general, esto puede lograrse pasando las cadenas separadoras en orden descendente de longitud.
  Si las cadenas separadoras forman un [código prefijo](https://en.wikipedia.org/wiki/Prefix_code), pueden pasarse en cualquier orden.
</Note>

Para entender cómo un tokenizador divide la cadena de entrada, puede usar las funciones [tokens](/docs/es/reference/functions/regular-functions/splitting-merging-functions#tokens) y [tokensForLikePattern](/docs/es/reference/functions/regular-functions/splitting-merging-functions#tokensForLikePattern):

Ejemplo:

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

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

*Trabajo con entradas no ASCII.*
Los índices de texto pueden construirse sobre datos de texto en cualquier idioma y conjunto de caracteres.
Para texto no ASCII, se recomienda el tokenizador `asciiCJK`, ya que gestiona correctamente los límites de palabras de Unicode, incluidos los caracteres CJK.

<a id="preprocessor-argument-optional" />**Argumento de preprocesador (opcional)**. El preprocesador se refiere a una expresión que se aplica a la cadena de entrada antes de la tokenización.

Los casos de uso típicos del argumento de preprocesador incluyen

1. Conversión a minúsculas o mayúsculas, o case folding para habilitar la coincidencia sin distinción entre mayúsculas y minúsculas, p. ej., [lower](/docs/es/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/docs/es/reference/functions/regular-functions/string-functions#lowerUTF8), [caseFoldUTF8](/docs/es/reference/functions/regular-functions/string-functions#caseFoldUTF8).
2. Normalización en UTF-8, p. ej. [normalizeUTF8NFC](/docs/es/reference/functions/regular-functions/string-functions#normalizeUTF8NFC), [normalizeUTF8NFD](/docs/es/reference/functions/regular-functions/string-functions#normalizeUTF8NFD), [normalizeUTF8NFKC](/docs/es/reference/functions/regular-functions/string-functions#normalizeUTF8NFKC), [normalizeUTF8NFKD](/docs/es/reference/functions/regular-functions/string-functions#normalizeUTF8NFKD), [normalizeUTF8NFKCCasefold](/docs/es/reference/functions/regular-functions/string-functions#normalizeUTF8NFKCCasefold), [toValidUTF8](/docs/es/reference/functions/regular-functions/string-functions#toValidUTF8).
3. Eliminación o transformación de caracteres o subcadenas no deseados, como los acentos, p. ej. [extractTextFromHTML](/docs/es/reference/functions/regular-functions/string-functions#extractTextFromHTML), [substring](/docs/es/reference/functions/regular-functions/string-functions#substring), [idnaEncode](/docs/es/reference/functions/regular-functions/string-functions#idnaEncode), [translate](/docs/es/reference/functions/regular-functions/string-replace-functions#translate), [removeDiacriticsUTF8](/docs/es/reference/functions/regular-functions/string-functions#removeDiacriticsUTF8).

La expresión del preprocesador debe transformar un valor de entrada de tipo [String](/docs/es/reference/data-types/string) o [FixedString](/docs/es/reference/data-types/fixedstring) en un valor del mismo tipo.
Si el índice de texto se construyó sobre una columna de tipo `Nullable(T)` o `LowCardinality(T)`, la expresión del preprocesador debe aceptar valores anulables o de baja cardinalidad (es decir, no debe lanzar una excepción).

Ejemplos:

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

Además, la expresión del preprocesador solo debe hacer referencia a la columna o expresión sobre la que está definido el índice de texto.

Ejemplos:

* `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)))`
* No permitido: `INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(col, col))`

No se permite el uso de funciones no deterministas.

<Note>
  Los preprocesadores son, en principio, equivalentes a envolver la columna o expresión del índice con la expresión del preprocesador.
  Por ejemplo, el preprocesador `lower` en `INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))` puede emularse con `INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha')`.
  Esta última forma tiene la desventaja de que el preprocesador emulado solo se aplica si coincide con la condición de filtro de la cláusula WHERE.
  Por ejemplo, `WHERE hasAllTokens(lower(col), [...])` coincide, mientras que `WHERE hasAllTokens(col, [...])` no.
  Por lo tanto, para una experiencia de usuario óptima, recomendamos usar expresiones de preprocesador.
</Note>

Las funciones [hasToken](/docs/es/reference/functions/regular-functions/string-search-functions#hasToken), [hasAllTokens](/docs/es/reference/functions/regular-functions/string-search-functions#hasAllTokens), [hasAnyTokens](/docs/es/reference/functions/regular-functions/string-search-functions#hasAnyTokens) y [hasPhrase](/docs/es/reference/functions/regular-functions/string-search-functions#hasPhrase) usan el preprocesador para transformar primero el término de búsqueda antes de tokenizarlo.
Tenga en cuenta que, dado que el preprocesador solo se aplica en la ruta del índice de texto, los resultados de estas funciones pueden diferir entre las consultas que usan el índice de texto y las que no lo usan (p. ej., `SETTINGS use_skip_indexes = 0`).

Por ejemplo,

```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');
```

equivale a:

```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'));
```

En este caso, la expresión del preprocesador transforma los elementos del array individualmente.

Ejemplo:

```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');
```

Para definir un preprocesador en un índice de texto al crear columnas de tipo [Map](/docs/es/reference/data-types/map), los usuarios deben decidir si el índice se
crea a partir de las claves o de los valores del mapa.

Ejemplo:

```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" />**Argumento posprocesador (opcional)**. El posprocesador es una expresión que se aplica a cada token de salida después de la tokenización.

A diferencia del preprocesador, que transforma toda la cadena de entrada antes de que el tokenizador la divida en tokens, el posprocesador actúa sobre los propios tokens, uno a uno.
Este es el lugar natural para las transformaciones que son intrínsecamente a nivel de token.

Los casos de uso típicos del argumento posprocesador incluyen:

1. **Filtrado de palabras vacías (tokens extremadamente frecuentes)**. Los tokens muy comunes como "the", "a" e "is" aportan poca relevancia a la búsqueda e inflan el índice.
   Puede usar el posprocesador para descartarlos convirtiéndolos en tokens vacíos; los tokens vacíos se ignoran, es decir, no se añaden al índice.
   Ejemplo: `if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str)`
2. **Eliminación de marcas de tiempo**. Las líneas de log suelen empezar con una marca de tiempo estructurada como `2024-01-15T10:23:45` o contenerla.
   Indexar tokens de marcas de tiempo hincha el índice con cadenas que no aportan relevancia para la búsqueda.
   Hay dos enfoques complementarios para ignorar las marcas de tiempo:
   * **Enfoque con posprocesador**: use el tokenizador `splitByString` (división por espacios en blanco) para que la marca de tiempo completa se convierta en un único token y, a continuación, use `parseDateTimeOrNull` para detectarlo y descartarlo.
     Ejemplo: `if(isNull(parseDateTimeOrNull(str, '%Y-%m-%dT%H:%i:%S')), str, '')`
     Para marcas de tiempo con desplazamientos de zona horaria o segundos fraccionarios, use `parseDateTimeBestEffortOrNull(str)` sin una cadena de formato explícita.
   * **Enfoque con preprocesador**: elimine la marca de tiempo de la línea de log completa *antes* de la tokenización mediante una expresión regular.
     Ejemplo: `replaceRegexpAll(str, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')`
     Esto funciona con cualquier tokenizador y es más eficiente, ya que los caracteres de la marca de tiempo nunca se tokenizan.
     Ambos enfoques pueden combinarse: el preprocesador elimina la marca de tiempo, mientras que el posprocesador normaliza o filtra los tokens restantes (p. ej., convierte a minúsculas + elimina palabras de severidad como `ERROR` o `INFO`).
3. **Stemming**. Asignar a cada token su raíz mejora la exhaustividad de la búsqueda al hacer coincidir variantes morfológicas que comparten la misma raíz.
   Por ejemplo, con stemming en inglés, "running", "runs" y "run" se reducen todos a "run", por lo que una consulta de cualquiera de estas variantes coincide con todas ellas.
   ClickHouse proporciona una función [stem](/docs/es/reference/functions/regular-functions/nlp-functions#stem) integrada para varios idiomas.
   Ejemplo: `stem(str, 'en')`
4. **Normalización de mayúsculas y minúsculas**. Convertir los tokens a minúsculas o mayúsculas para permitir la coincidencia sin distinción entre mayúsculas y minúsculas, p. ej. [lower](/docs/es/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/docs/es/reference/functions/regular-functions/string-functions#lowerUTF8).
   Para convertir a minúsculas o mayúsculas, recomendamos usar un preprocesador en lugar de un posprocesador.

La expresión de posprocesamiento transforma tokens de tipo [String](/docs/es/reference/data-types/string) en tokens del mismo tipo.
Además, la expresión de posprocesamiento solo puede hacer referencia a la columna o expresión sobre la que se define el índice de texto.
Cuando la columna es de tipo `Array(String)`, el posprocesador sigue operando sobre tokens individuales como valores `String` simples.

No se permite el uso de funciones no deterministas.

El posprocesador se aplica a cada token generado durante la creación del índice (para el tokenizer `array`, cada elemento del array es un token). En tiempo de consulta, el comportamiento depende de la función:

* Para `hasToken`, `hasAllTokens`, `hasAnyTokens` y `hasPhrase` (con cualquier tokenizer compatible): el posprocesador se aplica tanto a los tokens del texto buscado como al término de búsqueda, lo que permite una coincidencia completamente normalizada (p. ej., búsqueda sin distinción entre mayúsculas y minúsculas). En `hasPhrase`, los tokens posprocesados se colocan de forma contigua, de modo que si el posprocesador descarta un token, no queda ningún hueco posicional y la frase sigue coincidiendo a través de él; por ejemplo, con un posprocesador de palabras vacías que elimina `the`, `hasPhrase(col, 'see cat')` coincide con un documento `see the cat`.
* Para todas las demás funciones (`=`, `IN`, `has`, `hasAny`, `hasAll`, `mapContains*`): solo el término de búsqueda se posprocesa para la búsqueda asistida por el índice; el predicado a nivel de fila sigue comparándose con los valores originales de la columna.

Ejemplos:

* Eliminar palabras vacías mediante una expresión de posprocesamiento:

```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();
```

* Elimina las marcas de tiempo mediante una expresión de posprocesamiento:

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

* Elimine las marcas de tiempo mediante una expresión de preprocesamiento:

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

* Elimine las marcas de tiempo mediante una expresión combinada de preprocesador y posprocesador:

```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']);
```

* Aplica stemming a los tokens mediante una expresión de posprocesamiento:

```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']);
```

**Compatibilidad con funciones**.

Para los predicados que consultan el índice de texto, el preprocesador y el posprocesador se aplican al valor de búsqueda antes de la comprobación a nivel de gránulo, de modo que la búsqueda en el índice use los mismos tokens que se almacenaron al crear el índice.
Para la mayoría de las funciones (`=`, `IN`, `startsWith`, `endsWith`, `LIKE`, `mapContains*`), el índice de texto se usa solo para omitir bloques de datos irrelevantes; ClickHouse sigue verificando cada fila resultante con el predicado original sobre los datos originales de la columna.
Para las funciones de búsqueda de tokens (`hasToken`, `hasAllTokens`, `hasAnyTokens`), el índice de texto es la vía principal de evaluación: ClickHouse normaliza el término buscado mediante el mismo preprocesador, tokenizador y posprocesador que se aplicaron al crear el índice, y usa esta forma normalizada tanto para las partes de tabla indexadas como para las no indexadas. Con un posprocesador, los tokens del texto buscado también se normalizan en tiempo de consulta (para cualquier tokenizador, no solo `array`), de modo que ambos lados de la comparación se transforman de forma coherente y el resultado no depende de si el índice se lee directamente (ajuste `query_plan_direct_read_from_text_index`) ni de si una parte determinada tiene un índice materializado; por ejemplo, habilitar la coincidencia sin distinción entre mayúsculas y minúsculas para `hasAllTokens(col, ['FOO'])` con un posprocesador `lower`.
Sin `support_phrase_search`, `hasPhrase` usa el índice solo como referencia y verifica cada fila resultante con el predicado original; además, un posprocesador normaliza de la misma manera tanto la frase como los tokens del texto buscado, por lo que el resultado es independiente de la ruta de lectura, y los tokens que el posprocesador descarta no rompen la adyacencia de la frase. Con `support_phrase_search = 1`, `hasPhrase` usa lecturas directas exactas (y sigue aplicando el posprocesador, si lo hay).
Los tokens de búsqueda que el posprocesador convierte en una cadena vacía se ignoran; es decir, se tratan como ausentes de la frase de búsqueda.

| Función                                                                                                    | Admite un preprocesador                                     | Tokenizadores compatibles                                | Admite un posprocesador |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------- | ----------------------- |
| `=`                                                                                                        | sí                                                          | todos                                                    | sí                      |
| `IN`                                                                                                       | sí                                                          | todos                                                    | sí                      |
| [hasToken](/docs/es/reference/functions/regular-functions/string-search-functions#hasToken)                     | sí                                                          | todos (diseñado para `splitByNonAlpha`)                  | sí                      |
| [hasAnyTokens(col, str)](/docs/es/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | sí                                                          | todos                                                    | sí                      |
| [hasAllTokens(col, str)](/docs/es/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | sí                                                          | todos                                                    | sí                      |
| [hasAnyTokens(col, arr)](/docs/es/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | no (los elementos del array se tratan como tokens tal cual) | todos                                                    | sí                      |
| [hasAllTokens(col, arr)](/docs/es/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | no (los elementos del array se tratan como tokens tal cual) | todos                                                    | sí                      |
| [hasPhrase](/docs/es/reference/functions/regular-functions/string-search-functions#hasPhrase)                   | sí                                                          | `splitByNonAlpha`, `splitByString`, `ngrams`, `asciiCJK` | sí                      |
| [startsWith](/docs/es/reference/functions/regular-functions/string-functions#startsWith)                        | sí                                                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | sí                      |
| [endsWith](/docs/es/reference/functions/regular-functions/string-functions#endsWith)                            | sí                                                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | sí                      |
| [like](/docs/es/reference/functions/regular-functions/string-search-functions#like)                             | sí¹                                                         | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹  | sí¹                     |
| [match](/docs/es/reference/functions/regular-functions/string-search-functions#match)                           | sí¹                                                         | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹  | sí¹                     |
| [ilike](/docs/es/reference/functions/regular-functions/string-search-functions#like)                            | sí² (`lower`/`upper` solamente)                             | `splitByNonAlpha`, `array`²                              | no²                     |
| [mapContainsKey](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsKey)             | sí                                                          | todos                                                    | sí                      |
| [mapContainsValue](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsValue)         | sí                                                          | todos                                                    | sí                      |
| [mapContainsKeyLike](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike)     | sí                                                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | sí                      |
| [mapContainsValueLike](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) | sí                                                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | sí                      |
| [has](/docs/es/reference/functions/regular-functions/array-functions#has)                                       | sí                                                          | `array`                                                  | sí                      |
| [hasAny](/docs/es/reference/functions/regular-functions/array-functions#hasAny)                                 | sí                                                          | `array`                                                  | sí                      |
| [hasAll](/docs/es/reference/functions/regular-functions/array-functions#hasAll)                                 | sí                                                          | `array`                                                  | sí                      |

¹ `LIKE` y `match` usan lectura directa como indicación para los tokenizadores listados; de lo contrario, recurren a una exploración por fuerza bruta.
Además, `LIKE` admite *lectura directa (sin indicación)* (habilitada mediante `use_text_index_like_evaluation_by_dictionary_scan`) para los tokenizadores `splitByNonAlpha` y `array` sin preprocesador ni posprocesador.

² `ILIKE` solo es compatible mediante lectura directa (sin indicación) (`use_text_index_like_evaluation_by_dictionary_scan = 1`, tokenizador `splitByNonAlpha` o `array`).
No hay fallback al uso del índice como indicación: si la configuración está deshabilitada o el tokenizador no está en el conjunto compatible, el índice no se usa para `ILIKE`.
El preprocesador, si está presente, debe ser `lower` o `upper`; los posprocesadores no son compatibles.

**Experimental: soporte para el argumento de búsqueda de frases (opcional)**.

El parámetro experimental `support_phrase_search` (predeterminado: `0`) controla si el índice almacena las posiciones de los tokens.
Cuando se establece en `1`, el índice también almacena datos de posición (en un archivo `.pos`), lo que permite la coincidencia exacta de frases mediante lecturas directas para la función [`hasPhrase`](#functions-example-hasphrase).
Almacenar posiciones aumenta el tamaño del índice en disco y el costo de escritura, por lo que es una opción de activación explícita.
El formato en disco aún no es estable, por lo que este parámetro es experimental y puede cambiar en una versión futura.
Por lo tanto, crear un índice con `support_phrase_search = 1` requiere que esté habilitado el ajuste de MergeTree [`allow_experimental_text_index_phrase_search`](/docs/es/reference/settings/merge-tree-settings#allow_experimental_text_index_phrase_search).
Establezca `support_phrase_search = 0` (el valor predeterminado) para conservar el almacenamiento solo con posting lists; los índices de texto creados sin este argumento siguen sin posiciones.

<Warning>
  Este argumento es experimental y solo debe usarse para pruebas.
  Establezca el ajuste de MergeTree [`allow_experimental_text_index_phrase_search`](/docs/es/reference/settings/merge-tree-settings#allow_experimental_text_index_phrase_search) para habilitar el almacenamiento de posiciones.
</Warning>

<details markdown="1">
  <summary>Parámetros avanzados opcionales</summary>

  Los valores predeterminados de los siguientes parámetros avanzados funcionan bien prácticamente en cualquier situación.
  No recomendamos modificarlos.

  El parámetro opcional `dictionary_block_size` (predeterminado: 512) especifica el tamaño de los bloques del diccionario en filas.

  El parámetro opcional `dictionary_block_frontcoding_compression` (predeterminado: 1) especifica si los bloques del diccionario usan front coding para la compresión.

  El parámetro opcional `posting_list_block_size` (predeterminado: 1048576) especifica el tamaño de los bloques de posting lists en filas.

  El parámetro opcional `posting_list_codec` (predeterminado: `none`) especifica el códec de la posting list:

  * `none` - las posting lists se almacenan sin compresión adicional.
  * `bitpacking` - aplica [codificación diferencial (delta)](https://en.wikipedia.org/wiki/Delta_encoding) y, a continuación, [bit-packing](https://dev.to/madhav_baby_giraffe/bit-packing-the-secret-to-optimizing-data-storage-and-transmission-m70) (cada uno dentro de bloques de tamaño fijo). Ralentiza las consultas SELECT; no se recomienda por el momento.

  Los parámetros avanzados anteriores también pueden establecerse alternativamente a nivel de tabla mediante los ajustes de MergeTree correspondientes: [`text_index_dictionary_block_size`](/docs/es/reference/settings/merge-tree-settings#text_index_dictionary_block_size), [`text_index_dictionary_block_frontcoding_compression`](/docs/es/reference/settings/merge-tree-settings#text_index_dictionary_block_frontcoding_compression), [`text_index_posting_list_block_size`](/docs/es/reference/settings/merge-tree-settings#text_index_posting_list_block_size) y [`text_index_posting_list_codec`](/docs/es/reference/settings/merge-tree-settings#text_index_posting_list_codec).
  Se aplican a cada índice de texto de la tabla que no especifique el parámetro explícitamente.

  El principal caso de uso de los ajustes a nivel de tabla es cambiar los parámetros de índice de una tabla existente sin eliminar y volver a crear el índice de texto en todas las partes de la tabla.
  Cambiar un ajuste a nivel de tabla aplica los nuevos parámetros solo a los índices de texto creados para nuevas partes; las partes existentes conservan su layout actual.

  Un argumento proporcionado en la definición del índice tiene prioridad sobre el ajuste de la tabla; por ejemplo:

  ```sql theme={null}
  CREATE TABLE table(
      s String,
      -- Este índice usa 'bitpacking', anulando el valor predeterminado a nivel de tabla que aparece abajo:
      INDEX idx_a s TYPE text(tokenizer = 'splitByNonAlpha', posting_list_codec = 'bitpacking'),
      -- Este índice hereda 'none' del ajuste de la tabla:
      INDEX idx_b lower(s) TYPE text(tokenizer = 'splitByNonAlpha'))
  ENGINE = MergeTree()
  ORDER BY tuple()
  SETTINGS text_index_posting_list_codec = 'none';
  ```
</details>

*Granularidad del índice.*
Los índices de texto se implementan en ClickHouse como un tipo de [índices de omisión](/docs/es/reference/engines/table-engines/mergetree-family/mergetree#skip-index-types).
Sin embargo, a diferencia de otros índices de omisión, los índices de texto usan una granularidad infinita (100 millones).
Esto puede verse en la definición de tabla de un índice de texto.

Ejemplo:

```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 granularidad de índice tan alta garantiza que el índice de texto se cree para toda la parte.
Se ignora cualquier granularidad de índice especificada explícitamente.

<div id="using-a-text-index">
  ## Uso de un índice de texto
</div>

Usar un índice de texto en las consultas SELECT es sencillo, ya que las funciones habituales de búsqueda de cadenas aprovechan el índice automáticamente.
Si no existe ningún índice en una columna o en una parte de la tabla, las funciones de búsqueda de cadenas recurren a búsquedas exhaustivas lentas.

<Note>
  Recomendamos usar las funciones `hasAnyTokens` y `hasAllTokens` para buscar en el índice de texto; consulta [más abajo](#functions-example-hasanytokens-hasalltokens).
  Estas funciones funcionan con todos los tokenizadores disponibles y con todas las expresiones posibles de preprocesador y posprocesador.
  Como las demás funciones compatibles son históricamente anteriores al índice de texto, en muchos casos tuvieron que conservar su comportamiento heredado (p. ej., sin compatibilidad con preprocesadores ni posprocesadores).
</Note>

<div id="functions-support">
  ### Funciones compatibles
</div>

El índice de texto se puede usar si se utilizan funciones de texto en la cláusula `WHERE` o en las cláusulas `PREWHERE`:

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

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

`=` ([equals](/docs/es/reference/functions/regular-functions/comparison-functions#equals)) coincide con el término de búsqueda completo.

Ejemplo:

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

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

`IN` ([in](/docs/es/reference/functions/regular-functions/in-functions)) es similar a `equals`, pero coincide con cualquiera de los términos de búsqueda.

Ejemplo:

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

<Note>
  `NOT IN` (`notIn`) no es compatible con el índice de texto.
</Note>

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

<Note>
  Actualmente, estas funciones usan el índice de texto para filtrar solo si el tokenizador del índice es `splitByNonAlpha`, `ngrams` o `sparseGrams`.
</Note>

<Note>
  `NOT LIKE` (`notLike`) no es compatible con el índice de texto.
</Note>

Para usar `LIKE` ([like](/docs/es/reference/functions/regular-functions/string-search-functions#like)) y la función [match](/docs/es/reference/functions/regular-functions/string-search-functions#match) con índices de texto, ClickHouse debe poder extraer tokens completos del término de búsqueda.
En el caso del índice con el tokenizador `ngrams`, esto ocurre si la longitud de las cadenas buscadas entre comodines es igual o mayor que la longitud del ngram.

Ejemplo de índice de texto con el tokenizador `splitByNonAlpha`:

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

`support` en el ejemplo podría coincidir con `support`, `supports`, `supporting`, etc.
Este tipo de consulta es una consulta por subcadena y un índice de texto no puede acelerarla.

Para aprovechar un índice de texto en consultas LIKE, el patrón LIKE debe reescribirse de la siguiente manera:

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

Los espacios a la izquierda y a la derecha de `support` garantizan que el término pueda extraerse como token.

Afortunadamente, hay un caso especial en el que ClickHouse puede aprovechar el índice invertido para acelerar significativamente las consultas LIKE.

Consulta la [sección sobre optimización del rendimiento de LIKE/ILIKE](#like-ilike-queries-perf) para obtener más información.

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

[multiSearchAny](/docs/es/reference/functions/regular-functions/string-search-functions#multiSearchAny) y su variante UTF-8 [multiSearchAnyUTF8](/docs/es/reference/functions/regular-functions/string-search-functions#multiSearchAnyUTF8) comprueban si alguna de varias subcadenas literales aparece en el texto de entrada, y [multiMatchAny](/docs/es/reference/functions/regular-functions/string-search-functions#multiMatchAny) comprueba si alguna de varias expresiones regulares coincide.
Estas funciones usan el índice de texto en las mismas condiciones que `LIKE` y `match` (véase arriba): ClickHouse debe poder extraer tokens completos de cada subcadena buscada, y la lista de subcadenas buscadas debe ser constante.
Se lee un gránulo si alguna subcadena buscada puede estar presente en él.

En `multiMatchAny`, si un patrón no puede reducirse a un requisito de token (por ejemplo, `.*`, que coincide con cualquier documento), no se puede usar el índice de texto y la consulta recurre a un escaneo completo.

Al igual que con `LIKE` y `match`, la búsqueda de subcadenas y expresiones regulares funciona mejor con los tokenizadores `ngrams` y `sparseGrams`.
Estos tokenizadores indexan n-grams de caracteres superpuestos, por lo que una subcadena buscada se descompone en n-grams presentes en el índice allí donde aparece como subcadena, independientemente de si empieza o termina en medio de una palabra.
Por tanto, una subcadena buscada puede usarse tal cual, siempre que tenga al menos la misma longitud que el tamaño del n-gram.

Ejemplo del índice de texto con el tokenizador `ngrams`:

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

El tokenizador `splitByNonAlpha`, en cambio, solo indexa tokens completos (palabras enteras).
Como una cadena de búsqueda puede empezar o terminar en medio de una palabra, ClickHouse descarta los tokens inicial y final de cada cadena de búsqueda, de modo que el índice solo pueda descartar gránulos usando tokens completos.
Para que la búsqueda por subcadenas y expresiones regulares use el índice con `splitByNonAlpha`, rodea cada cadena de búsqueda con caracteres separadores (como espacios) para que forme uno o más tokens completos.

Ejemplo del índice de texto con el tokenizador `splitByNonAlpha`:

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

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

Al igual que `LIKE`, las funciones [startsWith](/docs/es/reference/functions/regular-functions/string-functions#startsWith) y [endsWith](/docs/es/reference/functions/regular-functions/string-functions#endsWith) solo pueden usar un índice de texto si se pueden extraer tokens completos del término de búsqueda.
En el caso del índice con el `tokenizador` `ngrams`, esto sucede si la longitud de las cadenas buscadas entre comodines es igual o mayor que la longitud del ngram.
Cuando un índice de texto usa un posprocesador, estas funciones aún pueden usar el índice en modo Hint si los tokens de hint extraídos siguen siendo no vacíos después de la normalización. Si la normalización elimina todos los tokens de hint, el índice no se usa para ese predicado.

Ejemplo del índice de texto con el `tokenizador` `splitByNonAlpha`:

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

En el ejemplo, solo `clickhouse` se considera un token.
`support` no es un token, porque puede coincidir con `support`, `supports`, `supporting`, etc.

Para encontrar todas las filas que comienzan con `clickhouse supports`, termine el patrón de búsqueda con un espacio final:

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

Del mismo modo, `endsWith` debe usarse con un espacio inicial:

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

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

<Note>
  `hasToken` presenta ciertos inconvenientes cuando se usa para lookups en índices de texto con tokenizadores que no son `splitByNonAlpha` y/o expresiones de preprocesador/postprocesador.
  Recomendamos usar en su lugar `hasAnyTokens` y `hasAllTokens`.

  Las variantes sin distinción entre mayúsculas y minúsculas `hasTokenCaseInsensitive` y `hasTokenCaseInsensitiveOrNull` no reconocen los índices de texto: siempre se ejecutan como un escaneo completo de filas, incluso en columnas con índice de texto. Para la coincidencia sin distinción entre mayúsculas y minúsculas, use un preprocesador o postprocesador `lower(...)` y combínelo con `hasToken` / `hasAllTokens` / `hasAnyTokens`.
</Note>

La función [hasToken](/docs/es/reference/functions/regular-functions/string-search-functions#hasToken) busca la coincidencia de un único token dado.

A diferencia de las funciones mencionadas anteriormente, no tokenizan el término de búsqueda (asumen que la entrada es un único token).

Ejemplo:

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

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

Las funciones [hasAnyTokens](/docs/es/reference/functions/regular-functions/string-search-functions#hasAnyTokens) y [hasAllTokens](/docs/es/reference/functions/regular-functions/string-search-functions#hasAllTokens) buscan coincidencias con uno o con todos los tokens proporcionados.

Estas dos funciones aceptan los tokens de búsqueda como una cadena, que se tokenizará con el mismo tokenizador que se usa para la columna del índice, o como un array de tokens ya procesados, al que no se aplicará tokenización antes de la búsqueda.
Consulta la documentación de la función para obtener más información.

Ejemplo:

```sql theme={null}
-- Tokens de búsqueda pasados como argumento de tipo String
SELECT count() FROM table WHERE hasAnyTokens(comment, 'clickhouse olap');
SELECT count() FROM table WHERE hasAllTokens(comment, 'clickhouse olap');

-- Tokens de búsqueda pasados como 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 función [hasPhrase](/docs/es/reference/functions/regular-functions/string-search-functions#hasPhrase) busca una frase: todos los tokens deben aparecer de forma consecutiva y en el mismo orden que en la cadena de búsqueda.

A diferencia de `hasAllTokens`, que solo requiere que todos los tokens estén presentes en algún lugar, `hasPhrase` exige que aparezcan como una secuencia consecutiva.
La frase de búsqueda se tokeniza con el mismo tokenizador configurado para la columna de índice.
Cuando el índice de texto usa un posprocesador, la frase de búsqueda también se normaliza antes de la búsqueda en el índice.
Ten en cuenta que la función requiere uno de estos tokenizadores: `splitByNonAlpha`, `splitByString`, `ngrams` o `asciiCJK`.

Ejemplo:

```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 función de array [has](/docs/es/reference/functions/regular-functions/array-functions#has) coincide con un solo token en el array de cadenas.

Ejemplo:

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

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

Las funciones de array [hasAny](/docs/es/reference/functions/regular-functions/array-functions#hasAny) y [hasAll](/docs/es/reference/functions/regular-functions/array-functions#hasAll) comprueban si la columna de array indexada contiene alguna o todas las cadenas buscadas de un conjunto constante.

Ejemplo:

```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 función [mapContains](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsKey) (es un alias de `mapContainsKey`) busca coincidencias entre los tokens extraídos de la cadena buscada y las claves de un mapa.
El comportamiento es similar al de la función `equals` con una columna `String`.
El índice de texto solo se utiliza si se creó sobre una expresión `mapKeys(map)`.

Ejemplo:

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

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

La función [mapContainsValue](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsValue) busca coincidencias entre los tokens extraídos de la cadena buscada y los valores de un map.
El comportamiento es similar al de la función `equals` con una columna `String`.
El índice de texto solo se utiliza si se creó sobre una expresión `mapValues(map)`.

Ejemplo:

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

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

Las funciones [mapContainsKeyLike](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike) y [mapContainsValueLike](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) comparan un patrón con todas las claves o todos los valores (respectivamente) de un Map.

Ejemplo:

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

El [operator\[\]](/docs/es/reference/operators/index#access-operators) de acceso puede usarse con el índice de texto para filtrar claves y valores. El índice de texto solo se utiliza si se crea sobre las expresiones `mapKeys(map)` o `mapValues(map)`, o sobre ambas.

Ejemplo:

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

Consulte los siguientes ejemplos de uso de columnas de tipo `Array(T)` y `Map(K, V)` con el índice de texto.

<div id="text-index-example-array">
  ### Indexación de columnas Array(String)
</div>

Piense en una plataforma de blogs, donde los autores clasifican las entradas de su blog mediante palabras clave.
Queremos que los usuarios descubran contenido relacionado buscando temas o haciendo clic en ellos.

Considere esta definición de tabla:

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

Sin un índice de texto, encontrar posts con una palabra clave específica (p. ej., `clickhouse`) obliga a escanear todas las entradas:

```sql theme={null}
SELECT count() FROM posts WHERE has(keywords, 'clickhouse'); -- exploración completa de la tabla lenta - comprueba cada palabra clave en cada publicación
```

A medida que la plataforma crece, esto se vuelve cada vez más lento porque la consulta debe examinar el array `keywords` de cada fila.
Para solucionar este problema de rendimiento, definimos un índice de texto para la columna `keywords`:

```sql theme={null}
ALTER TABLE posts ADD INDEX keywords_idx(keywords) TYPE text(tokenizer = splitByNonAlpha);
ALTER TABLE posts MATERIALIZE INDEX keywords_idx; -- No olvides reconstruir el índice para los datos existentes
```

<div id="text-index-example-map">
  ### Indexación de columnas de tipo Map
</div>

En muchos casos de uso de observabilidad, los mensajes de log se dividen en "componentes" y se almacenan con los tipos de datos adecuados, p. ej., fecha y hora para el timestamp, enum para el nivel de log, etc.
Los campos de métricas se almacenan mejor como pares clave-valor.
Los equipos de operaciones necesitan buscar de forma eficiente en los logs para depuración, incidentes de seguridad y monitorización.

Considere la siguiente tabla de logs:

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

Sin un índice de texto, buscar en datos de tipo [Map](/docs/es/reference/data-types/map) requiere un escaneo completo de la tabla:

```sql theme={null}
-- Encuentra todos los logs con datos de rate limiting:
SELECT * FROM logs WHERE has(mapKeys(attributes), 'rate_limit'); -- escaneo lento de tabla completa

-- Encuentra todos los logs de una IP específica:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- escaneo lento de tabla completa
```

A medida que aumenta el volumen de logs, estas consultas se vuelven lentas.

La solución es crear un índice de texto para las claves y los valores de [Map](/docs/es/reference/data-types/map).
Usa [mapKeys](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapKeys) para crear un índice de texto cuando necesites encontrar logs por nombres de campos o tipos de atributos:

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

Usa [mapValues](/docs/es/reference/functions/regular-functions/tuple-map-functions#mapValues) para crear un índice de texto cuando necesites buscar dentro del contenido propiamente dicho de los atributos:

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

Consultas de ejemplo:

```sql theme={null}
-- Encuentra todas las solicitudes con límite de tasa:
SELECT * FROM logs WHERE mapContainsKey(attributes, 'rate_limit'); -- rápido

-- Encuentra todos los logs de una IP específica:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- rápido

-- Encuentra todos los logs donde algún atributo incluye un error:
SELECT * FROM logs WHERE mapContainsValueLike(attributes, '% error %'); -- rápido
```

<div id="text-index-example-json">
  ### Indexación de columnas JSON
</div>

Los índices de texto pueden usarse con columnas `JSON` de tres formas:

1. **Índices en subcolumnas específicas** — crea un índice de texto en una ruta JSON conocida, igual que en una columna normal. Esto indexa los *valores* de esa ruta.
2. **Índices basados en rutas con [JSONAllPaths](/docs/es/reference/functions/regular-functions/json-functions#JSONAllPaths)** — indexan *todas las rutas* presentes en cada gránulo para omitir los gránulos que no pueden contener la ruta consultada. Es similar a las columnas `Map`.
3. **Índices basados en valores con [JSONAllValues](/docs/es/reference/functions/regular-functions/json-functions#JSONAllValues)** — indexan *todos los valores* de todas las rutas JSON para acelerar la búsqueda de texto completo en cualquier subcolumna JSON con un solo índice.

<div id="json-indexes-on-subcolumns">
  #### Índices sobre subcolumnas específicas
</div>

Se puede crear un índice de omisión en cualquier subcolumna de JSON con la misma sintaxis que para las columnas normales.

Hay dos formas de hacer referencia a una subcolumna de JSON en una expresión de índice:

* **Ruta tipada** declarada en la indicación de tipo de JSON: acceda directamente por nombre: `json.a`.
* **Ruta dinámica** con conversión explícita: use la sintaxis de conversión `::`: `json.b::String`.

Ejemplo de definición de índice:

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

Ejemplo de consulta:

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

Ejemplo de consulta:

```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">
  #### Índices basados en rutas con JSONAllPaths
</div>

Al igual que con las columnas `Map`, se pueden crear índices de texto en columnas [JSON](/docs/es/reference/data-types/newjson) mediante [`JSONAllPaths`](/docs/es/reference/functions/regular-functions/json-functions#JSONAllPaths).
El índice almacena el conjunto de rutas JSON presentes en cada gránulo y las utiliza para omitir los gránulos en los que no está presente la ruta consultada.

Definición de ejemplo del índice:

```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"}');
```

Puede usar `EXPLAIN indexes = 1` para verificar que se está utilizando el índice de omisión.
Cuando una ruta existe solo en una parte, el índice se salta la otra parte.

Ejemplo:

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

Cuando una ruta no existe en ninguna parte, se omiten todas las partes y todos los gránulos.

Ejemplo:

```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` también usa el índice: omite los gránulos en los que la ruta no existe (ya que el valor sería `NULL`):

Ejemplo:

```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">
  #### Índices basados en valores con JSONAllValues
</div>

Los índices de texto pueden utilizarse para acelerar las búsquedas en columnas [JSON](/docs/es/reference/data-types/newjson) mediante la función [`JSONAllValues`](/docs/es/reference/functions/regular-functions/json-functions#JSONAllValues).

`JSONAllValues` devuelve todos los valores de una columna JSON como `Array(String)`.
Los valores de tipos de datos que no son cadenas (p. ej., enteros y arrays) se convierten a su representación textual.
Un índice de texto creado con `JSONAllValues` indexa estas representaciones textuales en todas las rutas JSON de cada fila.
Este índice puede acelerar después las consultas que filtran por subcolumnas JSON individuales.
Cuando una consulta filtra por una subcolumna específica (p. ej., `data.user_name = 'alice'`), el índice de texto puede descartar rápidamente las filas (y los gránulos) que no contienen los tokens de búsqueda en ninguno de sus valores JSON.

<Note>
  El índice puede producir falsos positivos cuando distintas rutas JSON contienen los mismos tokens.
  Por ejemplo, si la fila 1 tiene `{"a": "hello", "b": "world"}` y una consulta busca `data.a = 'world'`, el índice de texto no puede distinguir que `world` pertenece a la ruta `b`, no a `a`.
  En esos casos, el índice no descartará la fila, y el filtro sobre los datos reales de la columna se encargará de la evaluación final.
  Este es el mismo comportamiento que en otros casos de uso de índices de texto, donde el índice actúa como un prefiltro rápido.
</Note>

<div id="json-all-values-creating-the-index">
  ##### Creación del índice
</div>

Ejemplo de definición del índice:

```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">
  ##### Patrones de consulta admitidos
</div>

Una vez creado el índice, puede acelerar las consultas sobre subcolumnas JSON usando las mismas funciones que para las columnas `String` y la función `equals` para todas las columnas.

Acceso a las subcolumnas:

```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');
```

Acceso a subcolumnas mediante `CAST` explícito:

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

Operador `IN`:

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

<div id="text-index-phrase-search">
  ### Búsqueda por frases
</div>

Una búsqueda habitual en un índice de texto, por ejemplo

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

coincide con todas las filas que contienen los tokens indicados en cualquier orden.
En el ejemplo, la fila `While she stayed in Tokyo, the weather was great.` cumple el filtro.

En cambio, la búsqueda de frases consiste en hacer coincidir los tokens en el orden dado.
Por ejemplo,

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

coincide con cualquier fila que contenga la secuencia de tokens `weather in Tokyo`, como en `How is the weather in Tokyo?`?

El índice de texto acelera la búsqueda de frases al intersectar las listas de postings de todos los tokens de la frase para identificar los gránulos candidatos.
Dentro de esos gránulos, ClickHouse verifica la adyacencia exacta de los tokens.
Este proceso es relativamente costoso y más lento que las consultas normales de búsqueda de texto.
Para acelerar las consultas de búsqueda de frases, habilite el almacenamiento de posiciones en el índice de texto (consulte `Optional parameters` más arriba).

`hasPhrase` puede usarse junto con los tokenizadores `splitByNonAlpha`, `splitByString`, `ngrams` y `asciiCJK`.
La cadena de la frase indicada se tokeniza usando el tokenizador del índice.
Los caracteres separadores de la frase se ignoran: `hasPhrase(text, 'quick+brown')` es equivalente a `hasPhrase(text, 'quick brown')`, siempre que `splitByNonAlpha` se use como tokenizador.

<div id="text-index-phrase-search-example">
  #### Ejemplo
</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 fila 2 (`'New weather in York'`) no coincide porque los tokens no están en el orden correcto.
La fila 3 (`'weather in New Orleans'`) no coincide porque no contiene el token `'York'`.

<div id="performance-tuning">
  ## Optimización del rendimiento
</div>

<div id="direct-read">
  ### Lectura directa
</div>

Ciertos tipos de consultas de texto pueden acelerarse considerablemente gracias a una optimización llamada "lectura directa".

Ejemplo:

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

La optimización de lectura directa responde a la consulta exclusivamente mediante el índice de texto (es decir, búsquedas en el índice de texto), sin acceder a la columna de texto subyacente.
Las búsquedas en el índice de texto leen relativamente pocos datos y, por tanto, son mucho más rápidas que los índices de omisión habituales en ClickHouse (que hacen una búsqueda en un índice de omisión, seguida de la carga y el filtrado de los gránulos restantes).

Lectura directa se controla con dos ajustes:

* El ajuste [query\_plan\_direct\_read\_from\_text\_index](/docs/es/reference/settings/session-settings#query_plan_direct_read_from_text_index) (`true` de forma predeterminada) especifica si lectura directa está habilitado en general.
* El ajuste [use\_skip\_indexes\_on\_data\_read](/docs/es/reference/settings/session-settings#use_skip_indexes_on_data_read) era un requisito previo para lectura directa en las versiones de ClickHouse \< 26.4.

**Funciones compatibles**

La optimización de lectura directa admite las funciones `hasToken`, `hasAllTokens` y `hasAnyTokens`.
Si el índice de texto se define con un tokenizador `array`, lectura directa también es compatible con las funciones `equals`, `has`, `hasAny`, `hasAll`, `mapContainsKey` y `mapContainsValue`.
Estas funciones también pueden combinarse mediante los operadores `AND`, `OR` y `NOT`.
Las cláusulas `WHERE` o `PREWHERE` también pueden contener filtros adicionales distintos de las funciones de búsqueda de texto (para columnas de texto u otras columnas); en ese caso, la optimización de lectura directa seguirá utilizándose, pero será menos eficaz (solo se aplica a las funciones de búsqueda de texto compatibles).

Para comprobar si una consulta utiliza lectura directa, ejecute la consulta con `EXPLAIN PLAN actions = 1`.
Por ejemplo, una consulta con lectura directa deshabilitado

```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, -- deshabilitar lectura directa
```

devuelve

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

mientras que la misma consulta, ejecutada con `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, -- habilitar la lectura directa
```

devuelve

```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 segunda salida de EXPLAIN PLAN contiene una columna virtual `__text_index_<index_name>_<function_name>_<id>`.
Si esta columna está presente, significa que se usa lectura directa.

Si la cláusula de filtro WHERE solo contiene funciones de búsqueda de texto, la consulta puede evitar por completo leer los datos de la columna y obtener el mayor beneficio de rendimiento con lectura directa.
Sin embargo, incluso si se accede a la columna de texto en otra parte de la consulta, lectura directa seguirá aportando una mejora del rendimiento.

**Lectura directa como pista**

Lectura directa como pista se basa en los mismos principios que lectura directa normal, pero añade un filtro adicional generado a partir de los datos del índice de texto sin dejar de usar la columna de texto subyacente.
Se utiliza en funciones para las que leer solo desde el índice de texto produciría falsos positivos.

Las funciones compatibles son: `like`, `startsWith`, `endsWith`, `equals`, `has`, `hasPhrase`, `mapContainsKey` y `mapContainsValue`.

El filtro adicional puede aportar más selectividad y, en combinación con otros filtros, restringir aún más el conjunto de resultados, lo que ayuda a reducir la cantidad de datos leídos de otras columnas.

Lectura directa como pista se controla mediante la configuración [query\_plan\_text\_index\_add\_hint](/docs/es/reference/settings/session-settings#query_plan_text_index_add_hint) (habilitada de forma predeterminada).

Ejemplo de consulta sin pista:

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

devuelve

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

mientras que la misma consulta, ejecutada con `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
```

devuelve

```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)
[...]
```

En la segunda salida de EXPLAIN PLAN, puedes ver que se ha añadido una conjunción adicional (`__text_index_...`) a la condición del filtro.
Gracias a la optimización [PREWHERE](/docs/es/reference/statements/select/prewhere), la condición del filtro se descompone en tres conjunciones independientes, que se aplican en orden de complejidad computacional creciente.
Para esta consulta, el orden de aplicación es `__text_index_...`, luego `greaterOrEquals(...)` y, por último, `like(...)`.
Este orden permite omitir todavía más gránulos de datos que los que ya omiten el índice de texto y el filtro original, antes de leer las columnas pesadas utilizadas en la consulta tras la cláusula `WHERE`, lo que reduce aún más la cantidad de datos que hay que leer.

<div id="like-ilike-queries-perf">
  ### Consultas LIKE/ILIKE
</div>

Cuando un patrón de consulta LIKE/ILIKE es `%<alpha-numeric-characters-without-spaces>%` y el tokenizador del índice de texto es `splitByNonAlpha` o `array`, ClickHouse aprovecha el índice invertido para acelerar significativamente las consultas LIKE/ILIKE. Para ello, ClickHouse examina el diccionario del índice invertido en lugar de hacer un escaneo completo de la tabla para encontrar el patrón coincidente.

Cuando la optimización está habilitada, las consultas LIKE/ILIKE deberían ser significativamente más rápidas que un escaneo completo de la tabla. Sin embargo, cuando el patrón coincide con la mayoría de los tokens del diccionario, el rendimiento puede ser peor que con un escaneo completo de la tabla. Afortunadamente, existe un mecanismo de fallback para evitarlo.

La optimización se controla mediante una configuración:

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

El mecanismo de fallback se controla mediante dos configuraciones:

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

Esta optimización solo admite las funciones `like` e `ilike`.

<div id="caching">
  ### Almacenamiento en caché
</div>

Hay diferentes cachés de servidor disponibles para almacenar en búfer partes del índice de texto en memoria (consulte la sección [Detalles de implementación](#implementation)):
Actualmente, existen cachés para los encabezados deserializados, los tokens y las listas de postings del índice de texto, con el fin de reducir la E/S.
Use las configuraciones [use\_text\_index\_header\_cache](/docs/es/reference/settings/session-settings#use_text_index_header_cache), [use\_text\_index\_tokens\_cache](/docs/es/reference/settings/session-settings#use_text_index_tokens_cache) y [use\_text\_index\_postings\_cache](/docs/es/reference/settings/session-settings#use_text_index_postings_cache) para deshabilitar la lectura y escritura de las cachés individuales por parte de las consultas.

Para limpiar las cachés, use la sentencia [SYSTEM CLEAR TEXT INDEX CACHES](/docs/es/reference/statements/system#drop-text-index-caches)

Consulte las siguientes configuraciones del servidor para ajustar las cachés.

<div id="caching-tokens">
  #### Configuración de la caché de tokens
</div>

| Configuración                                                                                                                   | Descripción                                                                                                        |
| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [text\_index\_tokens\_cache\_policy](/docs/es/reference/settings/server-settings/settings#text_index_tokens_cache_policy)            | Nombre de la política de la caché de tokens del índice de texto.                                                   |
| [text\_index\_tokens\_cache\_size](/docs/es/reference/settings/server-settings/settings#text_index_tokens_cache_size)                | Tamaño máximo de la caché en bytes.                                                                                |
| [text\_index\_tokens\_cache\_max\_entries](/docs/es/reference/settings/server-settings/settings#text_index_tokens_cache_max_entries) | Número máximo de tokens deserializados en la caché.                                                                |
| [text\_index\_tokens\_cache\_size\_ratio](/docs/es/reference/settings/server-settings/settings#text_index_tokens_cache_size_ratio)   | Tamaño de la cola protegida en la caché de tokens del índice de texto en relación con el tamaño total de la caché. |

<div id="caching-header">
  #### Configuración de la caché de encabezados
</div>

| Setting                                                                                                                         | Description                                                                                                             |
| ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [text\_index\_header\_cache\_policy](/docs/es/reference/settings/server-settings/settings#text_index_header_cache_policy)            | Nombre de la política de caché de encabezados del índice de texto.                                                      |
| [text\_index\_header\_cache\_size](/docs/es/reference/settings/server-settings/settings#text_index_header_cache_size)                | Tamaño máximo de la caché en bytes.                                                                                     |
| [text\_index\_header\_cache\_max\_entries](/docs/es/reference/settings/server-settings/settings#text_index_header_cache_max_entries) | Número máximo de encabezados deserializados en la caché.                                                                |
| [text\_index\_header\_cache\_size\_ratio](/docs/es/reference/settings/server-settings/settings#text_index_header_cache_size_ratio)   | Tamaño de la cola protegida en la caché de encabezados del índice de texto en relación con el tamaño total de la caché. |

<div id="caching-posting-lists">
  #### Configuración de la caché de listas de postings
</div>

| Configuración                                                                                                                       | Descripción                                                                                                                    |
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| [text\_index\_postings\_cache\_policy](/docs/es/reference/settings/server-settings/settings#text_index_postings_cache_policy)            | Nombre de la política de caché de las listas de postings del índice de texto.                                                  |
| [text\_index\_postings\_cache\_size](/docs/es/reference/settings/server-settings/settings#text_index_postings_cache_size)                | Tamaño máximo de la caché en bytes.                                                                                            |
| [text\_index\_postings\_cache\_max\_entries](/docs/es/reference/settings/server-settings/settings#text_index_postings_cache_max_entries) | Número máximo de listas de postings deserializadas en la caché.                                                                |
| [text\_index\_postings\_cache\_size\_ratio](/docs/es/reference/settings/server-settings/settings#text_index_postings_cache_size_ratio)   | Tamaño de la cola protegida en la caché de listas de postings del índice de texto en relación con el tamaño total de la caché. |

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

El índice de texto presenta actualmente las siguientes limitaciones:

* La materialización de índices de texto con un número elevado de tokens (p. ej., 10 mil millones de tokens) puede consumir cantidades significativas de memoria. La
  materialización de índices de texto puede producirse directamente (`ALTER TABLE <table> MATERIALIZE INDEX <index>`) o indirectamente durante las fusiones de partes.
* No es posible materializar índices de texto en partes con más de 4.294.967.296 (= 2^32 = aprox. 4,2 mil millones) filas. Sin un índice de texto materializado, las consultas recurren a una búsqueda exhaustiva lenta dentro de la parte. Como estimación del peor caso, suponga que una parte contiene una única columna de tipo String y que la configuración de MergeTree `max_bytes_to_merge_at_max_space_in_pool` (valor predeterminado: 150 GB) no se ha modificado. En este caso, esto ocurre si la columna contiene, de media, menos de 29,5 caracteres por fila. En la práctica, las tablas también contienen otras columnas y el umbral es varias veces menor (según el número, el tipo y el tamaño de las demás columnas).

<div id="text-index-vs-bloom-filter-indexes">
  ## Índices de texto frente a índices basados en filtros Bloom
</div>

Los predicados sobre cadenas pueden acelerarse mediante índices de texto e índices basados en filtros Bloom (tipos de índice `bloom_filter`, `ngrambf_v1`, `tokenbf_v1`, `sparse_grams`), pero ambos difieren fundamentalmente en su diseño y en los casos de uso a los que están destinados:

**Índices de filtros Bloom**

* Se basan en estructuras de datos probabilísticas que pueden producir falsos positivos.
* Solo pueden responder preguntas de pertenencia a conjuntos; es decir, si la columna puede contener el token X o si definitivamente no lo contiene.
* Almacenan información a nivel de gránulo para permitir omitir rangos amplios durante la ejecución de consultas.
* Son difíciles de ajustar correctamente (consulta [aquí](/docs/es/reference/engines/table-engines/mergetree-family/mergetree#n-gram-bloom-filter) un ejemplo).
* Son bastante compactos (unos pocos kilobytes o megabytes por parte).

**Índices de texto**

* Construyen un índice invertido determinista sobre tokens. El propio índice no puede producir falsos positivos.
* Están optimizados específicamente para cargas de trabajo de búsqueda de texto.
* Almacenan información a nivel de fila, lo que permite una búsqueda eficiente de términos.
* Son bastante grandes (de decenas a cientos de megabytes por parte).

Los índices basados en filtros Bloom solo admiten la búsqueda de texto completo como un "efecto secundario":

* No admiten tokenización ni preprocesamiento avanzados.
* No admiten búsquedas con varios tokens.
* No ofrecen las características de rendimiento esperadas de un índice invertido.

Los índices de texto, en cambio, están diseñados específicamente para la búsqueda de texto completo:

* Proporcionan tokenización y preprocesamiento
* Ofrecen soporte eficiente para `hasAllTokens`, `LIKE`, `match` y funciones similares de búsqueda de texto.
* Tienen una escalabilidad significativamente mayor para grandes corpus de texto.

<div id="implementation">
  ## Detalles de implementación
</div>

Cada índice de texto consta de dos estructuras de datos (abstractas):

* un diccionario que asigna cada token a una lista de postings, y
* un conjunto de listas de postings, cada una de las cuales representa un conjunto de números de fila.

El índice de texto se construye para toda la parte.
A diferencia de otros índices de omisión, el índice de texto puede fusionarse en lugar de reconstruirse durante la fusión de las partes de datos (véase más abajo).

Durante la creación del índice, se crean tres archivos (por parte):

**Archivo de bloques de diccionario (.dct)**

Los tokens del índice de texto se ordenan y se almacenan en bloques de diccionario de 512 tokens cada uno (el tamaño del bloque se puede configurar con el parámetro `dictionary_block_size`).
Un archivo de bloques de diccionario (.dct) consta de todos los bloques de diccionario de todos los gránulos de índice de una parte.

**Archivo de encabezado del índice (.idx)**

El archivo de encabezado del índice contiene, para cada bloque de diccionario, el primer token del bloque y su desplazamiento relativo dentro del archivo de bloques de diccionario.

Esta estructura de índice disperso es similar al [índice de clave primaria disperso](/docs/es/guides/clickhouse/data-modelling/sparse-primary-indexes)) de ClickHouse.

**Archivo de listas de postings (.pst)**

Las listas de postings de todos los tokens se almacenan secuencialmente en el archivo de listas de postings.
Para ahorrar espacio y, al mismo tiempo, permitir operaciones rápidas de intersección y unión, las listas de postings se almacenan como [bitmaps Roaring](https://roaringbitmap.org/).
Si la lista de postings es más grande que `posting_list_block_size`, se divide en varios bloques que se almacenan secuencialmente en el archivo de listas de postings.

**Archivo de posiciones (.pos)**

Opcional, solo si el argumento del índice `support_phrase_search = 1`.
Almacena las posiciones de los tokens dentro de las filas coincidentes.

**Fusión de índices de texto**

Cuando se fusionan partes de datos, no es necesario reconstruir el índice de texto desde cero; en su lugar, puede fusionarse eficientemente en una etapa independiente del proceso de fusión.
Durante esta etapa, los diccionarios ordenados de los índices de texto de cada parte de entrada se leen y se combinan en un nuevo diccionario unificado.
Los números de fila de las listas de postings también se recalculan para reflejar sus nuevas posiciones en la parte de datos fusionada, usando una correspondencia entre los números de fila antiguos y los nuevos que se crea durante la fase inicial de la fusión.
Este método de fusionar índices de texto es similar a cómo se fusionan las [projections](/docs/es/reference/statements/alter/projection#projection-indexes) con la columna `_part_offset`.
Si el índice no está materializado en la parte de origen, se construye, se escribe en un archivo temporal y luego se fusiona junto con los índices de las otras partes y de otros archivos de índice temporales.

**Depuración**

La table function [mergeTreeTextIndex](/docs/es/reference/functions/table-functions/mergeTreeTextIndex) puede usarse para inspeccionar índices de texto.

<div id="hacker-news-dataset">
  ## Ejemplo: conjunto de datos de Hacker News
</div>

Veamos las mejoras del rendimiento de los índices de texto en un conjunto de datos grande con mucho contenido textual.
Usaremos 28.7M filas de comentarios del popular sitio web Hacker News.
Esta es la tabla sin índice de texto:

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

Los 28,7 millones de filas están en un archivo Parquet en S3; vamos a insertarlas en la tabla `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');
```

Usaremos `ALTER TABLE` para añadir un índice de texto en la columna comment y luego materializarlo:

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

Ahora, ejecutemos consultas con las funciones `hasToken`, `hasAnyTokens` y `hasAllTokens`.
Los siguientes ejemplos mostrarán la gran diferencia de rendimiento entre un análisis estándar del índice y la optimización de lectura directa.

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

`hasToken` comprueba si el texto contiene un token específico.
Buscaremos el token sensible a mayúsculas y minúsculas 'ClickHouse'.

**Lectura directa deshabilitada (escaneo estándar)**
De forma predeterminada, ClickHouse usa el índice de omisión para filtrar gránulos y luego lee los datos de la columna de esos gránulos.
Podemos simular este comportamiento deshabilitando la lectura directa.

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

**Lectura directa habilitada (lectura rápida del índice)**
Ahora ejecutamos la misma consulta con la lectura directa habilitada (opción predeterminada).

```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 consulta de lectura directa es más de 45 veces más rápida (0.362s frente a 0.008s) y procesa significativamente menos datos (9.51 GB frente a 3.15 MB) al leer únicamente del índice.

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

`hasAnyTokens` comprueba si el texto contiene al menos uno de los tokens indicados.
Buscaremos comentarios que contengan 'love' o 'ClickHouse'.

**Direct read desactivado (Exploración estándar)**

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

**Lectura directa habilitada (lectura rápida del índice)**

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

La mejora de velocidad es aún más espectacular para esta búsqueda habitual con "OR".
La consulta es casi 89 veces más rápida (1.329s frente a 0.015s) al evitar el escaneo de toda la columna.

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

`hasAllTokens` comprueba si el texto contiene todos los tokens indicados.
Buscaremos comentarios que contengan tanto 'love' como 'ClickHouse'.

**Lectura directa deshabilitada (escaneo estándar)**
Incluso con lectura directa deshabilitada, el índice de omisión estándar sigue siendo eficaz.
Reduce las 28.7M filas a solo 147.46K filas, pero aun así debe leer 57.03 MB de la columna.

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

**Lectura directa activada (lectura rápida del índice)**
La lectura directa resuelve la consulta usando los datos del índice y solo lee 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
```

Para esta búsqueda "AND", la optimización de lectura directa es más de 26 veces más rápida (0.184s frente a 0.007s) que el escaneo estándar del índice de omisión.

<div id="compound-search">
  ### 4. Búsqueda compuesta: OR, AND, NOT, ...
</div>

La optimización de lectura directa también se aplica a las expresiones booleanas compuestas.
Aquí, realizaremos una búsqueda de 'ClickHouse' OR 'clickhouse' sin distinguir entre mayúsculas y minúsculas.

**Lectura directa deshabilitada (Escaneo estándar)**

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

**Lectura directa activada (lectura rápida del índice)**

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

Al combinar los resultados del índice, la consulta de lectura directa es 34 veces más rápida (0.450s frente a 0.013s) y evita leer 9.58 GB de datos de la columna.
Para este caso concreto, `hasAnyTokens(comment, ['ClickHouse', 'clickhouse'])` sería la sintaxis recomendada y más eficiente.

<div id="related-content">
  ## Contenido relacionado
</div>

* Blog: [Anuncio de la disponibilidad general de la búsqueda de texto completo de ClickHouse](https://clickhouse.com/blog/full-text-search-ga-release)
* Blog: [Cómo crear una búsqueda de texto completo de alto rendimiento para almacenamiento de objetos](https://clickhouse.com/blog/clickhouse-full-text-search-object-storage)
* Video: [Introducción a la búsqueda de texto completo en ClickHouse](https://www.youtube.com/watch?v=9zPmf1a_heU)
* Video: [Entre bastidores: la búsqueda de texto completo a la escala y velocidad de ClickHouse](https://www.youtube.com/watch?v=8JbqE_ubfkU)
* Presentation: [Por dentro de la búsqueda de texto completo en ClickHouse: rápida, nativa y columnar](https://github.com/ClickHouse/clickhouse-presentations/blob/master/2025-tumuchdata-munich/ClickHouse_%20full-text%20search%20-%2011.11.2025%20Munich%20Database%20Meetup.pdf)
* Presentation: [Índices invertidos de bases de datos: el porqué, el qué y el cómo, FOSDEM 2026](https://presentations.clickhouse.com/2026-fosdem-inverted-index/Inverted_indexes_the_what_the_why_the_how.pdf)

**Material obsoleto**

* Blog: [Presentamos los índices invertidos en ClickHouse](https://clickhouse.com/blog/clickhouse-search-with-inverted-indices)
* Blog: [Por dentro de la búsqueda de texto completo en ClickHouse: rápida, nativa y columnar](https://clickhouse.com/blog/clickhouse-full-text-search)
* Video: [Índices de texto completo: diseño y experimentos](https://www.youtube.com/watch?v=O_MnyUkrIq8)
