В прошлом уроке книги находились по тегам, которые кто-то проставил руками. Покупатель же ищет словами: «маргарита москва», «золотой шар». В этом уроке учим магазин искать по смыслу слов, а не по совпадению букв, и обходимся без отдельного поискового движка. Примеры на учебной базе магазина.
Дадим книгам аннотации:
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 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.