Перейти к содержанию

Полнотекстовой поиск

Postgres позволяет реализовать полнотекстовой поиск (FTS, fulltext search) без всяких Sphinx и т.д. Поиск можно будет проводить прям в самой БД, запросом на select. Делаться всё будет на PostgreSQL 15

Переводим данные в tsvector

Немного теории

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

Например, есть у нас текст "Драйверы и дравера". При перевода его в tsvector, он будет содержать всего ОДНУ лексему (драйвер), так как окончания (драйвер-а, драйвер-ы) отбросятся. Так же, все слова в тексте будут приведены к нижнему регистру, а всякие связующие слова и знаки препинания (их еще называют стоп-слова, по типу "что", "и") отбросятся из результата.

Переводим данные

Для перевода нашего текста в tsvector надо использовать функцию to_tsvector(конфиг regconfig, документ text), например:

select to_tsvector('russian', "содержание какого-то документа");

Эта функция разберёт текстовой документ на фрагменты, сведёт фрагменты к лексемам и возвратит значение с типом tsvector. Самое интересное здесь то, что мы указали словарь 'russian' в качестве опции конфига, который заюзает необходимый нам словарь и прочие оптимизации.

Храним данные

Разбор документа, преобразование в лексемы и т.д. - вещи довольно затратные, поэтому tsvector как правило хранят в базе. Тут есть 2 популярных способа:

  1. Храним прям в искомой таблице, если мы их будем бекапить - они забекапятся вместе с данными.
  2. Храним в отдельной таблице, при бекапе их можно не бекапить, так как они могут занимать существенное место.

Обновляем данные

Тут тоже есть несколько подходов.

  1. Обновляем при записи. Тут всё просто, при insert или update в базу вызываем to_tsvector прямо в запросе и радуемся жизни. Подходит, если БД пользуется только одно приложение и вся логика у вас на бекенде:
INSERT INTO messages VALUES('название', 'а вот тут текст', to_tsvector('а вот тут текст'));
  1. Через триггер. Сам postgresql предоставляет процедурку tsvector_update_trigger, которая позволяет создавать триггер, в котором при изменении данных в БД будет вызываться обновление колонки с tsvector:

Код процедуры выглядит как-то так:

CREATE TRIGGER search_messages_body BEFORE INSERT OR UPDATE OF body ON messages
FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(
    tsv, 'pg_catalog.russian', body
);

Инсертим:

INSERT INTO messages VALUES('название', 'а вот тут текст');

Кстати, в запросе используется часть с OF body. Это значит, что триггер будет исполнятся только при обновлении столбца с body, что позволяет нам не вызывать обновление для tsv лишний раз.

Ну и как было понятно, подходит для всех остальных случаев.

Ищем данные при помощи tsquery

Тип tsquery содержит лексемы, которые объеденены специальными операторами (тут читайте какие), которые задают правила поиска.

Но в основном нам понадится 3 функции, которые позволяют искать нам по tsvector - это to_tsquery, plainto_tsquery, phraseto_tsquery и websearch_to_tsquery. Про них можно почитать вот здесь, а здесь мы будем разбирать лишь phraseto_tsquery, который по моему опыту используется чаще всего:

SELECT * FROM messages
WHERE tsv @@ phraseto_tsquery('russian', 'важное сообщение')

Здесь запрос будет искать сообщения, которые соответствует шаблону 'важн' <-> 'сообщен'. Оператор <-> показывает что какое-то слово предшествует другому.

Ранжируем данные

Ранжирование документов - это попытка представить, насколько они соответствуют запросу и отсортировать их по релевантности. Алгоритмов ранжирования много, но Postgres предоставляет для этого две функции ts_rank и ts_rank_cd. Для примера возьмем первую.

ts_rank ранжирует вектора по частоте найденных лексем. Запрос с его использованием выглядит так:

SELECT * FROM messages
WHERE tsv @@ phraseto_tsquery('russian', 'важное сообщение')
ORDER BY ts_rank(tsv, phraseto_tsquery('russian', 'важное сообщение'))

Индексируем

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

Так как мы уже сделали колонку tsv, которая содержит подготовленные для поиска данные, мы можем создать индекс вот таким способом:

CREATE INDEX idx_gin_messages 
ON messages
USING gin ("tsv");

Ссылки