Полнотекстовый поиск обычно работает с обычными словами: названиями товаров, заголовками, комментариями и описаниями. Такие токены редко бывают длиннее нескольких десятков символов.
В логах и технических данных всё иначе. 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-таблицы | Поддерживаются | Поддерживаются |
Настройка задаётся для всей таблицы:
CREATE TABLE events (
message text,
event_id text
)
dict='keywords_32k';
Обычные короткие слова в такой таблице по-прежнему обрабатываются настроенной морфологией. Токены длиннее 42 байт сохраняются после нормализации, но без стемминга и лемматизации.
Для машинных идентификаторов это обычно именно то, что нужно: морфология для хешей и ID, как правило, просто не имеет смысла.
Когда нужен keywords_32k
Используйте его, если одновременно выполняются два условия:
- Значение после токенизации может быть длиннее 42 байт.
- Его нужно искать через
MATCH(), по префиксу или по подстроке.
Типичные примеры:
- SHA-256 и другие длинные хеши;
- event ID и message ID;
- request ID, trace ID и другие технические идентификаторы;
- длинные ключи записей;
- email-адреса с длинной локальной или доменной частью;
- технические значения из логов;
- длинные идентификаторы с разделителями.
Однако keywords_32k нужен не для любого поиска по ID.
Если требуется только полное равенство
Если приложение всегда получает полный ID и нужно проверить только его точное равенство, достаточно строкового атрибута:
CREATE TABLE events (
message text,
event_id string
);
Тогда поиск выполняется обычным фильтром:
SELECT id, message
FROM events
WHERE event_id =
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08';
Если нужны и точное сравнение, и полнотекстовый поиск
Используйте string attribute indexed:
CREATE TABLE events (
message text,
event_id string attribute indexed
)
dict='keywords_32k';
В этом случае Manticore:
- хранит исходное значение как строковый атрибут;
- позволяет фильтровать по нему через
WHERE; - одновременно индексирует его для
MATCH()и wildcard-поиска.
Для технических идентификаторов это обычно самый удобный вариант.
Поиск по полному токену
Создадим таблицу и добавим 64-символьный SHA-256:
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'
);
Полнотекстовый поиск по полному нормализованному токену:
SELECT id, message
FROM events
WHERE MATCH(
'@event_id 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
);
Оператор @event_id ограничивает поиск нужным полем. Без него Manticore будет искать значение во всех полнотекстовых полях таблицы.
Кавычки вокруг одного простого буквенно-цифрового токена здесь не нужны. Они обозначают фразовый поиск, а не превращают MATCH() в побайтовое сравнение исходной строки.
Полнотекстовое совпадение и точное равенство — не одно и то же
MATCH() работает с результатом токенизации и нормализации. На результат могут влиять:
charset_table;- приведение символов к нижнему регистру;
blend_chars;ignore_chars;- словоформы и другие настройки обработки текста.
Для строгого сравнения сохранённого значения используйте строковый атрибут:
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 действительно видит значение как один токен:
CALL KEYWORDS(
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
'events'
);
В столбце normalized должен появиться полный 64-символьный хеш.
CALL KEYWORDS особенно полезен для значений, содержащих:
- точки;
- дефисы;
- символ
@; - двоеточия;
- слеши;
- символы разных алфавитов.
Так можно проверить реальные границы токенов ещё до индексации данных.
Префиксный поиск и поиск по подстроке
Для поиска внутри токена включите min_infix_len:
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'
);
Поиск по префиксу:
SELECT id, message
FROM events_infix
WHERE MATCH('@event_id 9f86d081*');
Поиск по подстроке:
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:
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-адрес, так и отдельные его части.
Поиск полного значения:
SELECT id, subject
FROM mail_events
WHERE MATCH(
'@sender "alessandro.verylonggeneratedlocalpart@example-corporate-domain.test"'
);
Кавычки здесь важны: символ @ одновременно используется в синтаксисе полнотекстовых запросов. Внутри фразы парсер может обработать его как часть токена благодаря blend_chars.
Поиск по фрагменту:
SELECT id, subject
FROM mail_events
WHERE MATCH('@sender *generatedlocalpart*');
Проверить результат токенизации можно так:
CALL KEYWORDS(
'"alessandro.verylonggeneratedlocalpart@example-corporate-domain.test"',
'mail_events'
);
В реальном приложении значения, передаваемые в MATCH(), нужно корректно экранировать. Нельзя просто подставлять пользовательский ввод в строку запроса: символы @, -, |, !, ", * и другие операторы могут изменить его смысл.
Как перевести существующую таблицу
Для RT-таблицы настройку можно изменить через ALTER TABLE:
ALTER TABLE events dict='keywords_32k';
Но изменение затронет только документы, добавленные или заменённые после этого.
Уже существующие документы не будут автоматически токенизированы заново. Их длинные токены останутся в старом, обрезанном виде до переиндексации.
Поэтому порядок действий должен быть таким:
- Обновить
dict. - Проверить настройку через
SHOW CREATE TABLE. - Переиндексировать или повторно загрузить существующие документы.
- Проверить несколько длинных значений через
CALL KEYWORDSиMATCH().
Для plain-таблицы нужно:
- Изменить конфигурацию на
dict = keywords_32k. - Применить настройки через
ALTER TABLE ... RECONFIGURE, если вы обновляете конфигурацию таким способом. - Полностью перестроить таблицу из источника данных.
Пока данные не переиндексированы, в одной таблице могут одновременно находиться:
- старые документы с обрезанными токенами;
- новые документы с полными токенами.
Из-за этого одинаковые на первый взгляд значения могут давать разные результаты поиска.
Почему 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 проверьте:
- Действительно ли нормализованный токен длиннее 42 байт?
- Нужен ли полнотекстовый или wildcard-поиск, а не только
WHERE value = ...? - Видит ли
CALL KEYWORDSвсё значение как один токен? - Нужны ли
blend_charsдля точек, дефисов,@и других разделителей? - Не слишком ли мал
min_infix_len? - Ограничено ли число термов, в которые может развернуться wildcard-запрос?
- Переиндексированы ли старые документы?
- Не содержит ли поле секретных данных?
- Не зависит ли приложение от подсветки,
SUGGEST, percolate или полнотекстовогоREGEX?
Итог
dict='keywords_32k' решает конкретную проблему: позволяет полнотекстовому индексу хранить нормализованные токены длиной до 32768 байт вместо обычных 42 байт.
Он хорошо подходит для длинных хешей, event ID, message ID, email-адресов и других машинных идентификаторов. При этом важно помнить три вещи:
keywords_32kувеличивает максимальную длину токена, но не меняет правила токенизации;MATCH()по полному токену не равен строгому сравнению исходной строки;- после изменения настройки существующие документы необходимо переиндексировать.
Если нужно только точное равенство, используйте строковый атрибут. Если нужны и точное сравнение, и поиск по частям значения, используйте string attribute indexed вместе с dict='keywords_32k'.
