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

> Быстро находите нужные слова и фразы в тексте.

# Полнотекстовый поиск с текстовыми индексами

Текстовые индексы (также известные как [инвертированные индексы](https://en.wikipedia.org/wiki/Inverted_index)) обеспечивают быстрый полнотекстовый поиск по текстовым данным.
Текстовый индекс хранит сопоставление токенов с номерами строк, в которых встречается каждый токен.
Токены создаются в процессе, называемом токенизацией.
Например, стандартный токенизатор ClickHouse преобразует английское предложение "The cat likes mice." в токены \["The", "cat", "likes", "mice"].

В качестве примера рассмотрим таблицу с одним столбцом и тремя строками

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

Соответствующие токены:

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

Обычно мы предпочитаем регистронезависимый поиск, поэтому приводим токены к нижнему регистру:

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

Мы также удалим стоп-слова, такие как "I", "the" и "and", поскольку они встречаются почти в каждой строке:

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

Текстовый индекс в таком случае (концептуально) содержит следующую информацию:

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

По заданному поисковому токену эта структура индекса позволяет быстро находить все совпадающие строки.

<div id="creating-a-text-index">
  ## Создание текстового индекса
</div>

Текстовые индексы стали общедоступными (GA) в ClickHouse 26.2 и более новых версиях.
В этих версиях для использования текстового индекса не требуется настраивать какие-либо специальные параметры.
Мы настоятельно рекомендуем использовать ClickHouse версии >= 26.2 в продакшне.

<Note>
  Текстовые индексы можно использовать в любой версии ClickHouse >= 26.2 независимо от настройки [compatibility](/docs/ru/reference/settings/session-settings#compatibility).
</Note>

Чтобы создать текстовый индекс, используйте следующий синтаксис:

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

Текстовые индексы можно создавать для столбцов следующих типов:

* [String](/docs/ru/reference/data-types/string) и [FixedString](/docs/ru/reference/data-types/fixedstring),
* [Array(String)](/docs/ru/reference/data-types/array) и [Array(FixedString)](/docs/ru/reference/data-types/array),
* [Map](/docs/ru/reference/data-types/map) (через функции [mapKeys](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapKeys) и [mapValues](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapValues)),
* [JSON](/docs/ru/reference/data-types/newjson) (через функции [JSONAllPaths](/docs/ru/reference/functions/regular-functions/json-functions#JSONAllPaths) и [`JSONAllValues`](/docs/ru/reference/functions/regular-functions/json-functions#JSONAllValues)).

Также поддерживаются столбцы типа [Nullable(T)](/docs/ru/reference/data-types/nullable) и [LowCardinality()](/docs/ru/reference/data-types/lowcardinality), включая `Array(Nullable(String or FixedString))`.

Кроме того, текстовый индекс можно добавить в существующую таблицу:

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

```

Если вы добавляете индекс в существующую таблицу, мы рекомендуем материализовать его для существующих частей таблицы (иначе при поиске по частям без индекса будет использоваться медленное полное сканирование).

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

Чтобы удалить текстовый индекс, выполните

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

**Аргумент tokenizer (обязательный)**. Аргумент `tokenizer` задаёт используемый токенизатор:

* `splitByNonAlpha` разбивает строки по неалфавитно-цифровым ASCII-символам (см. функцию [splitByNonAlpha](/docs/ru/reference/functions/regular-functions/splitting-merging-functions#splitByNonAlpha)).
* `splitByString(S)` разбивает строки по заданным пользователем строкам-разделителям `S` (см. функцию [splitByString](/docs/ru/reference/functions/regular-functions/splitting-merging-functions#splitByString)).
  Разделители можно задать с помощью необязательного параметра, например, `tokenizer = splitByString([', ', '; ', '\n', '\\'])`.
  Обратите внимание, что каждая строка может состоять из нескольких символов (`', '` в примере).
  Список разделителей по умолчанию, если он не указан явно (например, `tokenizer = splitByString`), — это один пробел `[' ']`.
* `asciiCJK` разбивает строки на токены, используя правила границ слов Unicode (аналогично [Unicode Text Segmentation (UAX #29)](https://unicode.org/reports/tr29/)). ASCII-буквенно-цифровые символы и символы подчёркивания образуют токены с соединительными символами (ASCII `:` для букв, `.` и `'` для символов одного типа). Не-ASCII-символы Unicode, включая символы [CJK](https://en.wikipedia.org/wiki/CJK_characters), становятся односимвольными токенами.
* `ngrams(N)` разбивает строки на `N`-граммы одинаковой длины (см. функцию [ngrams](/docs/ru/reference/functions/regular-functions/splitting-merging-functions#ngrams)).
  Длину n-граммы можно задать с помощью необязательного целочисленного параметра от 1 до 8, например, `tokenizer = ngrams(3)`.
  Размер n-граммы по умолчанию, если он не указан явно (например, `tokenizer = ngrams`), равен 3.
* `sparseGrams(min_length, max_length, min_cutoff_length)` разбивает строки на n-граммы переменной длины, содержащие не менее `min_length` и не более `max_length` (включительно) символов (см. функцию [sparseGrams](/docs/ru/reference/functions/regular-functions/string-functions#sparseGrams)).
  Если не указано явно, значения `min_length` и `max_length` по умолчанию равны 3 и 100.
  Если передан параметр `min_cutoff_length`, возвращаются только n-граммы длиной не меньше `min_cutoff_length`.
  По сравнению с `ngrams(N)`, токенизатор `sparseGrams` создаёт N-граммы переменной длины, что позволяет более гибко представлять исходный текст.
  Например, `tokenizer = sparseGrams(3, 5, 4)` внутренне генерирует из входной строки 3-, 4- и 5-граммы, но возвращаются только 4- и 5-граммы.
* `array` не выполняет токенизацию, то есть каждое значение строки является токеном (см. функцию [array](/docs/ru/reference/functions/regular-functions/array-functions#array)).

Все доступные токенизаторы перечислены в [system.tokenizers](/docs/ru/reference/system-tables/tokenizers).

<Note>
  Токенизатор `splitByString` применяет разделители слева направо.
  Это может создавать неоднозначности.
  Например, строки-разделители `['%21', '%']` приведут к тому, что `%21abc` будет токенизировано как `['abc']`, тогда как при перестановке разделителей на `['%', '%21']` результатом будет `['21abc']`.
  В большинстве случаев лучше, чтобы при сопоставлении сначала выбирались более длинные разделители.
  Обычно этого можно добиться, передавая строки-разделители в порядке убывания длины.
  Если строки-разделители образуют [префиксный код](https://en.wikipedia.org/wiki/Prefix_code), их можно передавать в произвольном порядке.
</Note>

Чтобы понять, как токенизатор разбивает входную строку, можно использовать функции [tokens](/docs/ru/reference/functions/regular-functions/splitting-merging-functions#tokens) и [tokensForLikePattern](/docs/ru/reference/functions/regular-functions/splitting-merging-functions#tokensForLikePattern):

Пример:

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

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

*Работа с не-ASCII-входными данными.*
Текстовые индексы можно строить на основе текстовых данных на любом языке и в любой кодировке.
Для не-ASCII-текста рекомендуется токенизатор `asciiCJK`, поскольку он корректно определяет границы слов в Unicode, включая символы CJK.

<a id="preprocessor-argument-optional" />**Аргумент препроцессора (необязательно)**. Препроцессор — это выражение, которое применяется к входной строке перед токенизацией.

Типичные сценарии использования аргумента препроцессора:

1. Приведение к нижнему/верхнему регистру или свёртка регистра для регистронезависимого сопоставления, например [lower](/docs/ru/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/docs/ru/reference/functions/regular-functions/string-functions#lowerUTF8), [caseFoldUTF8](/docs/ru/reference/functions/regular-functions/string-functions#caseFoldUTF8).
2. Нормализация UTF-8, например [normalizeUTF8NFC](/docs/ru/reference/functions/regular-functions/string-functions#normalizeUTF8NFC), [normalizeUTF8NFD](/docs/ru/reference/functions/regular-functions/string-functions#normalizeUTF8NFD), [normalizeUTF8NFKC](/docs/ru/reference/functions/regular-functions/string-functions#normalizeUTF8NFKC), [normalizeUTF8NFKD](/docs/ru/reference/functions/regular-functions/string-functions#normalizeUTF8NFKD), [normalizeUTF8NFKCCasefold](/docs/ru/reference/functions/regular-functions/string-functions#normalizeUTF8NFKCCasefold), [toValidUTF8](/docs/ru/reference/functions/regular-functions/string-functions#toValidUTF8).
3. Удаление или преобразование нежелательных символов или подстрок, например диакритических знаков, с помощью [extractTextFromHTML](/docs/ru/reference/functions/regular-functions/string-functions#extractTextFromHTML), [substring](/docs/ru/reference/functions/regular-functions/string-functions#substring), [idnaEncode](/docs/ru/reference/functions/regular-functions/string-functions#idnaEncode), [translate](/docs/ru/reference/functions/regular-functions/string-replace-functions#translate), [removeDiacriticsUTF8](/docs/ru/reference/functions/regular-functions/string-functions#removeDiacriticsUTF8).

Выражение препроцессора должно преобразовывать входное значение типа [String](/docs/ru/reference/data-types/string) или [FixedString](/docs/ru/reference/data-types/fixedstring) в значение того же типа.
Если текстовый индекс был создан по столбцу типа `Nullable(T)` или `LowCardinality(T)`, то выражение препроцессора должно принимать nullable- или low-cardinality-значения (то есть не должно сгенерировать исключение).

Примеры:

* `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)))`
* Не допускается: `INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = concat(col, col))`

Использование недетерминированных функций не допускается.

<Note>
  Препроцессоры, по сути, эквивалентны оборачиванию индексируемого столбца или выражения выражением препроцессора.
  Например, препроцессор `lower` в `INDEX idx col TYPE text(tokenizer = 'splitByNonAlpha', preprocessor = lower(col))` можно эмулировать с помощью `INDEX idx lower(col) TYPE text(tokenizer = 'splitByNonAlpha')`.
  Недостаток второй формы в том, что эмулированный препроцессор применяется только в том случае, если он соответствует условию фильтрации в предложении WHERE.
  Например, `WHERE hasAllTokens(lower(col), [...])` соответствует, а `WHERE hasAllTokens(col, [...])` — нет.
  Поэтому для оптимального пользовательского опыта мы рекомендуем использовать выражения препроцессора.
</Note>

Функции [hasToken](/docs/ru/reference/functions/regular-functions/string-search-functions#hasToken), [hasAllTokens](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAllTokens), [hasAnyTokens](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAnyTokens) и [hasPhrase](/docs/ru/reference/functions/regular-functions/string-search-functions#hasPhrase) используют препроцессор, чтобы сначала преобразовать поисковый запрос перед его токенизацией.
Обратите внимание, что, поскольку препроцессор применяется только по пути текстового индекса, результаты этих функций могут различаться между запросами, использующими текстовый индекс, и запросами, которые его не используют (например, `SETTINGS use_skip_indexes = 0`).

Например,

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

эквивалентно:

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

В этом случае выражение препроцессора преобразует каждый элемент массива отдельно.

Пример:

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

Чтобы определить препроцессор в текстовом индексе, создаваемом для столбцов типа [Map](/docs/ru/reference/data-types/map), пользователям нужно решить, строится ли индекс
по ключам или по значениям map.

Пример:

```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" />**Аргумент постпроцессор (необязательно)**. Постпроцессор — это выражение, применяемое к каждому выходному токену после токенизации.

В отличие от препроцессора, который преобразует всю входную строку до того, как токенизатор разобьет ее на токены, постпроцессор работает с самими токенами — по одному за раз.
Это естественное место для преобразований, которые по своей сути выполняются на уровне токенов.

Типичные сценарии использования аргумента постпроцессор включают:

1. **Фильтрация стоп-слов (чрезвычайно частых токенов)**. Очень распространенные токены, такие как "the", "a" и "is", почти не влияют на релевантность поиска и раздувают индекс.
   Вы можете использовать постпроцессор, чтобы отбрасывать их, преобразуя в пустые токены — пустые токены игнорируются, то есть не добавляются в индекс.
   Пример: `if(str IN ('the', 'a', 'an', 'of', 'in', 'is', 'it'), '', str)`
2. **Удаление временных меток**. Строки Log часто начинаются со структурированной временной метки, например `2024-01-15T10:23:45`, или содержат ее.
   Индексация токенов временных меток раздувает индекс строками, не имеющими значения для релевантности поиска.
   Есть два взаимодополняющих способа игнорировать временные метки:
   * **Подход с постпроцессором**: используйте токенизатор `splitByString` (разбиение по пробельным символам), чтобы вся временная метка стала одним токеном, а затем используйте `parseDateTimeOrNull`, чтобы распознать и отбросить ее.
     Пример: `if(isNull(parseDateTimeOrNull(str, '%Y-%m-%dT%H:%i:%S')), str, '')`
     Для временных меток со смещением часового пояса или дробными секундами используйте `parseDateTimeBestEffortOrNull(str)` без явной строки формата.
   * **Подход с препроцессором**: удалите временную метку из полной строки лога *до* токенизации с помощью regular expression.
     Пример: `replaceRegexpAll(str, '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ', '')`
     Это работает с любым токенизатором и эффективнее, поскольку символы временной метки вообще не токенизируются.
     Оба подхода можно комбинировать: препроцессор удаляет временную метку, а постпроцессор нормализует или фильтрует оставшиеся токены (например, приводит к нижнему регистру и отбрасывает слова уровня серьезности, такие как `ERROR` или `INFO`).
3. **Стемминг**. Сопоставление каждого токена с его основой улучшает полноту поиска, позволяя находить морфологические варианты с общим корнем.
   Например, при английском стемминге "running", "runs" и "run" приводятся к основе "run", поэтому запрос по любому из этих вариантов найдет их все.
   ClickHouse предоставляет встроенную функцию [stem](/docs/ru/reference/functions/regular-functions/nlp-functions#stem) для нескольких языков.
   Пример: `stem(str, 'en')`
4. **Нормализация регистра**. Приведение токенов к нижнему или верхнему регистру для включения сопоставления, например [lower](/docs/ru/reference/functions/regular-functions/string-functions#lower), [lowerUTF8](/docs/ru/reference/functions/regular-functions/string-functions#lowerUTF8).
   Для приведения к нижнему или верхнему регистру мы рекомендуем использовать препроцессор вместо постпроцессора."

Выражение постпроцессора преобразует токены типа [String](/docs/ru/reference/data-types/string) в токены того же типа.
Кроме того, выражение постпроцессора должно ссылаться только на столбец или выражение, на основе которого определён текстовый индекс.
Если столбец имеет тип `Array(String)`, постпроцессор по-прежнему оперирует отдельными токенами как обычными значениями типа `String`.

Использование недетерминированных функций запрещено.

Постпроцессор применяется к каждому сгенерированному токену при построении индекса (для токенизатора `array` каждый элемент массива является токеном). Во время выполнения запроса поведение зависит от функции:

* Для `hasToken`, `hasAllTokens`, `hasAnyTokens` и `hasPhrase` (с любым поддерживаемым токенизатором): постпроцессор применяется и к токенам в haystack, и к поисковому needle, обеспечивая полностью нормализованное сопоставление (например, регистронезависимый поиск). Для `hasPhrase` токены после постобработки располагаются без промежутков, поэтому токен, который постпроцессор отбрасывает, не оставляет позиционного разрыва, и фраза всё равно сопоставляется через него — например, при постпроцессоре стоп-слов, отбрасывающем `the`, `hasPhrase(col, 'see cat')` соответствует документу `see the cat`.
* Для всех остальных функций (`=`, `IN`, `has`, `hasAny`, `hasAll`, `mapContains*`): для поиска с использованием индексной подсказки постобработке подвергается только needle; предикат на уровне строки по-прежнему сравнивается с исходными значениями столбца.

Примеры:

* Удаление стоп-слов с помощью выражения постпроцессора:

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

* Удалите временные метки с помощью выражения постпроцессора:

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

* Удалите временные метки с помощью выражения препроцессора:

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

* Удалите временные метки с помощью общего выражения для препроцессора и постпроцессора:

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

* Примените стемминг к токенам с помощью выражения постпроцессора:

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

**Поддержка функций**.

Для предикатов, использающих текстовый индекс, препроцессор и постпроцессор применяются к искомому значению перед проверкой на уровне гранулы, чтобы при обращении к индексу использовались те же токены, которые были сохранены при его построении.
Для большинства функций (`=`, `IN`, `startsWith`, `endsWith`, `LIKE`, `mapContains*`) текстовый индекс используется только для пропуска нерелевантных блоков данных; ClickHouse по-прежнему проверяет каждую оставшуюся строку по исходному предикату на исходных данных столбца.
Для функций поиска по токенам (`hasToken`, `hasAllTokens`, `hasAnyTokens`) текстовый индекс является основным способом вычисления: ClickHouse нормализует искомое значение с помощью того же препроцессора, токенизатора и постпроцессора, которые применялись при построении индекса, и использует эту нормализованную форму как для индексированных, так и для неиндексированных частей таблицы. Если задан постпроцессор, токены в haystack также нормализуются во время выполнения запроса (для любого токенизатора, а не только `array`), поэтому обе стороны сравнения преобразуются единообразно, и результат не зависит от того, читается ли индекс напрямую (настройка `query_plan_direct_read_from_text_index`) или у конкретной части есть материализованный индекс — например, это позволяет включить регистронезависимое сопоставление для `hasAllTokens(col, ['FOO'])` с постпроцессором `lower`.
Без `support_phrase_search` функция `hasPhrase` использует индекс только как подсказку и проверяет каждую оставшуюся строку по исходному предикату; постпроцессор дополнительно одинаково нормализует и фразу, и токены в haystack, поэтому результат не зависит от способа чтения, а токены, которые постпроцессор отбрасывает, не нарушают смежность фразы. При `support_phrase_search = 1` функция `hasPhrase` использует точное прямое чтение (при этом постпроцессор, если он задан, всё равно применяется).
Поисковые токены, которые постпроцессор преобразует в пустую строку, игнорируются, то есть считаются отсутствующими в поисковой фразе.

| Функция                                                                                                    | Поддерживает препроцессор                                    | Совместимые токенизаторы                                 | Поддерживает постпроцессор |
| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------- | -------------------------- |
| `=`                                                                                                        | да                                                           | все                                                      | да                         |
| `IN`                                                                                                       | да                                                           | все                                                      | да                         |
| [hasToken](/docs/ru/reference/functions/regular-functions/string-search-functions#hasToken)                     | да                                                           | все (в первую очередь для `splitByNonAlpha`)             | да                         |
| [hasAnyTokens(col, str)](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | да                                                           | все                                                      | да                         |
| [hasAllTokens(col, str)](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | да                                                           | все                                                      | да                         |
| [hasAnyTokens(col, arr)](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAnyTokens)   | нет (элементы массива используются как токены без изменений) | все                                                      | да                         |
| [hasAllTokens(col, arr)](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAllTokens)   | нет (элементы массива используются как токены без изменений) | все                                                      | да                         |
| [hasPhrase](/docs/ru/reference/functions/regular-functions/string-search-functions#hasPhrase)                   | да                                                           | `splitByNonAlpha`, `splitByString`, `ngrams`, `asciiCJK` | да                         |
| [startsWith](/docs/ru/reference/functions/regular-functions/string-functions#startsWith)                        | да                                                           | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | да                         |
| [endsWith](/docs/ru/reference/functions/regular-functions/string-functions#endsWith)                            | да                                                           | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | да                         |
| [like](/docs/ru/reference/functions/regular-functions/string-search-functions#like)                             | да¹                                                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹  | да¹                        |
| [match](/docs/ru/reference/functions/regular-functions/string-search-functions#match)                           | да¹                                                          | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`¹  | да¹                        |
| [ilike](/docs/ru/reference/functions/regular-functions/string-search-functions#like)                            | да² (`lower`/`upper` only)                                   | `splitByNonAlpha`, `array`²                              | нет²                       |
| [mapContainsKey](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsKey)             | да                                                           | все                                                      | да                         |
| [mapContainsValue](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsValue)         | да                                                           | все                                                      | да                         |
| [mapContainsKeyLike](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike)     | да                                                           | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | да                         |
| [mapContainsValueLike](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) | да                                                           | `splitByNonAlpha`, `ngrams`, `sparseGrams`, `asciiCJK`   | да                         |
| [has](/docs/ru/reference/functions/regular-functions/array-functions#has)                                       | да                                                           | `array`                                                  | да                         |
| [hasAny](/docs/ru/reference/functions/regular-functions/array-functions#hasAny)                                 | да                                                           | `array`                                                  | да                         |
| [hasAll](/docs/ru/reference/functions/regular-functions/array-functions#hasAll)                                 | да                                                           | `array`                                                  | да                         |

¹ `LIKE` и `match` используют прямое чтение как подсказку для указанных токенизаторов; в остальных случаях используется сканирование полным перебором.
`LIKE` также поддерживает *прямое чтение (без подсказки)* (включается через `use_text_index_like_evaluation_by_dictionary_scan`) для токенизаторов `splitByNonAlpha` и `array` без препроцессора и постпроцессора.

² `ILIKE` поддерживается только через прямое чтение (без подсказки) (`use_text_index_like_evaluation_by_dictionary_scan = 1`, токенизатор `splitByNonAlpha` или `array`).
Перехода к использованию индекса как подсказки нет: если настройка отключена или токенизатор не входит в поддерживаемый набор, индекс для `ILIKE` не используется.
Препроцессор, если он задан, должен быть `lower` или `upper`; постпроцессоры не поддерживаются.

**Экспериментально: аргумент для поддержки поиска по фразе (необязательный)**.

Экспериментальный параметр `support_phrase_search` (по умолчанию: `0`) определяет, хранит ли индекс позиции токенов.
Если указать значение `1`, индекс дополнительно сохраняет позиционные данные (в файле `.pos`), что позволяет выполнять точный поиск по фразе с помощью прямого чтения для функции [`hasPhrase`](#functions-example-hasphrase).
Хранение позиций увеличивает размер индекса на диске и стоимость записи, поэтому эта возможность включается только явно.
Формат хранения на диске пока не является стабильным, поэтому этот параметр экспериментальный и может измениться в одном из будущих релизов.
Поэтому для создания индекса с `support_phrase_search = 1` требуется, чтобы была включена настройка MergeTree [`allow_experimental_text_index_phrase_search`](/docs/ru/reference/settings/merge-tree-settings#allow_experimental_text_index_phrase_search).
Установите `support_phrase_search = 0` (значение по умолчанию), чтобы сохранить хранение только списков вхождений; текстовые индексы, созданные без этого аргумента, остаются без позиций.

<Warning>
  Этот аргумент является экспериментальным и должен использоваться только для тестирования.
  Чтобы включить хранение позиций, установите настройку MergeTree [`allow_experimental_text_index_phrase_search`](/docs/ru/reference/settings/merge-tree-settings#allow_experimental_text_index_phrase_search).
</Warning>

<details markdown="1">
  <summary>Необязательные расширенные параметры</summary>

  Значения по умолчанию для следующих расширенных параметров подходят практически для всех случаев.
  Мы не рекомендуем их изменять.

  Необязательный параметр `dictionary_block_size` (по умолчанию: 512) задает размер блоков словаря в строках.

  Необязательный параметр `dictionary_block_frontcoding_compression` (по умолчанию: 1) указывает, используют ли блоки словаря фронт-кодирование в качестве сжатия.

  Необязательный параметр `posting_list_block_size` (по умолчанию: 1048576) задает размер блоков списка вхождений в строках.

  Необязательный параметр `posting_list_codec` (по умолчанию: `none`) задает кодек для списка вхождений:

  * `none` — списки вхождений хранятся без дополнительного сжатия.
  * `bitpacking` — применяет [дифференциальное (delta) кодирование](https://en.wikipedia.org/wiki/Delta_encoding), а затем [упаковку битов](https://dev.to/madhav_baby_giraffe/bit-packing-the-secret-to-optimizing-data-storage-and-transmission-m70) (в обоих случаях в блоках фиксированного размера). Замедляет запросы SELECT, поэтому сейчас не рекомендуется.

  Указанные выше расширенные параметры также можно задавать на уровне таблицы с помощью соответствующих настроек MergeTree: [`text_index_dictionary_block_size`](/docs/ru/reference/settings/merge-tree-settings#text_index_dictionary_block_size), [`text_index_dictionary_block_frontcoding_compression`](/docs/ru/reference/settings/merge-tree-settings#text_index_dictionary_block_frontcoding_compression), [`text_index_posting_list_block_size`](/docs/ru/reference/settings/merge-tree-settings#text_index_posting_list_block_size) и [`text_index_posting_list_codec`](/docs/ru/reference/settings/merge-tree-settings#text_index_posting_list_codec).
  Они применяются ко всем текстовым индексам таблицы, в которых параметр не задан явно.

  Основной сценарий использования настроек уровня таблицы — изменение параметров индекса в существующей таблице без удаления и повторного создания текстового индекса для всех частей таблицы.
  Изменение настройки уровня таблицы применяет новые параметры только к текстовым индексам, создаваемым для новых частей; существующие части сохраняют свою текущую структуру.

  Аргумент, указанный в определении индекса, имеет приоритет над настройкой таблицы, например:

  ```sql theme={null}
  CREATE TABLE table(
      s String,
      -- Этот индекс использует 'bitpacking', переопределяя заданное ниже значение по умолчанию на уровне таблицы:
      INDEX idx_a s TYPE text(tokenizer = 'splitByNonAlpha', posting_list_codec = 'bitpacking'),
      -- Этот индекс наследует 'none' из настройки таблицы:
      INDEX idx_b lower(s) TYPE text(tokenizer = 'splitByNonAlpha'))
  ENGINE = MergeTree()
  ORDER BY tuple()
  SETTINGS text_index_posting_list_codec = 'none';
  ```
</details>

*Гранулярность индекса.*
Текстовые индексы реализованы в ClickHouse как тип [индексов пропуска данных](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree#skip-index-types).
Однако, в отличие от других индексов пропуска данных, текстовые индексы используют бесконечную гранулярность (100 миллионов).
Это видно в определении таблицы для текстового индекса.

Пример:

```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                                      │
└────────────────────────────────────────────────────────────────────────┘
```

Очень большая гранулярность индекса гарантирует, что текстовый индекс создаётся для всей части.
Явно заданная гранулярность индекса игнорируется.

<div id="using-a-text-index">
  ## Использование текстового индекса
</div>

Использовать текстовый индекс в запросах SELECT просто: распространённые функции поиска по строкам автоматически задействуют индекс.
Если на столбце или части таблицы нет индекса, функции поиска по строкам будут выполнять медленное полное сканирование.

<Note>
  Мы рекомендуем использовать функции `hasAnyTokens` и `hasAllTokens` для поиска по текстовому индексу; см. [ниже](#functions-example-hasanytokens-hasalltokens).
  Эти функции работают со всеми доступными токенизаторами и всеми возможными выражениями препроцессора и постпроцессора.
  Поскольку остальные поддерживаемые функции исторически появились раньше текстового индекса, во многих случаях им пришлось сохранить прежнее поведение (например, без поддержки препроцессора или постпроцессора).
</Note>

<div id="functions-support">
  ### Поддерживаемые функции
</div>

Текстовый индекс можно использовать при применении текстовых функций в секции `WHERE` или секциях `PREWHERE`:

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

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

`=` ([equals](/docs/ru/reference/functions/regular-functions/comparison-functions#equals)) соответствует заданному поисковому запросу целиком.

Пример:

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

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

`IN` ([in](/docs/ru/reference/functions/regular-functions/in-functions)) похожа на `equals`, но выполняет поиск по всем терминам запроса.

Пример:

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

<Note>
  Текстовый индекс не поддерживает `NOT IN` (`notIn`).
</Note>

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

<Note>
  В настоящее время эти функции используют текстовый индекс для фильтрации, только если в качестве токенизатора индекса используется `splitByNonAlpha`, `ngrams` или `sparseGrams`.
</Note>

<Note>
  `NOT LIKE` (`notLike`) не поддерживается текстовым индексом.
</Note>

Чтобы использовать `LIKE` ([like](/docs/ru/reference/functions/regular-functions/string-search-functions#like)) и функцию [match](/docs/ru/reference/functions/regular-functions/string-search-functions#match) с текстовыми индексами, ClickHouse должен уметь извлекать полные токены из поискового выражения.
Для индекса с токенизатором `ngrams` это возможно, если длина искомых строк между подстановочными шаблонами равна длине n-граммы или больше неё.

Пример для текстового индекса с токенизатором `splitByNonAlpha`:

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

`support` в примере может соответствовать `support`, `supports`, `supporting` и т. д.
Такой запрос является поиском по подстроке, и его нельзя ускорить с помощью текстового индекса.

Чтобы использовать текстовый индекс для запросов LIKE, шаблон LIKE нужно переписать следующим образом:

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

Пробелы слева и справа от `support` гарантируют, что этот термин можно извлечь как токен.

К счастью, есть особый случай, когда ClickHouse может использовать инвертированный индекс, чтобы значительно ускорить LIKE-запросы.

Подробнее см. в разделе [Настройка производительности для LIKE/ILIKE](#like-ilike-queries-perf).

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

[multiSearchAny](/docs/ru/reference/functions/regular-functions/string-search-functions#multiSearchAny) и его вариант для UTF-8 [multiSearchAnyUTF8](/docs/ru/reference/functions/regular-functions/string-search-functions#multiSearchAnyUTF8) проверяют, встречается ли в строке хотя бы одна из нескольких буквальных подстрок, а [multiMatchAny](/docs/ru/reference/functions/regular-functions/string-search-functions#multiMatchAny) проверяет, соответствует ли строка хотя бы одному из нескольких регулярных выражений.
Эти функции используют текстовый индекс при тех же условиях, что и `LIKE` и `match` (см. выше): ClickHouse должен иметь возможность извлечь полные токены из каждой искомой подстроки, а список искомых подстрок должен быть константным.
Гранула считывается, если в ней может присутствовать хотя бы одна искомая подстрока.

Для `multiMatchAny`, если отдельный шаблон нельзя свести к требованию наличия токена (например, `.*`, который соответствует любому документу), текстовый индекс использовать нельзя, и запрос переходит к полному сканированию.

Как и в случае с `LIKE` и `match`, поиск по подстрокам и регулярным выражениям лучше всего работает с токенизаторами `ngrams` и `sparseGrams`.
Эти токенизаторы индексируют перекрывающиеся символьные n-граммы, поэтому искомая подстрока раскладывается на n-граммы, которые присутствуют в индексе везде, где она встречается как подстрока, независимо от того, начинается она или заканчивается в середине слова.
Поэтому искомую подстроку можно использовать как есть, если её длина не меньше размера n-граммы.

Пример текстового индекса с токенизатором `ngrams`:

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

Токенизатор `splitByNonAlpha`, напротив, индексирует только полные токены (целые слова).
Поскольку искомая подстрока может начинаться или заканчиваться в середине слова, ClickHouse отбрасывает первый и последний токены каждой искомой подстроки, поэтому индекс может отсеивать гранулы, используя только полные токены.
Чтобы при поиске по подстроке и регулярному выражению использовался индекс с `splitByNonAlpha`, окружайте каждую искомую подстроку символами-разделителями (например, пробелами), чтобы она образовывала один или несколько полных токенов.

Пример текстового индекса с токенизатором `splitByNonAlpha`:

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

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

Как и `LIKE`, функции [startsWith](/docs/ru/reference/functions/regular-functions/string-functions#startsWith) и [endsWith](/docs/ru/reference/functions/regular-functions/string-functions#endsWith) могут использовать текстовый индекс, только если из поискового выражения можно извлечь полные токены.
Для индекса с токенизатором `ngrams` это возможно, если длина искомых строк между подстановочными шаблонами равна длине n-граммы или больше неё.
Если текстовый индекс использует постпроцессор, эти функции всё равно могут использовать индекс в режиме Hint, если извлечённые hint-токены остаются непустыми после нормализации. Если нормализация удаляет все hint-токены, индекс не используется для этого предиката.

Пример для текстового индекса с токенизатором `splitByNonAlpha`:

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

В этом примере токеном считается только `clickhouse`.
`support` не считается токеном, поскольку может соответствовать `support`, `supports`, `supporting` и т. д.

Чтобы найти все строки, начинающиеся с `clickhouse supports`, добавьте в конец шаблона поиска пробел:

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

Аналогично, `endsWith` следует использовать с пробелом в начале:

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

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

<Note>
  Функция `hasToken` на первый взгляд кажется простой в использовании, но при использовании для lookup-операций в текстовых индексах с токенизаторами, отличными от `splitByNonAlpha`, и/или выражениями препроцессора/постпроцессора у неё есть определённые подводные камни.
  Вместо неё рекомендуем использовать `hasAnyTokens` и `hasAllTokens`.

  Регистронезависимые варианты `hasTokenCaseInsensitive` и `hasTokenCaseInsensitiveOrNull` не учитывают текстовый индекс — они всегда выполняются как полное сканирование строк, даже для столбцов с текстовым индексом. Для регистронезависимого сопоставления используйте препроцессор или постпроцессор `lower(...)` и комбинируйте его с `hasToken` / `hasAllTokens` / `hasAnyTokens`.
</Note>

Функция [hasToken](/docs/ru/reference/functions/regular-functions/string-search-functions#hasToken) выполняет сопоставление с одним указанным токеном.

В отличие от ранее упомянутых функций, они не токенизируют поисковый запрос (предполагается, что входное значение — это один токен).

Пример:

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

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

Функции [hasAnyTokens](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAnyTokens) и [hasAllTokens](/docs/ru/reference/functions/regular-functions/string-search-functions#hasAllTokens) проверяют наличие любого или всех указанных токенов.

Эти две функции принимают поисковые токены либо в виде строки, которая будет разбита на токены с помощью того же токенизатора, что и для индексного столбца, либо в виде массива уже обработанных токенов, к которым перед поиском токенизация применяться не будет.
Дополнительные сведения см. в документации по функциям.

Пример:

```sql theme={null}
-- Токены поиска переданы как строковый аргумент
SELECT count() FROM table WHERE hasAnyTokens(comment, 'clickhouse olap');
SELECT count() FROM table WHERE hasAllTokens(comment, 'clickhouse olap');

-- Токены поиска переданы как 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>

Функция [hasPhrase](/docs/ru/reference/functions/regular-functions/string-search-functions#hasPhrase) выполняет поиск по фразе: все токены должны идти подряд и в том же порядке, что и в поисковой строке.

В отличие от `hasAllTokens`, для которой достаточно, чтобы все токены присутствовали где угодно, `hasPhrase` требует, чтобы они образовывали непрерывную последовательность.
Поисковая фраза токенизируется с помощью того же токенизатора, который настроен для столбца индекса.
Если текстовый индекс использует постпроцессор, поисковая фраза также нормализуется перед поиском по индексу.
Обратите внимание, что для функции требуется один из токенизаторов `splitByNonAlpha`, `splitByString`, `ngrams` или `asciiCJK`.

Пример:

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

Функция Array [has](/docs/ru/reference/functions/regular-functions/array-functions#has) проверяет наличие одного токена в массиве строк.

Пример:

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

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

Функции для работы с массивами [hasAny](/docs/ru/reference/functions/regular-functions/array-functions#hasAny) и [hasAll](/docs/ru/reference/functions/regular-functions/array-functions#hasAll) проверяют, содержит ли индексируемый столбец типа Array хотя бы одну или все строки из постоянного набора.

Пример:

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

Функция [mapContains](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsKey) (алиас `mapContainsKey`) сопоставляет токены, извлечённые из искомой строки, с ключами map.
Поведение аналогично функции `equals` для столбца `String`.
Текстовый индекс используется только в том случае, если он был создан для выражения `mapKeys(map)`.

Пример:

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

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

Функция [mapContainsValue](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsValue) сопоставляет токены, извлечённые из искомой строки, со значениями map.
Поведение аналогично функции `equals` для столбца `String`.
Текстовый индекс используется только в том случае, если он был создан для выражения `mapValues(map)`.

Пример:

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

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

Функции [mapContainsKeyLike](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsKeyLike) и [mapContainsValueLike](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapContainsValueLike) сопоставляют шаблон регулярного выражения со всеми ключами или значениями (соответственно) типа Map.

Пример:

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

Оператор доступа [operator\[\]](/docs/ru/reference/operators/index#access-operators) можно использовать с текстовым индексом для фильтрации по ключам и значениям. Текстовый индекс используется только в том случае, если он создан для выражений `mapKeys(map)` или `mapValues(map)`, либо для обоих.

Пример:

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

См. примеры ниже, чтобы узнать, как использовать столбцы типа `Array(T)` и `Map(K, V)` с текстовым индексом.

<div id="text-index-example-array">
  ### Индексация столбцов Array(String)
</div>

Представьте платформу для ведения блога, где авторы помечают свои записи ключевыми словами.
Важно, чтобы пользователи могли находить связанный контент, выполняя поиск по топикам или нажимая на них.

Рассмотрим следующее определение таблицы:

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

Без текстового индекса для поиска постов по определённому ключевому слову (например, `clickhouse`) требуется сканировать все записи:

```sql theme={null}
SELECT count() FROM posts WHERE has(keywords, 'clickhouse'); -- медленное полное сканирование таблицы — проверяет каждое ключевое слово в каждом посте
```

По мере роста платформы это становится всё медленнее, поскольку запросу приходится проверять каждый массив keywords в каждой строке.
Чтобы решить эту проблему с производительностью, мы определяем текстовый индекс для столбца `keywords`:

```sql theme={null}
ALTER TABLE posts ADD INDEX keywords_idx(keywords) TYPE text(tokenizer = splitByNonAlpha);
ALTER TABLE posts MATERIALIZE INDEX keywords_idx; -- Не забудьте перестроить индекс для существующих данных
```

<div id="text-index-example-map">
  ### Индексация столбцов типа Map
</div>

Во многих сценариях использования обсервабилити сообщения логов разбиваются на "компоненты" и сохраняются в подходящих типах данных, например: дата-время для временной метки, enum для уровня логирования и т. д.
Поля метрик лучше всего хранить как пары ключ-значение.
Командам эксплуатации необходимо эффективно искать в журналах для отладки, расследования инцидентов безопасности и мониторинга.

Рассмотрим эту таблицу журналов:

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

Без текстового индекса поиск по данным типа [Map](/docs/ru/reference/data-types/map) требует полного сканирования всей таблицы:

```sql theme={null}
-- Находит все журналы с данными об ограничении частоты запросов:
SELECT * FROM logs WHERE has(mapKeys(attributes), 'rate_limit'); -- медленное полное сканирование таблицы

-- Находит все журналы с определённого IP-адреса:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- медленное полное сканирование таблицы
```

По мере роста объёма логов эти запросы начинают выполняться медленнее.

Решение — создать текстовый индекс для ключей и значений [Map](/docs/ru/reference/data-types/map).
Используйте [mapKeys](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapKeys), чтобы создать текстовый индекс, если вам нужно находить логи по именам полей или типам атрибутов:

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

Используйте [mapValues](/docs/ru/reference/functions/regular-functions/tuple-map-functions#mapValues), чтобы создать текстовый индекс, если вам нужно выполнять поиск по фактическому содержимому атрибутов:

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

Примеры запросов:

```sql theme={null}
-- Найти все запросы с ограничением частоты:
SELECT * FROM logs WHERE mapContainsKey(attributes, 'rate_limit'); -- fast

-- Найти все записи журнала с определённого IP-адреса:
SELECT * FROM logs WHERE has(mapValues(attributes), '192.168.1.1'); -- fast

-- Найти все записи журнала, в которых любой атрибут содержит ошибку:
SELECT * FROM logs WHERE mapContainsValueLike(attributes, '% error %'); -- fast
```

<div id="text-index-example-json">
  ### Индексация JSON-столбцов
</div>

Текстовые индексы можно использовать с `JSON`-столбцами тремя способами:

1. **Индексы для конкретных подстолбцов** — создайте текстовый индекс для известного JSON-пути, как и для обычного столбца. При этом индексируются *значения* по этому пути.
2. **Индексы на основе путей с [JSONAllPaths](/docs/ru/reference/functions/regular-functions/json-functions#JSONAllPaths)** — индексируют *все пути*, присутствующие в каждой грануле, чтобы пропускать гранулы, в которых не может быть запрашиваемого пути. Как и в случае со столбцами `Map`.
3. **Индексы на основе значений с [JSONAllValues](/docs/ru/reference/functions/regular-functions/json-functions#JSONAllValues)** — индексируют *все значения* по всем JSON-путям, чтобы ускорить полнотекстовый поиск по любому подстолбцу JSON с помощью одного индекса.

<div id="json-indexes-on-subcolumns">
  #### Индексы для определённых подстолбцов
</div>

Вы можете создать индекс пропуска данных для любого подстолбца JSON, используя тот же синтаксис, что и для обычных столбцов.

Есть два способа обратиться к подстолбцу JSON в выражении индекса:

* **Типизированный путь**, объявленный в подсказке типа JSON, — прямой доступ по имени: `json.a`.
* **Динамический путь** с явным приведением типа — используйте синтаксис приведения `::`: `json.b::String`.

Пример определения индекса:

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

Пример запроса:

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

Пример запроса:

```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">
  #### Индексы по путям с JSONAllPaths
</div>

Как и в случае со столбцами `Map`, текстовые индексы можно создавать для столбцов [JSON](/docs/ru/reference/data-types/newjson) с помощью [`JSONAllPaths`](/docs/ru/reference/functions/regular-functions/json-functions#JSONAllPaths).
Индекс хранит набор JSON-путей, присутствующих в каждой грануле, и использует их, чтобы пропускать гранулы, в которых запрашиваемый путь отсутствует.

Пример определения индекса:

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

Вы можете использовать `EXPLAIN indexes = 1`, чтобы убедиться, что индекс пропуска данных действительно используется.
Если путь существует только в одной части, индекс пропускает другую часть.

Пример:

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

Если путь отсутствует во всех частях, все части и гранулы пропускаются.

Пример:

```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` также использует индекс — он пропускает гранулы, в которых путь отсутствует (поскольку в таком случае значение было бы `NULL`):

Пример:

```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">
  #### Индексы по значениям с JSONAllValues
</div>

Текстовый индекс можно использовать для ускорения поиска по [JSON](/docs/ru/reference/data-types/newjson) столбцам с помощью функции [`JSONAllValues`](/docs/ru/reference/functions/regular-functions/json-functions#JSONAllValues).

`JSONAllValues` возвращает все значения из JSON-столбца в виде `Array(String)`.
Значения нестроковых типов данных (например, целые числа и массивы) преобразуются в текстовое представление.
Текстовый индекс, построенный с использованием `JSONAllValues`, индексирует эти текстовые представления по всем JSON-путям в каждой строке.
Затем этот индекс может ускорять запросы с фильтрацией по отдельным подстолбцам JSON.
Когда запрос фильтрует по конкретному подстолбцу (например, `data.user_name = 'alice'`), текстовый индекс может быстро пропускать строки (и гранулы), в которых токены поиска отсутствуют во всех JSON-значениях.

<Note>
  Индекс может давать ложноположительные срабатывания, если одинаковые токены встречаются в разных JSON-путях.
  Например, если строка 1 содержит `{"a": "hello", "b": "world"}`, а запрос ищет `data.a = 'world'`, текстовый индекс не сможет определить, что `world` относится к пути `b`, а не `a`.
  В таких случаях индекс не будет пропускать строку, а окончательную проверку выполнит фильтр по фактическим данным столбца.
  Это то же поведение, что и в других сценариях использования текстового индекса, где он выступает в роли быстрого предварительного фильтра.
</Note>

<div id="json-all-values-creating-the-index">
  ##### Создание индекса
</div>

Пример определения индекса:

```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">
  ##### Поддерживаемые шаблоны запросов
</div>

После создания индекс может ускорять выполнение запросов по подстолбцам JSON с использованием тех же функций, что и для столбцов `String`, а также функции `equals` для всех столбцов.

Доступ к подстолбцам:

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

Доступ к подстолбцу с явным `CAST`:

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

Оператор `IN`:

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

<div id="text-index-phrase-search">
  ### Фразовый поиск
</div>

Например, обычный поиск по текстовому индексу

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

соответствует всем строкам, содержащим заданные токены в произвольном порядке.
В примере строка `While she stayed in Tokyo, the weather was great.` соответствует условию фильтрации.

Напротив, фразовый поиск подразумевает совпадение токенов в заданном порядке.
Например,

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

соответствует любой строке, содержащей последовательность токенов `weather in Tokyo`, например `How is the weather in Tokyo?`?

Текстовый индекс ускоряет поиск по фразам, пересекая списки вхождений для всех токенов во фразе, чтобы определить гранулы-кандидаты.
Затем в пределах этих гранул ClickHouse проверяет точное соседство токенов.
Этот процесс относительно затратен и медленнее обычных запросов текстового поиска.
Чтобы ускорить запросы поиска по фразам, включите сохранение позиций в текстовом индексе (см. `Optional parameters` выше).

`hasPhrase` можно использовать вместе с токенизаторами `splitByNonAlpha`, `splitByString`, `ngrams` и `asciiCJK`.
Указанная строка фразы токенизируется токенизатором индекса.
Символы-разделители во фразе игнорируются: `hasPhrase(text, 'quick+brown')` эквивалентно `hasPhrase(text, 'quick brown')`, если в качестве токенизатора используется `splitByNonAlpha`.

<div id="text-index-phrase-search-example">
  #### Пример
</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 │
   └────┴─────────────────────┘
```

Строка 2 (`'New weather in York'`) не подходит, потому что токены расположены в неправильном порядке.
Строка 3 (`'weather in New Orleans'`) не подходит, потому что не содержит токен `'York'`.

<div id="performance-tuning">
  ## Настройка производительности
</div>

<div id="direct-read">
  ### Прямое чтение
</div>

Некоторые типы текстовых запросов можно значительно ускорить благодаря оптимизации, называемой "прямое чтение".

Пример:

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

Оптимизация прямого чтения выполняет запрос исключительно с использованием текстового индекса (то есть обращений к текстовому индексу), без доступа к исходному текстовому столбцу.
При обращениях к текстовому индексу считывается сравнительно небольшой объем данных, поэтому они работают значительно быстрее, чем обычные индексы пропуска данных в ClickHouse (которые сначала выполняют обращение к индексу пропуска данных, а затем загружают и фильтруют оставшиеся гранулы).

Прямое чтение управляется двумя настройками:

* Настройка [query\_plan\_direct\_read\_from\_text\_index](/docs/ru/reference/settings/session-settings#query_plan_direct_read_from_text_index) (по умолчанию true), которая определяет, включено ли прямое чтение в целом.
* Настройка [use\_skip\_indexes\_on\_data\_read](/docs/ru/reference/settings/session-settings#use_skip_indexes_on_data_read) была обязательным предварительным условием для прямого чтения в версиях ClickHouse \< 26.4.

**Поддерживаемые функции**

Оптимизация прямого чтения поддерживает функции `hasToken`, `hasAllTokens` и `hasAnyTokens`.
Если для текстового индекса задан токенизатор `array`, прямое чтение также поддерживается для функций `equals`, `has`, `hasAny`, `hasAll`, `mapContainsKey` и `mapContainsValue`.
Эти функции также можно комбинировать с помощью операторов `AND`, `OR` и `NOT`.
Секции `WHERE` или `PREWHERE` также могут содержать дополнительные фильтры, не связанные с функциями текстового поиска (для текстовых или других столбцов) — в этом случае оптимизация прямого чтения все равно будет использоваться, но менее эффективно (она применяется только к поддерживаемым функциям текстового поиска).

Чтобы понять, использует ли запрос прямое чтение, выполните запрос с `EXPLAIN PLAN actions = 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 = 0, -- отключить прямое чтение
```

возвращает

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

в то время как тот же запрос, выполненный с параметром `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, -- включить прямое чтение
```

возвращает

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

Второй вывод EXPLAIN PLAN содержит виртуальный столбец `__text_index_<index_name>_<function_name>_<id>`.
Если этот столбец присутствует, значит используется прямое чтение.

Если условие WHERE содержит только функции текстового поиска, запрос может вовсе не читать данные столбца и получить максимальный прирост производительности за счет прямого чтения.
Однако даже если к текстовому столбцу обращаются в других частях запроса, прямое чтение все равно даст прирост производительности.

**Прямое чтение в качестве подсказки**

Прямое чтение в качестве подсказки основано на тех же принципах, что и обычное прямое чтение, но дополнительно добавляет фильтр, построенный на основе данных текстового индекса, не исключая при этом исходный текстовый столбец.
Оно используется для функций, для которых чтение только из текстового индекса приводило бы к ложноположительным срабатываниям.

Поддерживаются следующие функции: `like`, `startsWith`, `endsWith`, `equals`, `has`, `hasPhrase`, `mapContainsKey` и `mapContainsValue`.

Дополнительный фильтр может повысить селективность и в сочетании с другими фильтрами сильнее ограничить результирующий набор, помогая сократить объем данных, считываемых из других столбцов.

Прямое чтение в качестве подсказки управляется настройкой [query\_plan\_text\_index\_add\_hint](/docs/ru/reference/settings/session-settings#query_plan_text_index_add_hint) (включена по умолчанию).

Пример запроса без подсказки:

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

возвращает

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

в то время как тот же запрос, выполненный при `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
```

возвращает

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

Во втором выводе EXPLAIN PLAN видно, что в условие фильтрации добавлен дополнительный конъюнкт (`__text_index_...`).
Благодаря оптимизации [PREWHERE](/docs/ru/reference/statements/select/prewhere) условие фильтрации разбивается на три отдельных конъюнкта, которые применяются в порядке возрастания вычислительной сложности.
Для этого запроса они применяются в следующем порядке: `__text_index_...`, затем `greaterOrEquals(...)` и, наконец, `like(...)`.
Такой порядок позволяет пропускать ещё больше гранул данных, чем только за счёт текстового индекса и исходного фильтра, ещё до чтения тяжёлых столбцов, используемых в запросе после условия `WHERE`, что дополнительно уменьшает объём считываемых данных.

<div id="like-ilike-queries-perf">
  ### Запросы LIKE/ILIKE
</div>

Если шаблон запроса LIKE/ILIKE имеет вид `%<буквенно-цифровые-символы-без-пробелов>%`, а токенизатор текстового индекса — `splitByNonAlpha` или `array`, ClickHouse использует инвертированный индекс, чтобы существенно ускорить запросы LIKE/ILIKE. Для этого ClickHouse сканирует словарь инвертированного индекса вместо полного сканирования таблицы, чтобы найти совпадения с шаблоном.

Когда эта оптимизация включена, запросы LIKE/ILIKE должны выполняться значительно быстрее, чем при полном сканировании таблицы. Однако если шаблон соответствует большинству токенов в словаре, производительность может быть хуже, чем при полном сканировании таблицы. К счастью, существует fallback-механизм, который помогает этого избежать.

Эта оптимизация управляется настройкой:

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

Fallback-механизм управляется двумя настройками:

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

Эта оптимизация поддерживает только функции `like` и `ilike`.

<div id="caching">
  ### Кэширование
</div>

Существуют различные кэши уровня сервера, позволяющие хранить части текстового индекса в памяти (см. раздел [Подробности реализации](#implementation)):
В настоящее время доступны кэши для десериализованных заголовков, токенов и списков вхождений текстового индекса, чтобы сократить I/O.
Используйте настройки [use\_text\_index\_header\_cache](/docs/ru/reference/settings/session-settings#use_text_index_header_cache), [use\_text\_index\_tokens\_cache](/docs/ru/reference/settings/session-settings#use_text_index_tokens_cache) и [use\_text\_index\_postings\_cache](/docs/ru/reference/settings/session-settings#use_text_index_postings_cache), чтобы отключить для запросов чтение из отдельных кэшей и запись в них.

Чтобы очистить кэши, используйте оператор [SYSTEM CLEAR TEXT INDEX CACHES](/docs/ru/reference/statements/system#drop-text-index-caches)

Чтобы настроить кэши, обратитесь к следующим настройкам сервера.

<div id="caching-tokens">
  #### Настройки кэша токенов
</div>

| Параметр                                                                                                                        | Описание                                                                                      |
| ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [text\_index\_tokens\_cache\_policy](/docs/ru/reference/settings/server-settings/settings#text_index_tokens_cache_policy)            | Имя политики кэша токенов текстового индекса.                                                 |
| [text\_index\_tokens\_cache\_size](/docs/ru/reference/settings/server-settings/settings#text_index_tokens_cache_size)                | Максимальный размер кэша в байтах.                                                            |
| [text\_index\_tokens\_cache\_max\_entries](/docs/ru/reference/settings/server-settings/settings#text_index_tokens_cache_max_entries) | Максимальное количество десериализованных токенов в кэше.                                     |
| [text\_index\_tokens\_cache\_size\_ratio](/docs/ru/reference/settings/server-settings/settings#text_index_tokens_cache_size_ratio)   | Размер защищённой очереди в кэше токенов текстового индекса относительно общего размера кэша. |

<div id="caching-header">
  #### Настройки кэша заголовков
</div>

| Setting                                                                                                                         | Description                                                                                      |
| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [text\_index\_header\_cache\_policy](/docs/ru/reference/settings/server-settings/settings#text_index_header_cache_policy)            | Имя политики кэша заголовков текстового индекса.                                                 |
| [text\_index\_header\_cache\_size](/docs/ru/reference/settings/server-settings/settings#text_index_header_cache_size)                | Максимальный размер кэша в байтах.                                                               |
| [text\_index\_header\_cache\_max\_entries](/docs/ru/reference/settings/server-settings/settings#text_index_header_cache_max_entries) | Максимальное количество десериализованных заголовков в кэше.                                     |
| [text\_index\_header\_cache\_size\_ratio](/docs/ru/reference/settings/server-settings/settings#text_index_header_cache_size_ratio)   | Размер защищённой очереди в кэше заголовков текстового индекса относительно общего размера кэша. |

<div id="caching-posting-lists">
  #### Настройки кэша списков вхождений
</div>

| Настройка                                                                                                                           | Описание                                                                                                |
| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| [text\_index\_postings\_cache\_policy](/docs/ru/reference/settings/server-settings/settings#text_index_postings_cache_policy)            | Имя политики кэша списков вхождений текстового индекса.                                                 |
| [text\_index\_postings\_cache\_size](/docs/ru/reference/settings/server-settings/settings#text_index_postings_cache_size)                | Максимальный размер кэша в байтах.                                                                      |
| [text\_index\_postings\_cache\_max\_entries](/docs/ru/reference/settings/server-settings/settings#text_index_postings_cache_max_entries) | Максимальное количество десериализованных списков вхождений в кэше.                                     |
| [text\_index\_postings\_cache\_size\_ratio](/docs/ru/reference/settings/server-settings/settings#text_index_postings_cache_size_ratio)   | Размер защищённой очереди в кэше списков вхождений текстового индекса относительно общего размера кэша. |

<div id="limitations">
  ## Ограничения
</div>

У текстового индекса на данный момент есть следующие ограничения:

* Материализация текстовых индексов с большим количеством токенов (например, 10 миллиардов токенов) может потреблять значительный объём памяти. Материализация текстового
  индекса может происходить напрямую (`ALTER TABLE <table> MATERIALIZE INDEX <index>`) или косвенно во время слияния частей.
* Невозможно материализовать текстовые индексы для частей, содержащих более 4.294.967.296 (= 2^32 = около 4,2 миллиарда) строк. Без материализованного текстового индекса запросы переходят к медленному полному перебору внутри части. Для оценки наихудшего случая предположим, что часть содержит один столбец типа String и настройка MergeTree `max_bytes_to_merge_at_max_space_in_pool` (по умолчанию: 150 GB) не изменялась. В этом случае такая ситуация возникает, если в столбце в среднем содержится менее 29,5 символа на строку. На практике таблицы также содержат другие столбцы, и этот порог в несколько раз ниже (в зависимости от количества, типа и размера других столбцов).

<div id="text-index-vs-bloom-filter-indexes">
  ## Текстовые индексы и индексы на основе фильтра Блума
</div>

Предикаты над `String` можно ускорить с помощью текстовых индексов и индексов на основе фильтра Блума (типы индексов `bloom_filter`, `ngrambf_v1`, `tokenbf_v1`, `sparse_grams`), однако по устройству и предполагаемым сценариям использования они принципиально различаются:

**Индексы на основе фильтра Блума**

* Основаны на вероятностных структурах данных, которые могут давать ложноположительные срабатывания.
* Способны отвечать только на вопросы о принадлежности множеству, то есть столбец может содержать токен X или точно не содержать X.
* Хранят информацию на уровне гранул, что позволяет пропускать крупные диапазоны при выполнении запроса.
* Их сложно правильно настроить (пример см. [здесь](/docs/ru/reference/engines/table-engines/mergetree-family/mergetree#n-gram-bloom-filter)).
* Они довольно компактны (от нескольких килобайт до нескольких мегабайт на часть).

**Текстовые индексы**

* Строят детерминированный инвертированный индекс по токенам. Сам индекс не может давать ложноположительных срабатываний.
* Специально оптимизированы для полнотекстового поиска.
* Хранят информацию на уровне строк, что обеспечивает эффективный поиск по терминам.
* Они довольно велики (от десятков до сотен мегабайт на часть).

Индексы на основе фильтра Блума поддерживают полнотекстовый поиск лишь как «побочный эффект»:

* Они не поддерживают расширенную токенизацию и предобработку.
* Они не поддерживают поиск по нескольким токенам.
* Они не обеспечивают характеристик производительности, ожидаемых от инвертированного индекса.

Текстовые индексы, напротив, изначально предназначены для полнотекстового поиска:

* Они обеспечивают токенизацию и предобработку
* Они эффективно поддерживают `hasAllTokens`, `LIKE`, `match` и аналогичные функции текстового поиска.
* Они значительно лучше масштабируются на больших текстовых корпусах.

<div id="implementation">
  ## Подробности реализации
</div>

Каждый текстовый индекс состоит из двух (абстрактных) структур данных:

* словаря, который сопоставляет каждому токену список вхождений, и
* набора списков вхождений, каждый из которых представляет собой множество номеров строк.

Текстовый индекс строится для всей части.
В отличие от других индексов пропуска данных, текстовый индекс можно слить при слиянии частей данных вместо того, чтобы перестраивать его заново (см. ниже).

При создании индекса для каждой части создаются три файла:

**Файл блоков словаря (.dct)**

Токены в текстовом индексе сортируются и сохраняются в блоках словаря по 512 токенов в каждом (размер блока настраивается параметром `dictionary_block_size`).
Файл блоков словаря (.dct) содержит все блоки словаря для всех гранул индекса в части.

**Файл заголовка индекса (.idx)**

Файл заголовка индекса содержит для каждого блока словаря первый токен блока и его относительное смещение в файле блоков словаря.

Эта структура разреженного индекса похожа на [разреженный индекс первичного ключа ClickHouse](/docs/ru/guides/clickhouse/data-modelling/sparse-primary-indexes)).

**Файл списков вхождений (.pst)**

Списки вхождений для всех токенов располагаются последовательно в файле списков вхождений.
Чтобы экономить место и при этом обеспечивать быстрые операции пересечения и объединения, списки вхождений хранятся в виде [roaring bitmaps](https://roaringbitmap.org/).
Если список вхождений больше `posting_list_block_size`, он разбивается на несколько блоков, которые последовательно сохраняются в файле списков вхождений.

**Файл позиций (.pos)**

Необязательный, только если аргумент индекса `support_phrase_search = 1`.
Хранит позиции токенов в совпадающих строках.

**Слияние текстовых индексов**

При слиянии частей данных текстовый индекс не нужно перестраивать с нуля; вместо этого его можно эффективно слить на отдельном этапе процесса слияния.
На этом этапе сортированные словари текстовых индексов каждой входной части считываются и объединяются в новый общий словарь.
Номера строк в списках вхождений также пересчитываются, чтобы отразить их новые позиции в слитой части данных, с использованием сопоставления старых и новых номеров строк, которое создаётся на начальном этапе слияния.
Этот способ слияния текстовых индексов аналогичен тому, как сливаются [проекции](/docs/ru/reference/statements/alter/projection#projection-indexes) со столбцом `_part_offset`.
Если индекс не материализован в исходной части, он строится, записывается во временный файл, а затем сливается вместе с индексами из других частей и из других временных файлов индекса.

**Отладка**

Для анализа текстовых индексов можно использовать табличную функцию [mergeTreeTextIndex](/docs/ru/reference/functions/table-functions/mergeTreeTextIndex).

<div id="hacker-news-dataset">
  ## Пример: датасет Hacker News
</div>

Давайте посмотрим, какой прирост производительности дают текстовые индексы на большом датасете с большим объёмом текста.
Мы будем использовать 28,7 млн строк комментариев с популярного сайта Hacker News.
Вот таблица без текстового индекса:

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

28,7 млн строк находятся в файле Parquet в S3 — давайте выполним их вставку в таблицу `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');
```

Мы воспользуемся `ALTER TABLE`, добавим текстовый индекс для столбца comment, а затем материализуем его:

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

Теперь выполним запросы с помощью функций `hasToken`, `hasAnyTokens` и `hasAllTokens`.
Следующие примеры наглядно покажут существенную разницу в производительности между стандартным сканированием индекса и оптимизацией прямого чтения.

<div id="using-hasToken">
  ### 1. Использование `hasToken`
</div>

`hasToken` проверяет, содержит ли текст один конкретный токен.
Мы будем искать токен 'ClickHouse' с учётом регистра.

**Прямое чтение отключено (стандартное сканирование)**
По умолчанию ClickHouse использует индекс пропуска данных для фильтрации гранул, а затем читает данные столбца для этих гранул.
Мы можем смоделировать это поведение, отключив прямое чтение.

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

**Прямое чтение включено (быстрое чтение по индексу)**
Теперь выполним тот же запрос с включенным прямым чтением (по умолчанию).

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

Запрос с прямым чтением более чем в 45 раз быстрее (0.362s против 0.008s) и обрабатывает значительно меньше данных (9.51 GB против 3.15 MB), поскольку выполняет чтение только по индексу.

<div id="using-hasAnyTokens">
  ### 2. Использование `hasAnyTokens`
</div>

`hasAnyTokens` проверяет, содержит ли текст хотя бы один из указанных токенов.
Мы будем искать комментарии, содержащие 'love' или 'ClickHouse'.

**Прямое чтение отключено (стандартное сканирование)**

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

**Прямое чтение включено (быстрое чтение по индексу)**

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

Ускорение для этого распространённого поиска с оператором "OR" ещё более впечатляющее.
Запрос выполняется почти в 89 раз быстрее (1.329s против 0.015s), поскольку удаётся избежать полного сканирования столбца.

<div id="using-hasAllTokens">
  ### 3. Использование `hasAllTokens`
</div>

`hasAllTokens` проверяет, содержит ли текст все заданные токены.
Мы будем искать комментарии, содержащие и 'love', и 'ClickHouse'.

**Прямое чтение отключено (стандартное сканирование)**
Даже при отключённом прямом чтении стандартный индекс пропуска данных всё равно остаётся эффективным.
Он сокращает выборку с 28.7M строк до всего 147.46K строк, но при этом всё равно требуется прочитать 57.03 MB из столбца.

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

**Прямое чтение включено (Быстрое чтение по индексу)**
Прямое чтение выполняет запрос по данным индекса, считывая всего 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
```

Для этого поиска по "AND" оптимизация прямого чтения работает более чем в 26 раз быстрее (0.184s против 0.007s), чем стандартное сканирование с использованием индекса пропуска данных.

<div id="compound-search">
  ### 4. Составной поиск: OR, AND, NOT, ...
</div>

Оптимизация прямого чтения также применяется к составным булевым выражениям.
Здесь мы выполним регистронезависимый поиск по 'ClickHouse' OR 'clickhouse'.

**Прямое чтение отключено (стандартное сканирование)**

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

**Включено прямое чтение (быстрое чтение по индексу)**

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

За счёт объединения результатов из индекса запрос с прямым чтением выполняется в 34 раза быстрее (0.450s против 0.013s) и позволяет избежать чтения 9.58 GB данных из столбца.
Для этого конкретного случая предпочтительным и более эффективным вариантом будет синтаксис `hasAnyTokens(comment, ['ClickHouse', 'clickhouse'])`.

<div id="related-content">
  ## Связанные материалы
</div>

* Блог: [Объявляем о выходе полнотекстового поиска ClickHouse в General Availability](https://clickhouse.com/blog/full-text-search-ga-release)
* Блог: [Создание высокопроизводительного полнотекстового поиска для Объектного хранилища](https://clickhouse.com/blog/clickhouse-full-text-search-object-storage)
* Видео: [Введение в полнотекстовый поиск в ClickHouse](https://www.youtube.com/watch?v=9zPmf1a_heU)
* Видео: [Что внутри: полнотекстовый поиск в ClickHouse при его масштабе и скорости](https://www.youtube.com/watch?v=8JbqE_ubfkU)
* Презентация: [Полнотекстовый поиск в ClickHouse изнутри: быстрый, нативный и столбцовый](https://github.com/ClickHouse/clickhouse-presentations/blob/master/2025-tumuchdata-munich/ClickHouse_%20full-text%20search%20-%2011.11.2025%20Munich%20Database%20Meetup.pdf)
* Презентация: [Инвертированные индексы баз данных: зачем, что и как, FOSDEM 2026](https://presentations.clickhouse.com/2026-fosdem-inverted-index/Inverted_indexes_the_what_the_why_the_how.pdf)

**Устаревшие материалы**

* Блог: [Представляем инвертированные индексы в ClickHouse](https://clickhouse.com/blog/clickhouse-search-with-inverted-indices)
* Блог: [Полнотекстовый поиск в ClickHouse изнутри: быстрый, нативный и столбцовый](https://clickhouse.com/blog/clickhouse-full-text-search)
* Видео: [Полнотекстовые индексы: проектирование и эксперименты](https://www.youtube.com/watch?v=O_MnyUkrIq8)
