Раздел 33 · PostgreSQL с нуля

Полнотекстовый поиск: tsvector и tsquery

junior~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке книги находились по тегам, которые кто-то проставил руками. Покупатель же ищет словами: «маргарита москва», «золотой шар». В этом уроке учим магазин искать по смыслу слов, а не по совпадению букв, и обходимся без отдельного поискового движка. Примеры на учебной базе магазина.

Дадим книгам аннотации:

ALTER TABLE books ADD COLUMN IF NOT EXISTS annotation text;

UPDATE books SET annotation = 'Воланд со свитой приезжает в Москву. Мастер пишет роман о Понтии Пилате, а Маргарита ради него становится королевой бала.' WHERE id = 1;
UPDATE books SET annotation = 'Профессор Преображенский пересаживает бродячему псу человеческий гипофиз, и пёс Шарик превращается в Полиграфа Шарикова.' WHERE id = 2;
UPDATE books SET annotation = 'После посещения пришельцев остались Зоны. Сталкер Рэдрик Шухарт ходит в Зону за артефактами и ищет Золотой шар, который исполняет желания.' WHERE id = 3;
UPDATE books SET annotation = 'Земной историк Румата Эсторский живёт в средневековом Арканаре и не имеет права вмешиваться в чужую историю.' WHERE id = 4;
UPDATE books SET annotation = 'Бедный чиновник Акакий Акакиевич копит на новую шинель, а её отнимают грабители.' WHERE id = 5;
UPDATE books SET annotation = 'Чичиков объезжает помещиков и скупает мёртвых крестьян, которые числятся живыми по ревизии.' WHERE id = 6;
UPDATE books SET annotation = 'Иллюстрированное введение в алгоритмы: бинарный поиск, сортировка, графы, жадные алгоритмы и динамическое программирование на Python.' WHERE id = 7;
UPDATE books SET annotation = 'Практическое руководство по конструированию программ: проектирование, именование переменных, отладка, рефакторинг и качество кода.' WHERE id = 8;
UPDATE books SET annotation = 'Нос майора Ковалёва пропадает с лица и разгуливает по Петербургу в мундире статского советника.' WHERE id = 9;

Почему LIKE не поиск

Покупатель вводит «москва». Пробуем ILIKE из урока про выборку:

SELECT title FROM books WHERE annotation ILIKE '%москва%';
 title
-------
(0 rows)

Пусто, хотя в аннотации «Мастера и Маргариты» написано «приезжает в Москву». LIKE сравнивает буквы, а в тексте другая форма слова. В русском языке у слова десятки форм, и перебирать их шаблонами безнадёжно. А ещё LIKE '%...%' не умеет ранжировать результаты и не использует обычный индекс, это разбирали в уроке про неиспользуемые индексы.

Библиотекарь с картотекой

Опытный библиотекарь не читает каждую книгу, когда ты спрашиваешь про Москву. Он заранее завёл карточки: для каждой книги список важных слов в начальной форме. «Москву», «Москвы» и «Москве» он записал как одно слово. Предлоги и союзы вроде «в», «и», «о» выбросил: по ним никто не ищет. Твой вопрос он тоже приводит к начальной форме и сверяет с карточками.

Полнотекстовый поиск PostgreSQL работает так же, и у него два типа данных:

  • tsvector это карточка документа: список лексем с позициями слов.
  • tsquery это вопрос: лексемы и связки между ними.

Функция to_tsvector делает карточку из текста:

SELECT to_tsvector('russian', 'Мастер пишет роман о Понтии Пилате') AS lexemes;
                     lexemes
--------------------------------------------------
 'мастер':1 'пилат':6 'пишет':2 'понт':5 'рома':3
(1 row)

Предлог «о» пропал, это стоп-слово. «Пилате» стало «пилат», «Понтии» стало «понт». Число после двоеточия это номер слова в тексте, он нужен для поиска фраз.

Первый аргумент 'russian' это конфигурация: правила русского языка, по которым отрезаются окончания и выбрасываются стоп-слова. Конфигурация simple ничего не отрезает, а только переводит в нижний регистр. Указывай конфигурацию всегда явно: иначе PostgreSQL возьмёт значение из настроек сервера, а оно может оказаться английским.

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

урок из курса

Урок из курса «PostgreSQL с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.