# Как искать длинные хеши и ID с dict='keywords_32k'

Практическое руководство по поиску длинных хешей, event ID, message ID и email в Manticore Search: лимиты, точное сравнение, wildcard-поиск, токенизация, миграция и ограничения.

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

В логах и технических данных всё иначе. SHA-256 занимает 64 символа, а идентификаторы сообщений, correlation ID, идентификаторы событий и некоторые email-адреса могут быть ещё длиннее. При этом такое значение часто имеет смысл только целиком: если потерять его хвост, один ID легко спутать с другим.

Для таких случаев в Manticore Search появился `dict='keywords_32k'`.

> `keywords_32k` доступен начиная с Manticore Search 27.1.1. Для перевода существующей таблицы с `keywords` рекомендуется использовать версию 27.1.5 или новее.

## В чём проблема обычного словаря

По умолчанию Manticore использует `dict='keywords'`. Максимальная длина токена при этом составляет **42 байта после нормализации**.

Важно, что речь идёт именно о байтах, а не о символах. Для ASCII один символ занимает один байт, но в UTF-8 один символ может занимать несколько байт.

Если токен длиннее 42 байт, Manticore обрезает его:

* при индексации документа;
* при обработке поискового запроса.

Из-за этого запрос по полному значению не обязательно вернёт ноль результатов. Поскольку запрос тоже обрезается, документ может быть найден — но только по первым 42 байтам.

Проблема серьёзнее, чем может показаться: два разных ID с одинаковыми первыми 42 байтами становятся неразличимыми для полнотекстового поиска. Кроме того, найти значение по части, расположенной после 42-го байта, уже не получится.

## Что меняет `keywords_32k`

`dict='keywords_32k'` увеличивает максимальную длину нормализованного токена до **32768 байт**, то есть до 32 КБ.

| Поведение                              | `dict='keywords'` |          `dict='keywords_32k'` |
| -------------------------------------- | ----------------: | -----------------------------: |
| Максимальная длина токена              |          42 байта |                     32768 байт |
| Токен длиннее лимита                   |        Обрезается | Пропускается с предупреждением |
| Префиксный и инфиксный поиск           |    Поддерживается |                 Поддерживается |
| Морфология для токенов длиннее 42 байт | Токен уже обрезан |                 Не применяется |
| RT-таблицы                             |    Поддерживаются |                 Поддерживаются |
| Plain-таблицы                          |    Поддерживаются |                 Поддерживаются |

Настройка задаётся для всей таблицы:

```sql
CREATE TABLE events (
  message text,
  event_id text
)
dict='keywords_32k';
```

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

Для машинных идентификаторов это обычно именно то, что нужно: морфология для хешей и ID, как правило, просто не имеет смысла.

## Когда нужен `keywords_32k`

Используйте его, если одновременно выполняются два условия:

1. Значение после токенизации может быть длиннее 42 байт.
2. Его нужно искать через `MATCH()`, по префиксу или по подстроке.

Типичные примеры:

* SHA-256 и другие длинные хеши;
* event ID и message ID;
* request ID, trace ID и другие технические идентификаторы;
* длинные ключи записей;
* email-адреса с длинной локальной или доменной частью;
* технические значения из логов;
* длинные идентификаторы с разделителями.

Однако `keywords_32k` нужен не для любого поиска по ID.

### Если требуется только полное равенство

Если приложение всегда получает полный ID и нужно проверить только его точное равенство, достаточно строкового атрибута:

```sql
CREATE TABLE events (
  message text,
  event_id string
);
```

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

```sql
SELECT id, message
FROM events
WHERE event_id =
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08';
```

### Если нужны и точное сравнение, и полнотекстовый поиск

Используйте `string attribute indexed`:

```sql
CREATE TABLE events (
  message text,
  event_id string attribute indexed
)
dict='keywords_32k';
```

В этом случае Manticore:

* хранит исходное значение как строковый атрибут;
* позволяет фильтровать по нему через `WHERE`;
* одновременно индексирует его для `MATCH()` и wildcard-поиска.

Для технических идентификаторов это обычно самый удобный вариант.

## Поиск по полному токену

Создадим таблицу и добавим 64-символьный SHA-256:

```sql
DROP TABLE IF EXISTS events;

CREATE TABLE events (
  message text,
  event_id string attribute indexed
)
dict='keywords_32k';

INSERT INTO events (id, message, event_id) VALUES
(
  1,
  'delivery accepted',
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);
```

Полнотекстовый поиск по полному нормализованному токену:

```sql
SELECT id, message
FROM events
WHERE MATCH(
  '@event_id 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);
```

Оператор `@event_id` ограничивает поиск нужным полем. Без него Manticore будет искать значение во всех полнотекстовых полях таблицы.

Кавычки вокруг одного простого буквенно-цифрового токена здесь не нужны. Они обозначают фразовый поиск, а не превращают `MATCH()` в побайтовое сравнение исходной строки.

## Полнотекстовое совпадение и точное равенство — не одно и то же

`MATCH()` работает с результатом токенизации и нормализации. На результат могут влиять:

* `charset_table`;
* приведение символов к нижнему регистру;
* `blend_chars`;
* `ignore_chars`;
* словоформы и другие настройки обработки текста.

Для строгого сравнения сохранённого значения используйте строковый атрибут:

```sql
SET collation_connection='binary';

SELECT id, message
FROM events
WHERE event_id =
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08';
```

`binary` задаёт побайтовое сравнение строк в текущей SQL-сессии. На поведение полнотекстового поиска эта настройка не влияет.

Практическое правило простое:

* `WHERE event_id = ...` — строгое сравнение сохранённой строки;
* `MATCH('@event_id ...')` — поиск нормализованного токена;
* `MATCH('@event_id prefix*')` — префиксный поиск;
* `MATCH('@event_id *fragment*')` — поиск по подстроке.

## Как проверить токенизацию

Перед загрузкой большого объёма данных полезно убедиться, что Manticore действительно видит значение как один токен:

```sql
CALL KEYWORDS(
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
  'events'
);
```

В столбце `normalized` должен появиться полный 64-символьный хеш.

`CALL KEYWORDS` особенно полезен для значений, содержащих:

* точки;
* дефисы;
* символ `@`;
* двоеточия;
* слеши;
* символы разных алфавитов.

Так можно проверить реальные границы токенов ещё до индексации данных.

## Префиксный поиск и поиск по подстроке

Для поиска внутри токена включите `min_infix_len`:

```sql
DROP TABLE IF EXISTS events_infix;

CREATE TABLE events_infix (
  message text,
  event_id string attribute indexed
)
dict='keywords_32k'
min_infix_len='4';

INSERT INTO events_infix (id, message, event_id) VALUES
(
  1,
  'delivery accepted',
  '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);
```

Поиск по префиксу:

```sql
SELECT id, message
FROM events_infix
WHERE MATCH('@event_id 9f86d081*');
```

Поиск по подстроке:

```sql
SELECT id, message
FROM events_infix
WHERE MATCH('@event_id *b2b0b822*');
```

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

### Почему короткие шаблоны опасны

При `dict='keywords_32k'`, как и при обычном `dict='keywords'`, Manticore не создаёт заранее все возможные подстроки. Вместо этого во время запроса wildcard-шаблон разворачивается в подходящие термы из словаря.

Например, шаблону `*ab*` может соответствовать огромное число значений. Чем больше найдено подходящих термов и чем больше документов содержит каждый из них, тем дороже будет такой запрос.

В production-системах:

* не разрешайте пользователям искать слишком короткие фрагменты;
* выбирайте `min_infix_len` с учётом реальных данных;
* используйте `expansion_limit`, чтобы ограничить число расширений;
* проверяйте производительность на словаре, сопоставимом по размеру с production;
* ограничивайте поиск конкретным полем через `@field`.

`index_exact_words='1'` для самого wildcard-поиска не требуется. Он нужен, если вы хотите при ранжировании отличать точные совпадения от wildcard-совпадений, обычно вместе с `expand_keywords`.

## Email и другие значения с разделителями

`keywords_32k` меняет только максимальную длину токена. Он не определяет, где токен начинается и заканчивается.

По умолчанию точка, `@`, дефис и другие символы могут разделять значение на несколько токенов. Если email-адрес или message ID нужно индексировать ещё и целиком, можно использовать `blend_chars`:

```sql
DROP TABLE IF EXISTS mail_events;

CREATE TABLE mail_events (
  sender string attribute indexed,
  subject text
)
dict='keywords_32k'
blend_chars='., @, -'
min_infix_len='4';

INSERT INTO mail_events (id, sender, subject) VALUES
(
  1,
  'alessandro.verylonggeneratedlocalpart@example-corporate-domain.test',
  'delivery accepted'
);
```

Символы, перечисленные в `blend_chars`, позволяют индексировать значение двумя способами:

* как один цельный токен;
* как отдельные части значения.

Благодаря этому можно искать как весь email-адрес, так и отдельные его части.

Поиск полного значения:

```sql
SELECT id, subject
FROM mail_events
WHERE MATCH(
  '@sender "alessandro.verylonggeneratedlocalpart@example-corporate-domain.test"'
);
```

Кавычки здесь важны: символ `@` одновременно используется в синтаксисе полнотекстовых запросов. Внутри фразы парсер может обработать его как часть токена благодаря `blend_chars`.

Поиск по фрагменту:

```sql
SELECT id, subject
FROM mail_events
WHERE MATCH('@sender *generatedlocalpart*');
```

Проверить результат токенизации можно так:

```sql
CALL KEYWORDS(
  '"alessandro.verylonggeneratedlocalpart@example-corporate-domain.test"',
  'mail_events'
);
```

В реальном приложении значения, передаваемые в `MATCH()`, нужно корректно экранировать. Нельзя просто подставлять пользовательский ввод в строку запроса: символы `@`, `-`, `|`, `!`, `"`, `*` и другие операторы могут изменить его смысл.

## Как перевести существующую таблицу

Для RT-таблицы настройку можно изменить через `ALTER TABLE`:

```sql
ALTER TABLE events dict='keywords_32k';
```

Но изменение затронет только документы, добавленные или заменённые после этого.

Уже существующие документы не будут автоматически токенизированы заново. Их длинные токены останутся в старом, обрезанном виде до переиндексации.

Поэтому порядок действий должен быть таким:

1. Обновить `dict`.
2. Проверить настройку через `SHOW CREATE TABLE`.
3. Переиндексировать или повторно загрузить существующие документы.
4. Проверить несколько длинных значений через `CALL KEYWORDS` и `MATCH()`.

Для plain-таблицы нужно:

1. Изменить конфигурацию на `dict = keywords_32k`.
2. Применить настройки через `ALTER TABLE ... RECONFIGURE`, если вы обновляете конфигурацию таким способом.
3. Полностью перестроить таблицу из источника данных.

Пока данные не переиндексированы, в одной таблице могут одновременно находиться:

* старые документы с обрезанными токенами;
* новые документы с полными токенами.

Из-за этого одинаковые на первый взгляд значения могут давать разные результаты поиска.

## Почему `dict='crc'` не решает эту задачу

`dict='crc'` хранит контрольные суммы ключевых слов вместо их исходного текста. Однако это не увеличивает допустимую длину токена.

Исключение из обычного лимита в 42 байта реализовано именно в `dict='keywords_32k'`.

Кроме того, словари `keywords` и `keywords_32k` хранят тексты термов, благодаря чему Manticore может разворачивать префиксные и инфиксные wildcard-запросы по словарю.

Если нужно искать длинные машинные идентификаторы, `crc` не заменяет `keywords_32k`.

## Текущие ограничения

На момент публикации у `dict='keywords_32k'` есть несколько ограничений:

* `CALL SUGGEST` и `CALL QSUGGEST` не поддерживаются;
* его нельзя использовать в percolate-таблицах;
* для токенов длиннее 42 байт не работает подсветка в snippets и highlights;
* `indextool --dumpdict` не умеет выгружать такой словарь;
* полнотекстовый оператор `REGEX` работает с `dict='keywords'`, но не с `keywords_32k`.

Последний пункт не следует путать с функцией `REGEX()` для фильтрации строковых атрибутов. Если поле объявлено как `string attribute indexed`, фильтрация по атрибуту и полнотекстовый поиск остаются двумя разными механизмами.

## Не индексируйте секреты

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

Без крайней необходимости не индексируйте:

* API-ключи;
* bearer-токены;
* сессионные cookie;
* приватные ключи;
* пароли и токены сброса пароля;
* другие данные, предоставляющие доступ к системе.

`keywords_32k` решает задачу поиска, но не защищает эти данные: они могут быть доступны пользователям и администраторам, а также попадать в резервные копии и логи запросов.

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

## Краткий чек-лист

Перед включением `keywords_32k` проверьте:

1. Действительно ли нормализованный токен длиннее 42 байт?
2. Нужен ли полнотекстовый или wildcard-поиск, а не только `WHERE value = ...`?
3. Видит ли `CALL KEYWORDS` всё значение как один токен?
4. Нужны ли `blend_chars` для точек, дефисов, `@` и других разделителей?
5. Не слишком ли мал `min_infix_len`?
6. Ограничено ли число термов, в которые может развернуться wildcard-запрос?
7. Переиндексированы ли старые документы?
8. Не содержит ли поле секретных данных?
9. Не зависит ли приложение от подсветки, `SUGGEST`, percolate или полнотекстового `REGEX`?

## Итог

`dict='keywords_32k'` решает конкретную проблему: позволяет полнотекстовому индексу хранить нормализованные токены длиной до 32768 байт вместо обычных 42 байт.

Он хорошо подходит для длинных хешей, event ID, message ID, email-адресов и других машинных идентификаторов. При этом важно помнить три вещи:

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

Если нужно только точное равенство, используйте строковый атрибут. Если нужны и точное сравнение, и поиск по частям значения, используйте `string attribute indexed` вместе с `dict='keywords_32k'`.

## Документация

* [`dict` и ограничения `keywords_32k`](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Low-level_tokenization#dict)
* [Ограничение длины токена](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Data_tokenization#Token-length-limit)
* [Настройки wildcard-поиска](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Wildcard_searching_settings)
* [`blend_chars`](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Low-level_tokenization#blend_chars)
* [`CALL KEYWORDS`](https://manual.manticoresearch.com/Searching/Autocomplete#CALL-KEYWORDS)
* [Строковые атрибуты и индексируемые строки](https://manual.manticoresearch.com/Creating_a_table/Local_tables#String)
* [Обновление полнотекстовых настроек и переиндексация](https://manual.manticoresearch.com/Updating_table_schema_and_settings)
* [Collations и сравнение строк](https://manual.manticoresearch.com/Searching/Collations)
* [Manticore Search 27.1.5](https://manticoresearch.com/blog/manticore-search-27-1-5/)
