Полнотекстовые индексы

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

Общее описание полнотекстового поиска см. в разделе Полнотекстовый поиск.

Характеристики полнотекстовых индексов

Полнотекстовые индексы в YDB строятся путём токенизации текста и создания инвертированного индекса. Это позволяет:

  • быстро фильтровать строки через FulltextMatch;
  • ранжировать результаты по релевантности (BM25) через FulltextScore при использовании fulltext_relevance;
  • применять нормализацию регистра, стемминг и N-граммы с помощью фильтров индекса.

В текущей реализации доступны два варианта индекса:

  • fulltext_plain — базовый полнотекстовый индекс;
  • fulltext_relevance — полнотекстовый индекс со статистикой BM25 для расчёта релевантности.

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

Виды полнотекстовых индексов

YDB поддерживает два типа полнотекстовых индексов, которые различаются составом хранимой статистики:

  • fulltext_plain — хранит только инвертированный индекс. Поддерживает фильтрацию через FulltextMatch, но не позволяет ранжировать результаты по релевантности.
  • fulltext_relevance — дополнительно хранит частотную статистику (TF-IDF / BM25), необходимую для работы FulltextScore.

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

Базовый полнотекстовый индекс (fulltext_plain)

Используйте fulltext_plain, когда достаточно проверить наличие термов в тексте без ранжирования по релевантности. Такой индекс компактнее fulltext_relevance и подходит для большинства задач фильтрации.

Пример создания глобального полнотекстового индекса по колонке body:

ALTER TABLE articles
  ADD INDEX ft_index
  GLOBAL USING fulltext_plain
  ON (body)
  WITH (tokenizer=standard, use_filter_lowercase=true);

Здесь tokenizer=standard разбивает текст на слова по пробелам и знакам препинания, а use_filter_lowercase=true нормализует все токены к нижнему регистру — это делает поиск регистронезависимым.

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

SELECT id, title
FROM articles VIEW ft_index
WHERE FulltextMatch(body, "поисковые термы")
LIMIT 20;

Полнотекстовый индекс для ранжирования (fulltext_relevance)

fulltext_relevance хранит инвертированный индекс вместе с частотной статистикой (BM25), которая позволяет функции FulltextScore вычислять оценку релевантности документа запросу. Используйте этот тип, когда нужно не просто найти документы с нужными словами, но и упорядочить их по степени соответствия.

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

ALTER TABLE articles
  ADD INDEX ft_index
  GLOBAL USING fulltext_relevance
  ON (body)
  WITH (tokenizer=standard, use_filter_lowercase=true);

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

SELECT id, title, FulltextScore(body, "поисковые термы") AS relevance
FROM articles VIEW ft_index
WHERE FulltextScore(body, "поисковые термы") > 0
ORDER BY relevance DESC
LIMIT 10;

Поиск по подстроке (N-граммы)

Если вам нужен поиск по подстроке, создайте индекс с N-граммами. Доступны два типа N-грамм:

  • Обычные N-граммы (use_filter_ngram) — разбивают слова на все возможные подстроки заданной длины, что позволяет находить совпадения в любой части слова. Например, слово "search" будет разбито на "sea", "ear", "arc", "rch" и т.д.
  • Краевые N-граммы (use_filter_edge_ngram) — создают подстроки только от начала слова, что идеально подходит для автодополнения. Например, слово "search" будет разбито на "se", "sea", "sear", "searc", "search".

При использовании N-грамм станут доступны:

  • FulltextMatch(..., "Wildcard" AS Mode) — поиск с шаблонами % и _ (аналогично LIKE);
  • предикаты LIKE / ILIKE по индексируемой текстовой колонке — YDB автоматически использует N-граммовый индекс при обращении к нему через VIEW IndexName.

Пример индекса с N-граммами:

ALTER TABLE articles
  ADD INDEX ngram_index
  GLOBAL USING fulltext_plain
  ON (body)
  WITH (
    tokenizer=standard,
    use_filter_lowercase=true,
    use_filter_ngram=true,
    filter_ngram_min_length=3,
    filter_ngram_max_length=5
  );

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

SELECT id, title
FROM articles VIEW ngram_index
WHERE FulltextMatch(body, "%обуч%", "Wildcard" AS Mode)
LIMIT 20;

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

SELECT id, title
FROM articles VIEW ngram_index
WHERE body LIKE "%обуч%ние%"
LIMIT 20;

Запрос с LIKE / ILIKE использует ту же логику, что и FulltextMatch(body, ..., "Wildcard" AS Mode), и обращается к тому же N-граммовому индексу.

Полнотекстовый индекс с фильтрацией

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

ALTER TABLE articles
  ADD INDEX ft_index
  GLOBAL USING fulltext_plain
  ON (user_id, body)
  WITH (tokenizer=standard, use_filter_lowercase=true);

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

SELECT id, title
FROM articles VIEW ft_index
WHERE user_id = 42 AND FulltextMatch(body, "поисковые термы")
LIMIT 20;

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

Типы первичного ключа

Внутри инвертированного индекса каждый проиндексированный документ идентифицируется числовым идентификатором документа (doc_id). Способ получения doc_id зависит от первичного ключа основной таблицы:

  • Первичный ключ из одной целочисленной колонки (Uint64, Int64, Uint32 или Int32) — значение первичного ключа используется напрямую в качестве doc_id. Это самая компактная форма, дополнительные структуры не создаются.
  • Любой другой первичный ключ (например, Utf8, String, другие нецелочисленные типы или составной ключ из нескольких колонок) — YDB поддерживает системную колонку __ydb_row_id типа Uint64 и использует её в качестве doc_id.

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

  • добавляет в таблицу колонку __ydb_row_id — существующие строки заполняются во время построения индекса;
  • генерирует значение __ydb_row_id для каждой строки, в которой эта колонка не указана в INSERT / UPSERT;
  • создаёт уникальный вторичный индекс с именем __ydb_unique_row_id по колонке __ydb_row_id. Во время запроса этот индекс сопоставляет найденный __ydb_row_id с первичным ключом таблицы перед чтением строки из основной таблицы.

Если на таблице создаётся несколько полнотекстовых индексов, все они переиспользуют одну и ту же колонку __ydb_row_id и индекс __ydb_unique_row_id — эти структуры создаются для таблицы только один раз.

Важно

Колонкой __ydb_row_id и индексом __ydb_unique_row_id управляет YDB:

  • Позвольте YDB заполнять __ydb_row_id — не указывайте её в INSERT / UPSERT, и значение проставится автоматически. Модификация колонки __ydb_row_id пользователем запрещена — любые попытки задать или изменить значение этой колонки будут отклонены.
  • Индекс __ydb_unique_row_id нельзя удалить, пока от него зависит хотя бы один полнотекстовый индекс. Сначала удалите зависимые полнотекстовые индексы.

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

Создание полнотекстового индекса:

  • при создании таблицы: CREATE TABLE;
  • добавление к существующей таблице: ALTER TABLE.

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

Функции и выражения для полнотекстового поиска:

Примечание

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

Если не использовать выражение VIEW, запросы с FulltextMatch / FulltextScore завершатся с ошибкой.

Это ограничение может быть снято в будущих версиях YDB.

Обновление полнотекстовых индексов

Полнотекстовые индексы автоматически поддерживаются при модификации данных. Таблицы с полнотекстовыми индексами поддерживают:

  • INSERT
  • UPSERT
  • REPLACE
  • UPDATE
  • DELETE

Удаление полнотекстовых индексов

ALTER TABLE articles DROP INDEX ft_index;

Ограничения

  • Для таблиц с нецелочисленным или составным первичным ключом автоматически создаются системная колонка __ydb_row_id и уникальный индекс __ydb_unique_row_id (см. Типы первичного ключа).
  • BulkUpsert не поддерживается для таблиц с полнотекстовыми индексами.
  • Использование полнотекстового индекса необходимо задавать явно с помощью VIEW IndexName.
  • В одном полнотекстовом индексе индексируется одна текстовая колонка.
  • FulltextMatch / FulltextScore нельзя использовать c OR или NOT. Допускается комбинация с другими предикатами через AND.
  • В одном чтении через VIEW поддерживается только один полнотекстовый предикат: нельзя использовать несколько FulltextScore и нельзя смешивать FulltextMatch и FulltextScore в одном WHERE.
  • Для доступа к индексу по релевантности требуется ограничение FulltextScore(...) > 0 в WHERE (иначе запрос завершится с ошибкой).
  • Полнотекстовые индексы с фильтрацией: для каждой колонки фильтрации требуется предикат равенства в WHERE.
  • Полнотекстовые индексы с фильтрацией: колонки фильтрации не должны быть колонками первичного ключа.