⚠️ Эта страница автоматически переведена, и перевод может быть несовершенным.
blog-post

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

View as markdown

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

В логах и технических данных всё иначе. 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

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

  1. Значение после токенизации может быть длиннее 42 байт.
  2. Его нужно искать через 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';

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

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

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

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

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

Установить Manticore Search

Установите Manticore Search одной командой в Linux или macOS:

curl https://manticoresearch.com | sh

Для расширенных вариантов установки см. полное руководство по установке и документацию .

Установить Manticore Search