Создание текстового индекса
tokenizer задаёт токенизатор:
splitByNonAlphaразбивает строки по неалфавитно-цифровым ASCII-символам (см. также функцию splitByNonAlpha).splitByString(S)разбивает строки по заданным пользователем строкам-разделителямS(см. также функцию splitByString). Разделители можно указать с помощью необязательного параметра, напримерtokenizer = splitByString([', ', '; ', '\n', '\\']). Обратите внимание, что каждый разделитель может состоять из нескольких символов (', 'в примере). Список разделителей по умолчанию, если он не указан явно (например,tokenizer = splitByString), — это один пробел[' '].ngrams(N)разбивает строки наN-граммы одинаковой длины (см. также функцию ngrams). Длину n-граммы можно указать с помощью необязательного целочисленного параметра от 2 до 8, напримерtokenizer = ngrams(3). Длина n-граммы по умолчанию, если она не указана явно (например,tokenizer = ngrams), равна 3.arrayне выполняет токенизацию, то есть каждое значение строки является токеном (см. также функцию array).sparseGrams(min_length, max_length, min_cutoff_length)— использует тот же алгоритм, что и функция sparseGrams, чтобы разбить строку на все n-граммы длиныmin_lengthи несколько n-грамм большей длины вплоть доmax_lengthвключительно. Если указанmin_cutoff_length, в индекс сохраняются только N-граммы длиной не меньшеmin_cutoff_length. В отличие отngrams(N), который генерирует только N-граммы фиксированной длины,sparseGramsсоздаёт набор N-грамм переменной длины в указанном диапазоне, что позволяет более гибко представлять текстовый контекст. Например,tokenizer = sparseGrams(3, 5, 4)сгенерирует из входной строки 3-, 4- и 5-граммы и сохранит в индексе только 4- и 5-граммы.
Токенизатор
splitByString применяет разделители слева направо.
Это может приводить к неоднозначностям.
Например, строки-разделители ['%21', '%'] приведут к тому, что %21abc будет токенизировано как ['abc'], тогда как при перестановке разделителей на ['%', '%21'] результатом будет ['21abc'].
В большинстве случаев желательно, чтобы при сопоставлении сначала выбирались более длинные разделители.
Обычно этого можно добиться, передавая строки-разделители в порядке убывания длины.
Если строки-разделители образуют префиксный код, их можно передавать в произвольном порядке.preprocessor — это выражение, которое преобразует входную строку перед токенизацией.
Типичные сценарии использования аргумента preprocessor:
- Приведение входных строк к нижнему (или верхнему) регистру для регистронезависимого сопоставления, например lower, lowerUTF8, см. первый пример ниже.
- Нормализация UTF-8, например normalizeUTF8NFC, normalizeUTF8NFD, normalizeUTF8NFKC, normalizeUTF8NFKD, toValidUTF8.
- Удаление или преобразование нежелательных символов или подстрок, например extractTextFromHTML, substring, idnaEncode.
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))
Необязательные расширенные параметры
Необязательные расширенные параметры
Значения по умолчанию для следующих расширенных параметров хорошо подходят практически для всех ситуаций.
Мы не рекомендуем их изменять.Необязательный параметр
dictionary_block_size (по умолчанию: 128) задаёт размер блоков словаря в строках.Необязательный параметр dictionary_block_frontcoding_compression (по умолчанию: 1) указывает, используют ли блоки словаря фронт-кодирование для сжатия.Необязательный параметр max_cardinality_for_embedded_postings (по умолчанию: 16) задаёт порог мощности, ниже которого списки вхождений следует встраивать в блоки словаря.Необязательный параметр bloom_filter_false_positive_rate (по умолчанию: 0.1) задаёт уровень ложноположительных срабатываний фильтра Блума словаря.Использование текстового индекса
Поддерживаемые функции
WHERE запроса SELECT используются текстовые функции:
= and !=
= (equals) and != (notEquals ) совпадают со всем указанным поисковым выражением.
Пример:
= и !=, однако поиск по равенству и неравенству имеет смысл только с токенизатором array (при котором индекс сохраняет значения строки целиком).
IN и NOT IN
IN (in) и NOT IN (notIn) похожи на функции equals и notEquals, но позволяют проверить наличие всех (IN) или отсутствие всех (NOT IN) поисковых терминов.
Пример:
= и !=, то есть IN и NOT IN имеют смысл только при использовании токенизатора array.
LIKE, NOT LIKE и match
В настоящее время эти функции используют текстовый индекс для фильтрации только в том случае, если в качестве токенизатора индекса используется
splitByNonAlpha или ngrams.LIKE like, NOT LIKE (notLike) и функцию match с текстовыми индексами, ClickHouse должен иметь возможность извлекать полные токены из поискового выражения.
Пример:
support в примере может соответствовать support, supports, supporting и т. д.
Такой запрос является запросом на поиск подстроки, и его нельзя ускорить с помощью текстового индекса.
Чтобы задействовать текстовый индекс для запросов LIKE, шаблон LIKE нужно переписать следующим образом:
support гарантируют, что этот термин можно выделить как токен.
startsWith и endsWith
LIKE, функции startsWith и endsWith могут использовать текстовый индекс только в том случае, если из поискового выражения можно выделить полные токены.
Пример:
clickhouse считается токеном.
support не считается токеном, потому что может соответствовать support, supports, supporting и т. д.
Чтобы найти все строки, начинающиеся с clickhouse supports, завершите шаблон поиска пробелом в конце:
endsWith следует использовать с пробелом в начале:
hasToken and hasTokenOrNull
hasToken и hasTokenOrNull обеспечивают максимальную производительность при использовании с индексом text.
hasAnyTokens and hasAllTokens
has
mapContains
mapContainsKey) проверяет наличие одного токена в ключах Map.
Пример:
operator[]
Array(T) и Map(K, V) с текстовым индексом.
Примеры поддержки Array и Map в текстовом индексе.
Индексация Array(String)
clickhouse) приходится сканировать все записи:
keywords, который формирует оптимизированную для поиска структуру, заранее обрабатывающую все ключевые слова и обеспечивающую мгновенный lookup:
Важно: после добавления текстового индекса его нужно перестроить для уже существующих данных:
Индексация Map
- Находит все записи журнала, связанные с ограничением частоты:
- Находит все записи журнала с определённого IP:
Важно: после добавления текстового индекса его нужно перестроить для уже существующих данных:
- Найдите все запросы, попавшие под ограничение частоты:
- Находит все записи журнала с определённого IP-адреса:
Реализация
Структура индекса
- словаря, который сопоставляет каждому токену список вхождений, и
- набора списков вхождений, каждый из которых представляет собой множество номеров строк.
dictionary_block_size).
Файл блоков словаря (.dct) содержит все блоки словаря для всех гранул индекса в части.
Файл гранул индекса (.idx)
Файл гранул индекса содержит для каждого блока словаря первый токен блока, его относительное смещение в файле блоков словаря и фильтр Блума для всех токенов в блоке.
Эта структура разреженного индекса похожа на разреженный индекс первичного ключа).
Фильтр Блума позволяет заранее пропускать блоки словаря, если искомый токен отсутствует в блоке.
Файл списков вхождений (.pst)
Списки вхождений для всех токенов располагаются последовательно в файле списков вхождений.
Чтобы экономить место и при этом обеспечивать быстрые операции пересечения и объединения, списки вхождений хранятся в виде битмапов Roaring.
Если мощность списка вхождений меньше 16 (настраивается параметром max_cardinality_for_embedded_postings), он встраивается в словарь.
Прямое чтение
- Настройка query_plan_direct_read_from_text_index (по умолчанию: 1), которая определяет, включено ли прямое чтение в целом.
- Настройка use_skip_indexes_on_data_read (по умолчанию: 1), которая является еще одним обязательным условием для прямого чтения. Обратите внимание: в базах данных ClickHouse с compatibility < 25.10
use_skip_indexes_on_data_readотключена, поэтому нужно либо повысить значение настройки compatibility, либо явно выполнитьSET use_skip_indexes_on_data_read = 1.
ALTER TABLE ... MATERIALIZE INDEX).
Поддерживаемые функции
Оптимизация прямого чтения поддерживает функции hasToken, hasAllTokens и hasAnyTokens.
Эти функции также можно комбинировать операторами AND, OR и NOT.
Предложение WHERE также может содержать дополнительные фильтры, не относящиеся к функциям текстового поиска (для текстовых столбцов или других столбцов) — в этом случае оптимизация прямого чтения все равно будет использоваться, но менее эффективно (она применяется только к поддерживаемым функциям текстового поиска).
Чтобы понять, использует ли запрос прямое чтение, выполните его с EXPLAIN PLAN actions = 1.
Например, запрос с отключенным прямым чтением
query_plan_direct_read_from_text_index = 1
__text_index_<index_name>_<function_name>_<id>.
Если этот столбец есть, значит используется прямое чтение.
Пример: набор данных Hacker News
hackernews:
ALTER TABLE, добавим текстовый индекс для столбца comment, а затем материализуем его:
hasToken, hasAnyTokens и hasAllTokens.
Следующие примеры наглядно покажут заметную разницу в производительности между стандартным сканированием индекса и оптимизацией прямого чтения.
1. Использование hasToken
hasToken проверяет, содержит ли текст определённый одиночный токен.
Мы будем искать токен ‘ClickHouse’ с учётом регистра.
Прямое чтение отключено (стандартное сканирование)
По умолчанию ClickHouse использует индекс пропуска данных для фильтрации гранул, а затем считывает данные столбца для этих гранул.
Мы можем смоделировать такое поведение, отключив прямое чтение.
2. Использование hasAnyTokens
hasAnyTokens проверяет, содержит ли текст хотя бы один из указанных токенов.
Мы будем искать комментарии, содержащие либо ‘love’, либо ‘ClickHouse’.
Прямое чтение отключено (стандартное сканирование)
3. Использование hasAllTokens
hasAllTokens проверяет, содержит ли текст все указанные токены.
Мы будем искать комментарии, содержащие и ‘love’, и ‘ClickHouse’.
Прямое чтение отключено (стандартное сканирование)
Даже при отключённом прямом чтении стандартный индекс пропуска данных всё равно остаётся эффективным.
Он сокращает выборку с 28.7M строк до всего 147.46K строк, но при этом всё равно должен прочитать 57.03 MB из столбца.
4. Составной поиск: OR, AND, NOT, …
hasAnyTokens(comment, ['ClickHouse', 'clickhouse']).