Полнотекстовый поиск
Обзор
Для эффективного полнотекстового поиска по столбцам типа String или FixedString (также поддерживаются типы Array(String), Array(FixedString), Map(String) и Map(String)) в таблицах с очень большим количеством записей можно использовать инвертированные индексы — специальный тип индекса пропуска данных.
В ADQM/ClickHouse инвертированный индекс строится следующим образом:
-
Текст каждой ячейки в столбце, по которому составляется индекс, разбивается на токены — термы (terms). По умолчанию разбиение осуществляется по пробелам, но также возможно разбить текст на n-граммы. Все термы записываются в отсортированный список — словарь (dictionary).
-
Для каждого терма составляется отсортированный список идентификаторов строк, содержащих этот терм (posting list).
Иными словами, инвертированный индекс реализует хранение сопоставления всех термов с указателями на их расположение в таблице (номера соответствующих строк). Если в таблице для текстового столбца создан инвертированный индекс, ADQM автоматически его применяет при выполнении запроса с поиском терма в этом столбце (например, запрос типа SELECT … WHERE <column_name> == | IN | LIKE '<term>'
) — быстро находит терм в словаре и возвращает нужные строки таблицы, используя posting list.
Например, рассмотрим таблицу со столбцом text
типа String:
┌─row_id─┬─text────────────┐ │ 0 │ Lower East Side │ │ 1 │ Upper West Side │ │ 2 │ West Village │ │ 3 │ East Village │ └────────┴─────────────────┘
Инвертированный индекс по столбцу text
в общем виде будет составлен следующим образом.
Dictionary (terms) | Posting lists |
---|---|
East |
0, 3 |
Lower |
0 |
Side |
0, 1 |
Upper |
1 |
Village |
2, 3 |
West |
1, 2 |
ВАЖНО
В настоящее время инвертированные индексы поддерживаются в экспериментальном режиме — не рекомендуется их использовать в production-системах.
|
Управление индексами
Перед созданием инвертированного индекса необходимо включить следующую опцию:
SET allow_experimental_inverted_index = true;
Инвертированный индекс описывается так же как и другие типы индекса пропуска данных — с помощью выражения INDEX
в секции описания столбцов запроса CREATE TABLE
:
CREATE TABLE <table_name>
( <column_name> <column_type>, ...
INDEX <index_name> <index_expr> TYPE inverted([<n>], [<max_rows_per_posting_list>]), ...)
ENGINE = MergeTree
...;
где:
-
<index_name>
— имя индекса. -
<index_expr>
— выражение, по которому будет строиться индекс (часто это имя текстового столбца). -
<n>
— токенизатор (tokenizer), который определяет, как строка в ячейке текстового столбца разбивается на термы:-
inverted(0)
(сокращенноinverted()
илиinverted
) — строка разбивается на слова по пробелам; -
inverted(n)
(гдеn
— число в диапазоне[2, 8]
) — строка разбивается на n-граммы (подстроки изn
символов).
-
-
<max_rows_per_posting_list>
— максимальное количество идентификаторов строк, содержащих терм, которое может быть записано в список posting list (если0
— размер списка posting list для терма не ограничивается, если параметр не указан — устанавливается ограничение по умолчанию, равное64K
).
Так как инвертированные индексы сопоставляют термы с идентификаторами (номерами) строк, а не с идентификаторами гранул, при описании инвертированного индекса не указывается параметр GRANULARITY
, который возможно применять для других типов индекса пропуска данных.
В существующую таблицу инвертированный индекс можно добавить с помощью следующего запроса:
ALTER TABLE <table_name> ADD INDEX <index_name> <index_expr> TYPE inverted([<n>], [<max_rows_per_posting_list>]);
Когда индекс добавляется в существующую таблицу, он не обновляется автоматически. ALTER TABLE
изменяет метаданные, поэтому индекс будет рассчитываться только для новых данных, вставляемых в таблицу. Чтобы применить индекс к существующим данным, выполните дополнительную команду:
ALTER TABLE <table_name> MATERIALIZE INDEX <index_name>;
Удалить индексы можно запросами:
-
ALTER TABLE <table_name> DROP INDEX <index_name>
— удаляет описание индекса из метаданных таблицы и удаляет индексные файлы с диска; -
ALTER TABLE <table_name> CLEAR INDEX <index_name>
— удаляет файлы индекса с диска без удаления описания индекса из метаданных.
Пример
Создайте таблицу trips
с данными такси Нью-Йорка, как описано в примере документации ClickHouse:
CREATE TABLE trips (
trip_id UInt32,
pickup_datetime DateTime,
dropoff_datetime DateTime,
pickup_longitude Nullable(Float64),
pickup_latitude Nullable(Float64),
dropoff_longitude Nullable(Float64),
dropoff_latitude Nullable(Float64),
passenger_count UInt8,
trip_distance Float32,
fare_amount Float32,
extra Float32,
tip_amount Float32,
tolls_amount Float32,
total_amount Float32,
payment_type Enum('CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4, 'UNK' = 5),
pickup_ntaname LowCardinality(String),
dropoff_ntaname LowCardinality(String)
)
ENGINE = MergeTree
PRIMARY KEY (pickup_datetime, dropoff_datetime);
INSERT INTO trips
SELECT
trip_id,
pickup_datetime,
dropoff_datetime,
pickup_longitude,
pickup_latitude,
dropoff_longitude,
dropoff_latitude,
passenger_count,
trip_distance,
fare_amount,
extra,
tip_amount,
tolls_amount,
total_amount,
payment_type,
pickup_ntaname,
dropoff_ntaname
FROM s3(
'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_{0..2}.gz',
'TabSeparatedWithNames');
Выполните тестовый запрос на выборку данных с условием по столбцу pickup_ntaname
:
SELECT pickup_datetime,pickup_datetime,pickup_ntaname,dropoff_ntaname
FROM trips
WHERE (pickup_ntaname LIKE '%Village%') OR (pickup_ntaname LIKE '%Square%');
┌─────pickup_datetime─┬─────pickup_datetime─┬─pickup_ntaname─────────────────────────────┬─dropoff_ntaname───────┐ │ 2015-07-01 00:02:24 │ 2015-07-01 00:02:24 │ East Village │ Gramercy │ │ 2015-07-01 00:03:15 │ 2015-07-01 00:03:15 │ Hudson Yards-Chelsea-Flatiron-Union Square │ West Village │ ... │ 2015-07-09 00:32:13 │ 2015-07-09 00:32:13 │ West Village │ East Village │ └─────────────────────┴─────────────────────┴────────────────────────────────────────────┴───────────────────────┘ Showed first 10000. 714664 rows in set. Elapsed: 0.831 sec. Processed 3.00 million rows, 18.00 MB (3.61 million rows/s., 21.66 MB/s.) Peak memory usage: 13.04 MiB.
Добавьте в таблицу инвертированный индекс:
SET allow_experimental_inverted_index = true;
ALTER TABLE trips ADD INDEX inv_idx(pickup_ntaname) TYPE inverted;
ALTER TABLE trips MATERIALIZE INDEX inv_idx;
Повторите запрос — обратите внимание, что скорость его выполнения увеличилась:
SELECT pickup_datetime,pickup_datetime,pickup_ntaname,dropoff_ntaname
FROM trips
WHERE (pickup_ntaname LIKE '%Village%') OR (pickup_ntaname LIKE '%Square%');
┌─────pickup_datetime─┬─────pickup_datetime─┬─pickup_ntaname─────────────────────────────┬─dropoff_ntaname───────┐ │ 2015-07-01 00:02:24 │ 2015-07-01 00:02:24 │ East Village │ Gramercy │ │ 2015-07-01 00:03:15 │ 2015-07-01 00:03:15 │ Hudson Yards-Chelsea-Flatiron-Union Square │ West Village │ ... │ 2015-07-09 00:32:13 │ 2015-07-09 00:32:13 │ West Village │ East Village │ └─────────────────────┴─────────────────────┴────────────────────────────────────────────┴───────────────────────┘ Showed first 10000. 714664 rows in set. Elapsed: 0.129 sec. Processed 3.00 million rows, 18.00 MB (23.27 million rows/s., 139.63 MB/s.) Peak memory usage: 13.04 MiB.
С помощью выражения EXPLAIN с параметром indexes
можно убедиться, что при выполнении запроса использовался инвертированный индекс:
EXPLAIN indexes = 1 SELECT pickup_datetime,pickup_datetime,pickup_ntaname,dropoff_ntaname
FROM trips
WHERE (pickup_ntaname LIKE '%Village%') OR (pickup_ntaname LIKE '%Square%');
┌─explain─────────────────────────────────────┐ │ Expression ((Project names + Projection)) │ │ Expression │ │ ReadFromMergeTree (default.trips) │ │ Indexes: │ │ PrimaryKey │ │ Condition: true │ │ Parts: 2/2 │ │ Granules: 368/368 │ │ Skip │ │ Name: inv_idx │ │ Description: inverted GRANULARITY 1 │ │ Parts: 2/2 │ │ Granules: 368/368 │ └─────────────────────────────────────────────┘