Полнотекстовый поиск с использованием полнотекстовых индексов
Полнотекстовые индексы являются экспериментальным типом вторичных индексов, которые обеспечивают быстрые возможности текстового поиска для колонок String или FixedString. Основная идея полнотекстового индекса заключается в хранении отображения от "терминов" к строкам, которые содержат эти термины. "Термины" - это токенизированные ячейки строковой колонки. Например, строковая ячейка "I will be a little late" по умолчанию токенизируется на шесть терминов: "I", "will", "be", "a", "little" и "late". Другой вид токенизатора - это n-граммы. Например, результат токенизации 3-граммами составит 21 термин: "I w", " wi", "wil", "ill", "ll ", "l b", " be" и т.д. Чем более мелко токенизированы входные строки, тем больше, но и более полезным будет полученный полнотекстовый индекс.
Полнотекстовые индексы являются экспериментальными и не должны использоваться в производственных средах. Они могут измениться в будущем несоответствующим образом, например, в отношении их синтаксиса DDL/DQL или характеристик производительности/сжатия.
Использование
Чтобы использовать полнотекстовые индексы, сначала включите их в конфигурации:
Полнотекстовый индекс можно определить на строковой колонке, используя следующий синтаксис:
где N
задает токенизатор:
gin(0)
(или короче:gin()
) устанавливает токенизатор на "tokens", т.е. разбивает строки по пробелам,gin(N)
сN
от 2 до 8 устанавливает токенизатор на "ngrams(N)"
Максимальное количество строк на один список публикаций можно указать как второй параметр. Этот параметр может использоваться для управления размерами списков публикаций, чтобы избежать генерации огромных файлов списков публикаций. Существуют следующие варианты:
gin(ngrams, max_rows_per_postings_list)
: Использовать данный max_rows_per_postings_list (при условии, что он не равен 0)gin(ngrams, 0)
: Нет ограничения на максимальное количество строк на список публикацийgin(ngrams)
: Использовать максимальное количество строк по умолчанию, равное 64K.
Будучи типом индекса пропуска, полнотекстовые индексы можно удалять или добавлять к колонке после создания таблицы:
Чтобы использовать индекс, не требуется специальных функций или синтаксиса. Типичные предикаты поиска строк автоматически используют индекс. В качестве примеров рассмотрим:
Полнотекстовый индекс также работает на колонках типа Array(String)
, Array(FixedString)
, Map(String)
и Map(String)
.
Как и для других вторичных индексов, каждая часть колонки имеет свой собственный полнотекстовый индекс. Кроме того, каждый полнотекстовый индекс внутренне делится на "сегменты". Существование и размер сегментов в целом прозрачны для пользователей, но размер сегмента определяет потребление памяти во время создания индекса (например, когда два компонента объединяются). Параметр конфигурации "max_digestion_size_per_segment" (по умолчанию: 256 МБ) контролирует количество данных, читаемых из основной колонки, прежде чем будет создан новый сегмент. Увеличение параметра повышает промежуточное потребление памяти для создания индекса, но также улучшает производительность поиска, поскольку в среднем требуется проверить меньше сегментов для оценки запроса.
Полнотекстовый поиск по набору данных Hacker News
Давайте рассмотрим улучшение производительности полнотекстовых индексов на большом наборе данных с большим количеством текста. Мы используем 28.7M строк комментариев на популярном сайте Hacker News. Вот таблица без полнотекстового индекса:
28.7M строк находятся в файле Parquet в S3 - давайте вставим их в таблицу hackernews
:
Рассмотрим следующий простой поиск термина ClickHouse
(и его различные варианты с заглавными и строчными буквами) в колонке comment
:
Обратите внимание, что выполнение запроса занимает 3 секунды:
Мы добавим полнотекстовый индекс на строчные буквы колонки comment
, затем материализуем его (это может занять некоторое время - подождите, пока он будет материализован):
Мы повторяем тот же запрос...
...и замечаем, что запрос выполняется в 4 раза быстрее:
Мы также можем искать один или все несколько терминов, т.е. дизъюнкции или конъюнкции:
В отличие от других вторичных индексов, полнотекстовые индексы (на данный момент) отображают номера строк (идентификаторы строк) вместо идентификаторов гранул. Причина этого дизайна заключается в производительности. На практике пользователи часто ищут несколько терминов одновременно. Например, предикат фильтрации WHERE s LIKE '%little%' OR s LIKE '%big%'
может быть оценен напрямую с использованием полнотекстового индекса, формируя объединение списков идентификаторов строк для терминов "little" и "big". Это также означает, что параметр GRANULARITY
, указанный при создании индекса, не имеет смысла (он может быть удален из синтаксиса в будущем).