Crear un índice de texto
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.Los índices de texto pueden usarse con cualquier versión de ClickHouse >= 26.2, independientemente de la configuración compatibility.
Query
- String y FixedString,
- Array(String) y Array(FixedString),
- Map, ya sea sobre la columna
Mapusando el tokenizadorkeyValuePairs, o solo sobre las claves o los valores del mapa mediante la función mapKeys o mapValues, y - JSON (mediante las funciones JSONAllPaths y
JSONAllValues).
Array(Nullable(String or FixedString)).
Como alternativa, para añadir un índice de texto a una tabla existente:
Query
Query
Query
tokenizer especifica el tokenizador:
splitByNonAlphadivide cadenas por caracteres ASCII no alfanuméricos (consulte la función splitByNonAlpha).splitByString(S)divide cadenas usando determinadas cadenas separadorasSdefinidas por el usuario (consulte la función 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[' '].splitByRegexp(regexp[, match_tokens])divide cadenas según una expresión regularregexpdefinida por el usuario. El argumentoregexpes obligatorio; por ejemplo,tokenizer = splitByRegexp('[a-zA-Z]+[0-9]'). El argumento opcionalmatch_tokens(falsede forma predeterminada; también se aceptan0/1) controla qué representaregexp:- con
match_tokens = false(el valor predeterminado),regexprepresenta separadores: los tokens son los fragmentos de texto (no vacíos) situados entre coincidencias sucesivas (la misma semántica que la función splitByRegexp). - con
match_tokens = true, en su lugar se busca la coincidencia directa deregexp: cada coincidencia aporta como máximo un token —su primer grupo de captura, o la coincidencia completa siregexpno tiene ningún grupo de captura— y todo lo que queda fuera de las coincidencias se descarta. Por ejemplo,tokenizer = splitByRegexp('tag:(\w+)', true)indexahelloyworlda partir detag:hello tag:world, descartando el prefijotag:. La coincidencia usa RE2, el mismo motor que emplean todas las funciones basadas en expresiones regulares en ClickHouse; solo se utiliza el primer grupo de captura, por lo que los grupos posteriores únicamente restringen lo que coincide. Un grupo de captura que no participó en la coincidencia, o que coincidió con una cadena vacía, no aporta ningún token, y el análisis siempre se reanuda después de la coincidencia completa en lugar de después del fragmento capturado (de modo que las coincidencias nunca se superponen). Conmatch_tokens = true, tenga en cuenta que los patrones de búsqueda de las funciones de búsqueda de texto se tokenizan con el propio tokenizador del índice, por lo que un patrón de cadena simple que no coincida por sí mismo conregexpno genera tokens y, por tanto, tampoco resultados. Los patrones pasados como arrays a las funciones de búsqueda de texto no se tokenizan y, por ello, se recomiendan para su uso con tokenizadores de expresiones regulares.
- con
asciiCJKdivide cadenas en tokens usando reglas de límites de palabra de Unicode (similares a Unicode Text Segmentation (UAX #29)). 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, se convierten en tokens de un solo carácter.chinese[(granularity)]segmenta texto chino en palabras mediante un diccionario y un modelo oculto de Markov (el algoritmo sigue jieba; los datos del diccionario y del modelo integrados derivan de cppjieba). A diferencia deasciiCJK, que trata cada carácter no ASCII como un token de un solo carácter,chineseagrupa caracteres chinos consecutivos en palabras (por ejemplo,北京大学se convierte en un token北京大学en lugar de cuatro tokens de un solo carácter). Esto genera tokens más significativos para texto chino y una mayor calidad de búsqueda. Para texto general o mixto, useasciiCJK; para texto exclusivamente chino, usechinese. El argumento opcionalgranularityes'coarse_grained'(el valor predeterminado si no se especifica) o'fine_grained'. Este último además enumera subpalabras superpuestas; por ejemplo,北京邮电大学también genera北京,邮电,大学. La tokenización de granularidad fina mejora la exhaustividad a costa de un índice más grande. Busque en un índice de textochinesecon hasAnyTokens / hasAllTokens (que tokenizan el patrón con el tokenizadorchinese), no conhasToken(que solo divide en separadores ASCII).icu(locale)divide cadenas en tokens de palabras mediante la segmentación de palabras Unicode (UAX #29) de la biblioteca ICU. Para sistemas de escritura que no separan las palabras con espacios en blanco (por ejemplo, chino, japonés o tailandés), ICU aplica una segmentación basada en diccionarios, por lo que, a diferencia deasciiCJK, este texto se divide en palabras significativas de varios caracteres en lugar de caracteres individuales. Aquí, «diccionario» se refiere a las listas de palabras incluidas en ICU para esos sistemas de escritura (consultebrkitr/dictionaries); ICU elige la división más probable entre esas palabras.localees la configuración regional de ICU que se pasa al segmentador; la segmentación depende principalmente del sistema de escritura y del diccionario, y la configuración regional selecciona la adaptación específica de ICU. Es un parámetro obligatorio; por ejemplo,tokenizer = icu('ja')otokenizer = icu('zh'). Las configuraciones regionales disponibles pueden enumerarse conSELECT * FROM system.collations.japanesedivide el texto japonés en palabras mediante el analizador morfológico MeCab. A diferencia deasciiCJK, que emite tokens de un solo carácter para entradas CJK, este tokenizador realiza una segmentación adecuada en palabras. Requiere un diccionario que se carga en tiempo de ejecución desde la configuración del servidor (consulte Japanese tokenizer dictionary).ngrams(N)divide cadenas enN-grams del mismo tamaño (consulte la función 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 menosmin_lengthy como máximomax_lengthcaracteres (inclusive) (consulte la función sparseGrams). A menos que se especifique explícitamente,min_lengthymax_lengthtoman los valores predeterminados 3 y 100. Si se proporciona el parámetromin_cutoff_length, solo se devuelven n-grams con una longitud mayor o igual quemin_cutoff_length. En comparación conngrams(N), el tokenizadorsparseGramsproduce 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.arrayno realiza tokenización; es decir, cada valor de fila es un token (consulte la función array). Para garantizar la compatibilidad con otros sistemas,keywordestá disponible como alias dearray.keyValuePairscombina los key-value pairs de una columnaMapen un único token. Esto ayuda a realizar lookups combinados de key-value comoWHERE map['key'] = 'value'(consulte el tokenizadorkeyValuePairs).
Diccionario del tokenizador japonés
El tokenizadorjapanese requiere un diccionario MeCab, que no se incluye con ClickHouse. Proporcione uno en la configuración del servidor:
dictionary_locationes la ubicación de un archivo comprimido de un diccionario MeCab compilado. Puede utilizarse cualquier diccionario oficial, como IPADIC o UniDic. La ubicación debe terminar con una extensión de archivo compatible (por ejemplo,.tar.gz,.tar.zsto.zip), a partir de la cual se detecta el tipo de archivo; por lo tanto, se rechaza una URL sin dicha extensión (p. ej.,https://example.com/download). Ubicaciones compatibles:- una ruta local
file://; - una URL
http(s)://, descargada directamente (úsela para un objeto público o prefirmado); - un almacén de objetos compatible con S3 — AWS S3, GCS, MinIO, local, etc. (no solo AWS) — especificado como
s3:///gs:///oss://o como una URL completahttp(s)://endpoint/bucket/key. Para un bucket privado, proporcione las credenciales de S3 como elementos secundarios de<japanese>(consulte el ejemplo a continuación).
- una ruta local
dictionary_shaes el SHA-256 de ese archivo comprimido. Se verifica antes de cargar el diccionario; si no coincide, el diccionario no se carga y se genera un error.
dictionary_sha en todas las réplicas.
Para leer desde un bucket privado compatible con S3, agregue la configuración de S3 como elementos secundarios de <japanese>, junto a dictionary_location y dictionary_sha:
access_key_id, secret_access_key, region, no_sign_request, use_environment_credentials, …) son las mismas opciones de autenticación de S3 que se usan en otras partes de ClickHouse. Su presencia también hace que una URL http(s):// se obtenga mediante el client de S3 (con firma de solicitudes) en lugar de descargarse directamente.
Una vez configurado el diccionario, el tokenizador japanese puede usarse de esta forma:
Query
Response
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, pueden pasarse en cualquier orden.Query
Response
asciiCJK, ya que gestiona correctamente los límites de palabra de Unicode, incluidos los caracteres CJK.
Para idiomas que no separan las palabras mediante espacios en blanco (por ejemplo, chino, japonés o tailandés), el tokenizador icu(locale) genera tokens de palabras significativos de varios caracteres mediante la segmentación de palabras basada en diccionarios de ICU.
En el caso del japonés, el tokenizador japanese (MeCab) segmenta el texto en palabras en lugar de caracteres individuales y, por lo general, ofrece mejores resultados de búsqueda.
En el caso del chino, el tokenizador chinese (jieba) segmenta el texto en palabras en lugar de caracteres individuales y, por lo general, ofrece mejores resultados de búsqueda.
Argumento del preprocesador (opcional). El preprocesador hace referencia a una expresión que se aplica a la cadena de entrada antes de la tokenización.
Los casos de uso habituales del argumento del preprocesador incluyen
- 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, lowerUTF8, caseFoldUTF8.
- Normalización en UTF-8, p. ej. normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, normalizeUTF8NFKCCasefold, toValidUTF8.
- Eliminación o transformación de caracteres o subcadenas no deseados, como los acentos, p. ej. extractTextFromHTML, substring, idnaEncode, translate, removeDiacriticsUTF8.
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)))
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))
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.SETTINGS use_skip_indexes = 0).
Por ejemplo,
Query
Query
Query
Query
- 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 posprocesamiento 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) - Eliminación de marcas de tiempo. Las líneas de log suelen empezar con una marca de tiempo estructurada como
2024-01-15T10:23:45o 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 posprocesamiento: 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, useparseDateTimeOrNullpara 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, useparseDateTimeBestEffortOrNull(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 posprocesamiento normaliza o filtra los tokens restantes (p. ej., convierte a minúsculas + elimina palabras de severidad comoERRORoINFO).
- Enfoque con posprocesamiento: use el tokenizador
- 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 integrada para varios idiomas.
Ejemplo:
stem(str, 'en') - 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, lowerUTF8. Para convertir a minúsculas o mayúsculas, recomendamos usar un preprocesador en lugar del posprocesamiento.
Array(String), el posprocesamiento sigue operando sobre tokens individuales como valores String simples.
No se permite el uso de funciones no deterministas.
El posprocesamiento se aplica a cada token generado durante la creación del índice (para el tokenizador array, cada elemento del array es un token). En tiempo de consulta, el comportamiento depende de la función:
- Para
hasToken,hasAllTokens,hasAnyTokensyhasPhrase(con cualquier tokenizador compatible): el posprocesamiento se aplica tanto a los tokens del texto en el que se busca 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). ParahasPhrase, los tokens posprocesados se posicionan de forma contigua, por lo que un token que el posprocesamiento descarta no deja ningún hueco posicional y la frase sigue coincidiendo a través de él; p. ej., con un posprocesamiento de palabras vacías que descartathe,hasPhrase(col, 'see cat')coincide con un documentosee the cat. La única excepción eshasPhraseen un índicesplitByRegexp, que no admite un posprocesamiento (la combinación se rechaza con una excepción). - Para todas las demás funciones (
=,IN,has,hasAny,hasAll,mapContains*): solo se posprocesa el término de búsqueda para la consulta mediante indicación de índice; el predicado a nivel de fila sigue comparando con los valores originales de la columna.
- Elimine palabras vacías mediante una expresión de posprocesamiento:
- Elimine las marcas de tiempo mediante una expresión de posprocesamiento:
- Elimine las marcas de tiempo mediante una expresión de preprocesador:
- Elimine las marcas de tiempo mediante una expresión combinada de preprocesador y posprocesamiento:
- Aplica stemming a los tokens mediante una expresión de posprocesamiento:
=, 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 posprocesamiento 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 posprocesamiento, el texto en el que se busca también se normaliza 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 posprocesamiento lower.
Sin support_phrase_search, hasPhrase usa el índice solo como indicación y verifica cada fila resultante con el predicado original; además, un posprocesamiento 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 posprocesamiento descarta no rompen la adyacencia de la frase. Con support_phrase_search = 1, hasPhrase usa lecturas directas exactas (y sigue aplicando el posprocesamiento, si lo hay). Esta compatibilidad con el posprocesamiento no se extiende al tokenizador splitByRegexp: se rechaza hasPhrase en un índice splitByRegexp combinado con un posprocesamiento (consulte la nota al pie ³ a continuación).
Los tokens de búsqueda que el posprocesamiento convierte en una cadena vacía se ignoran; es decir, se tratan como ausentes de la frase de búsqueda.
¹
LIKE y match usan lectura directa como indicación para los tokenizadores listados; de lo contrario, recurren a una búsqueda exhaustiva.
Además, LIKE admite la evaluación mediante un dictionary scan (habilitada mediante use_text_index_like_evaluation_by_dictionary_scan) para los tokenizadores splitByNonAlpha y array sin preprocesador ni posprocesamiento; véase la sección consultas LIKE/ILIKE más abajo.
² ILIKE solo es compatible mediante la evaluación con un dictionary scan (use_text_index_like_evaluation_by_dictionary_scan = 1, tokenizador splitByNonAlpha o array).
No hay fallback al uso del índice como indicación para los patrones que el dictionary scan no admite: 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 posprocesamientos no son compatibles.
Además, algunas needles no son elegibles; véase consultas LIKE/ILIKE.
³ hasPhrase sobre un índice de texto splitByRegexp no admite un posprocesamiento: la combinación se rechaza con una excepción, porque la reescritura a nivel de fila del posprocesamiento asume tokens de estilo splitByNonAlpha divididos por espacios en blanco. Sin posprocesamiento, splitByRegexp es totalmente compatible con hasPhrase.
⁴ startsWith y endsWith buscan los tokens completos del needle, y el token situado en el extremo abierto del needle está incompleto porque el valor continúa allí: startsWith(col, 'ClickHouse is') busca el token ClickHouse, mientras que startsWith(col, 'ClickHouse') no tiene ningún token completo que buscar.
Este último se evalúa en su lugar mediante un dictionary scan (use_text_index_like_evaluation_by_dictionary_scan = 1, tokenizador splitByNonAlpha o array, sin preprocesador ni posprocesamiento), que es también la vía que toma col LIKE 'ClickHouse%', porque la pasada del analyzer optimize_rewrite_like_perfect_affix la reescribe como startsWith.
Consulte consultas LIKE/ILIKE.
⁵ La columna tokenizadores compatibles omite el tokenizador keyValuePairs, que es un tokenizador especializado para columnas Map.
Experimental: argumento de soporte 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.
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.
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.
Granularidad del índice.
Los índices de texto se implementan en ClickHouse como un tipo de índices de omisión.
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:
Query
Response
Uso de un índice de texto
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.Recomendamos usar las funciones
hasAnyTokens y hasAllTokens para buscar en el índice de texto; consulta más abajo.
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).Funciones compatibles
El índice de texto se puede usar si se utilizan funciones de texto en la cláusulaWHERE o en las cláusulas PREWHERE:
=
= (equals) coincide con el término de búsqueda completo.
Ejemplo:
IN
IN (in) es similar a equals, pero coincide con cualquiera de los términos de búsqueda.
Ejemplo:
NOT IN (notIn) no es compatible con el índice de texto.LIKE and match
Actualmente, estas funciones usan el índice de texto para filtrar solo si el tokenizador del índice es
splitByNonAlpha, ngrams o sparseGrams.NOT LIKE (notLike) no es compatible con el índice de texto.LIKE (like) y la función 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:
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:
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 para obtener más información.
multiSearchAny and multiMatchAny
multiSearchAny y su variante UTF-8 multiSearchAnyUTF8 comprueban si alguna de varias subcadenas literales aparece en el texto de entrada, y 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:
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:
startsWith and endsWith
Al igual que LIKE, las funciones startsWith y 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:
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:
endsWith debe usarse con un espacio inicial:
hasToken
hasToken presenta ciertos inconvenientes cuando se usa para lookups en índices de texto con tokenizadores que no son splitByNonAlpha y/o expresiones de preprocesador/posprocesamiento.
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 una expresión de posprocesamiento lower(...) y combínelo con hasToken / hasAllTokens / hasAnyTokens.hasAnyTokens y hasAllTokens, no tokeniza el término de búsqueda (asume que la entrada es un único token).
Ejemplo:
hasAnyTokens and hasAllTokens
Las funciones hasAnyTokens y 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:
hasPhrase
La función 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 del índice.
Cuando el índice de texto usa un posprocesamiento, 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, splitByRegexp, ngrams, asciiCJK o icu.
Ejemplo:
has
La función de array has coincide con un solo token en el array de cadenas.
Ejemplo:
hasAny y hasAll
Las funciones de array hasAny y hasAll comprueban si la columna de array indexada contiene alguna o todas las cadenas buscadas de un conjunto constante.
Ejemplo:
mapContains
La función mapContains (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:
mapContainsValue
La función 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:
mapContainsKeyLike and mapContainsValueLike
Las funciones mapContainsKeyLike y mapContainsValueLike comparan un patrón con todas las claves o todos los valores (respectivamente) de un Map.
Ejemplo:
operator[]
El operator[] de acceso puede usarse con el índice de texto para filtrar claves y valores.
El índice de texto se utiliza si emplea un tokenizador keyValuePairs sobre la columna Map, o si se construye sobre una expresión de índice mapKeys(map) o mapValues(map).
En el primer caso (tokenizador keyValuePairs), puede usarse la lectura directa; en caso contrario, lectura directa con hint.
Ejemplo:
Array(T) y Map(K, V) con el índice de texto.
Indexación de columnas Array(String)
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:clickhouse) obliga a escanear todas las entradas:
keywords de cada fila.
Para solucionar este problema de rendimiento, definimos un índice de texto para la columna keywords:
Indexación de columnas de tipo Map
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:Búsquedas combinadas de clave-valor con el tokenizador keyValuePairs
Utilice el tokenizador keyValuePairs sobre la propia columna Map cuando busque una clave concreta que contenga un valor concreto:
(key, value) de una fila en un único token, de modo que el índice sabe qué valor pertenece a qué clave.
Los índices mapKeys y mapValues descritos más abajo no pueden responder a una consulta así, porque indexan claves y valores de forma independiente: pueden indicar que algún atributo de una fila tiene el valor error, pero no que se tratara del atributo level.
Un par se almacena como el token key ‖ value ‖ length(key).
La longitud final al final mantiene inequívoco el límite entre la clave y el valor, por lo que ambos pueden contener bytes arbitrarios: a diferencia de unirlos con un separador como key=value, una clave o un valor que contenga el separador no puede producir una coincidencia falsa.
La función de tabla mergeTreeTextIndex devuelve las partes decodificadas de cada token en las columnas token_key y token_value.
Se aplican las siguientes restricciones:
- El índice debe crearse sobre un
Mapcuyas claves y valores sean de tipoStringoLowCardinality(String).FixedStringse rechaza porque la columna almacena los bytes de relleno mientras que la constante buscada no, lo que provocaría que se omitieran filas de forma silenciosa.Nullablese rechaza porque la codificación no permite distinguir un valor vacío deNULL. - Los argumentos
preprocessor,postprocessorysupport_phrase_searchse rechazan al crear la tabla. - Por ahora, solo
=sobre un elemento de map se resuelve mediante el índice. Otras búsquedas en map, comomapContainsKey,mapContainsValue, sus variantes*LikeeIN, recurren a un escaneo por fuerza bruta. map['key']devuelve el valor predeterminado del tipo del valor cuando la clave no existe, por lo quemap['key'] = ''también estruepara las filas que no contienen la clave y, por tanto, no tienen token. Un predicado de este tipo recurre a un escaneo por fuerza bruta.- Si una fila contiene la misma clave más de una vez,
map['key']es el valor de su primera aparición, y el índice hace coincidir esa aparición.
Búsqueda de claves y valores con mapKeys y mapValues
Usa mapKeys para crear un índice de texto cuando necesites encontrar logs por nombres de campos o tipos de atributos:
Indexación de columnas JSON
Los índices de texto pueden usarse con columnasJSON de tres formas:
- Í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.
- Índices basados en rutas con 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. - Índices basados en valores con 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.
Índices sobre subcolumnas específicas
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.
Query
Query
Response
Query
Response
Índices basados en rutas con JSONAllPaths
Al igual que con las columnasMap, se pueden crear índices de texto en columnas JSON mediante 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:
Query
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:
Query
Response
Query
Response
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:
Query
Response
Índices basados en valores con JSONAllValues
Los índices de texto pueden utilizarse para acelerar las búsquedas en columnas JSON mediante la funciónJSONAllValues.
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.
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.Creación del índice
Ejemplo de definición del índice:Patrones de consulta admitidos
Una vez creado el índice, puede acelerar las consultas sobre subcolumnas JSON usando las mismas funciones que para las columnasString y la función equals para todas las columnas.
Acceso a las subcolumnas:
CAST explícito:
IN:
Búsqueda por frases
Una búsqueda habitual en un índice de texto, por ejemploWhile 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,
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, splitByRegexp, ngrams, asciiCJK e icu.
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.
Ejemplo
Query
Response
'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'.
Optimización del rendimiento
Lectura directa
Ciertos tipos de consultas de texto pueden acelerarse considerablemente gracias a una optimización llamada “lectura directa”. Ejemplo:- El ajuste query_plan_direct_read_from_text_index (
truede forma predeterminada) especifica si lectura directa está habilitado en general. - El ajuste use_skip_indexes_on_data_read era un requisito previo para lectura directa en las versiones de ClickHouse < 26.4.
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.
Si el índice de texto se define con un tokenizador keyValuePairs, lectura directa es compatible con equals sobre un elemento de un map (map['key'] = 'value').
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
query_plan_direct_read_from_text_index = 1
__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 (habilitada de forma predeterminada).
Ejemplo de consulta sin pista:
query_plan_text_index_add_hint = 1
__text_index_...) a la condición del filtro.
Gracias a la optimización 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.
Consultas LIKE/ILIKE
Cuando el patrón de una consulta LIKE/ILIKE es%<caracteres-alfanuméricos-sin-espacios>%, <caracteres-alfanuméricos-sin-espacios>% o %<caracteres-alfanuméricos-sin-espacios> y el tokenizador del índice de texto es splitByNonAlpha o array, ClickHouse aprovecha el inverted index para acelerar de forma significativa las consultas LIKE/ILIKE. Para ello, ClickHouse recorre el Diccionario del inverted index en lugar de realizar un escaneo completo de tabla para encontrar el patrón coincidente.
El uso que se da al resultado del escaneo del Diccionario depende de dónde esté anclado el needle:
%value%coincide con una fila si y solo si coincide con alguno de los tokens de esa fila, por lo que el índice resuelve la consulta por sí solo: una lectura directa (sin pista) que elimina el predicado original.value%y%valueanclan el needle al valor completo, mientras que el escaneo del Diccionario solo puede anclarlo a un token, de modo que el scan devuelve un superset de las filas coincidentes y se utiliza como una lectura directa como pista.
startsWith(col, 'value') y endsWith(col, 'value') cuando el needle no contiene ningún token completo que buscar.
Esta es la vía que sigue en realidad la mayoría de los patrones value% y %value, ya que la pasada del analyzer optimize_rewrite_like_perfect_affix (habilitada de forma predeterminada) reescribe col LIKE 'value%' como startsWith(col, 'value') y col LIKE '%value' como endsWith(col, 'value').
Los needles que abarcan varios tokens siguen usando los tokens completos del needle y no necesitan un escaneo del Diccionario.
Como caso especial, si el tokenizador del índice es array, entonces cualquier patrón es apto para la optimización: anclas, signos de puntuación, wildcards _, varios needles separados por % y metacharacters escapados.
Cuando la optimización está habilitada, las consultas LIKE/ILIKE deberían ser considerablemente más rápidas que un escaneo completo de tabla. No obstante, cuando el patrón coincide con la mayoría de los tokens del Diccionario, el rendimiento puede ser peor que el de un escaneo completo de tabla. Por suerte, existe un mecanismo de fallback para evitarlo.
La mejora de rendimiento de un patrón value% o %value proviene de omitir gránulos, por lo que depende de cómo estén distribuidas las filas coincidentes. Los needles que coinciden con filas en todos los gránulos no descartan nada, y la consulta paga el coste del escaneo del Diccionario además del full scan que habría realizado de todos modos. Deshabilita use_text_index_like_evaluation_by_dictionary_scan para este tipo de workloads.
La optimización se controla mediante una configuración:
El mecanismo de fallback se controla mediante dos configuraciones:
Esta optimización solo admite las funciones like, ilike, startsWith y endsWith.
Por lo general, requiere un índice sin preprocessor ni posprocesamiento (ilike también acepta lower o upper como función de preprocessor).
En el caso de ilike, la búsqueda recurre a un escaneo completo de tabla en dos casos especiales, tanto con el tokenizador splitByNonAlpha como con el array:
- El needle contiene la letra
k.iliketrataU+212A KELVIN SIGNcomo unak, pero el escaneo del Diccionario compara bytes y pasaría por alto una fila escrita de ese modo.U+212Aes el único carácter queilikeconvierte en una letra o dígito ASCII, por lo quekes el único carácter del needle afectado. - La preprocessor expression del índice contiene
lowerUTF8oupperUTF8. Estas reescriben los caracteres no ASCII como letras ASCII (ßcomoSS,ſcomoS), lo que haría que el índice informara de filas queilikeno hace coincidir.
Consultas de recuento simples
Una consulta que solo cuenta las filas que cumplen un predicado de búsqueda de textohasAnyTokens) o intersecarlos (hasAllTokens). Como un índice de texto cubre toda la parte, el recuento es exacto y se mantiene rápido incluso en partes muy grandes.
La optimización se aplica a un count() simple filtrado por un único predicado hasToken, hasAnyTokens o hasAllTokens (o un equivalente de Array/Map). Los predicados combinados con AND/OR/NOT, un filtro adicional (p. ej., ... AND id > 10), un predicado en modo pista como m['key'] = 'value', seleccionar cualquier elemento además del recuento o una búsqueda de frases o por LIKE/patrón hacen que la consulta lea filas en su lugar. Las partes sin un índice materializado se siguen contando correctamente mediante la lectura de sus filas.
Para confirmar que la optimización está activada, busque ReadFromTextIndexCount en el plan de consulta:
Almacenamiento en caché
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): 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, use_text_index_tokens_cache y use_text_index_postings_cache para deshabilitar la lectura y escritura de las cachés individuales por parte de las consultas. El almacenamiento en caché de tokens que no están presentes en una parte de datos está habilitado de forma predeterminada y se puede controlar de forma independiente conuse_text_index_negative_tokens_cache.
Para limpiar las cachés, use la sentencia SYSTEM CLEAR TEXT INDEX CACHES
Consulte las siguientes configuraciones del servidor para ajustar las cachés.
Configuración de la caché de tokens
Configuración de la caché de encabezados
Configuración de la caché de listas de postings
Limitaciones
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).
Notas de actualización
La versión del formato en disco de los índices de texto se controla mediante la configuración de tablatext_index_serialization_version (valor predeterminado: v2_with_positions).
La configuración es una preferencia, no una restricción estricta: si la versión configurada no puede representar un índice, se elige automáticamente una versión más reciente que pueda hacerlo, por lo que la escritura de un índice de texto nunca falla debido a esta configuración.
Durante una actualización progresiva, fije el formato mediante la configuración compatibility en los servidores que ya se hayan actualizado: cuando se establece en una versión anterior a la que introdujo el formato correspondiente, text_index_serialization_version vuelve automáticamente a un valor anterior y los servidores más recientes siguen escribiendo en un formato que los servidores más antiguos aún pueden leer.
El codec de la lista de postings no se rige por esta versión: una parte escrita con posting_list_codec = 'pfor' (o con la configuración text_index_posting_list_codec) no puede ser leída por servidores anteriores a ese codec, y ni text_index_serialization_version ni compatibility impiden su uso, porque un codec indicado para el índice tiene precedencia sobre la preferencia de versión, del mismo modo que ocurre con support_phrase_search. No habilite pfor hasta que se hayan actualizado todos los servidores que puedan leer la tabla.
Índices de texto frente a índices basados en filtros Bloom
Los predicados sobre cadenas pueden acelerarse mediante índices de texto e índices basados en filtros Bloom (tipos de índicebloom_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í un ejemplo).
- Son bastante compactos (unos pocos kilobytes o megabytes por parte).
- 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).
- 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.
- Proporcionan tokenización y preprocesamiento
- Ofrecen soporte eficiente para
hasAllTokens,LIKE,matchy funciones similares de búsqueda de texto. - Tienen una escalabilidad significativamente mayor para grandes corpus de texto.
Detalles de implementación
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.
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) 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.
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 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 puede usarse para inspeccionar índices de texto.
Ejemplo: conjunto de datos de Hacker News
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:hackernews:
ALTER TABLE para añadir un índice de texto en la columna comment y luego materializarlo:
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.
1. Uso de hasToken
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.
2. Uso de hasAnyTokens
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)
3. Uso de hasAllTokens
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.
4. Búsqueda compuesta: OR, AND, NOT, …
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)hasAnyTokens(comment, ['ClickHouse', 'clickhouse']) sería la sintaxis recomendada y más eficiente.
Contenido relacionado
- Blog: Anuncio de la disponibilidad general de la búsqueda de texto completo de ClickHouse
- Blog: Cómo crear una búsqueda de texto completo de alto rendimiento para almacenamiento de objetos
- Video: Introducción a la búsqueda de texto completo en ClickHouse
- Video: Entre bastidores: la búsqueda de texto completo a la escala y velocidad de ClickHouse
- Presentation: Por dentro de la búsqueda de texto completo en ClickHouse: rápida, nativa y columnar
- Presentation: Índices invertidos de bases de datos: el porqué, el qué y el cómo, FOSDEM 2026