Полнотекстовый поиск в PostgreSQL: разбор каталога товаров
Разбираем поиск по каталогу на PostgreSQL 16: как хранить поисковый вектор, подобрать GIN-индекс и ранжировать результаты. Показываем, что измерять и где решение начнёт тормозить.

В каталоге товаров поиск по названию и описанию должен находить не только точное совпадение, но и формы слова: запрос «кроссовки» — товар с названием «кроссовка». На примере каталога разберём рабочую схему для PostgreSQL 16: поисковый вектор, GIN-индекс и ранжирование. Это проектный пример, а не отчёт о замерах: реальные показатели зависят от данных и нагрузки.
Исходная задача: искать по нескольким полям
Представим каталог с таблицей products: в ней есть название товара и описание. Обычный поиск через LIKE '%запрос%' проверяет подстроку, но не приводит слова к нормальной форме. На крупной таблице такой фильтр без подходящего индекса может читать много строк, а добавление ещё одного текстового поля усложняет условие.
Полнотекстовый поиск PostgreSQL разбивает текст на лексемы и сопоставляет их с запросом. Для русского языка есть конфигурация russian, которая учитывает морфологию. Это не поиск опечаток: ошибочно набранное слово может не совпасть с правильным.
В примере название важнее описания, поэтому при ранжировании ему назначается вес A, а описанию — B. Для этого добавим вычисляемый столбец и индекс:
ALTER TABLE products
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('russian', coalesce(title, '')), 'A') ||
setweight(to_tsvector('russian', coalesce(description, '')), 'B')
) STORED;
CREATE INDEX products_search_vector_gin
ON products USING GIN (search_vector);
Здесь coalesce заменяет NULL пустой строкой, чтобы он не мешал построению вектора. Вычисляемый столбец пересчитывается при изменении исходных полей: приложению не нужно отдельно обновлять поисковый индексный документ. На большой существующей таблице добавление хранимого вычисляемого столбца может занять время и нагрузить базу; сначала проверьте миграцию на копии данных.
Как выполнить поиск и не потерять смысл запроса
Для строки, введённой пользователем, подойдёт websearch_to_tsquery: функция разбирает поисковый ввод, в том числе фразы в кавычках и операторы OR и минус. В приложении передавайте пользовательский запрос параметром, а не собирайте SQL конкатенацией строк.
WITH query AS (
SELECT websearch_to_tsquery('russian', $1) AS tsq
)
SELECT
p.id,
p.title,
ts_rank_cd(p.search_vector, query.tsq) AS rank
FROM products AS p
CROSS JOIN query
WHERE p.search_vector @@ query.tsq
ORDER BY rank DESC, p.id
LIMIT 20;
Оператор @@ оставляет строки, в которых найдено совпадение, а ts_rank_cd оценивает релевантность. Вес A в векторе повышает вклад названия по сравнению с описанием. Рейтинг — не вероятность и не обещание, что первый товар лучше для каждого пользователя. Вторичная сортировка по id делает порядок при равных оценках предсказуемее.
Перед выкладкой проверьте поведение на реальных запросах: одно слово, несколько слов, фраза, пустая строка, дефисное написание и слово из описания. В частности, решите, должны ли несколько слов означать поиск всех слов или достаточно любого совпадения. Это продуктовая логика: её нельзя надёжно вывести только из наличия индекса.
Три подхода: от простого к рабочему каталогу
| Подход | Что выигрываете | За что платите |
|---|---|---|
| Строить вектор в запросе без индекса | Минимум изменений в схеме, удобно для небольших данных и проверки идеи. | Базе приходится обрабатывать текст при поиске; с ростом таблицы задержка может стать проблемой. |
| Индексировать выражение | Можно ускорить поиск без отдельного столбца, если выражение в индексе и запросе совпадает. | Легко получить расхождение между индексируемым выражением и реальным запросом; несколько полей и весов усложняют поддержку. |
Хранимый tsvector и GIN | Явно задаются поля и их веса; запрос использует готовый вектор. | Индекс занимает место и добавляет работу при записи и обновлении товаров. |
Для каталога, где поиск — постоянная функция, третий вариант обычно удобнее контролировать: состав вектора виден в схеме, а GIN-индекс предназначен для поиска по лексемам. Но наличие индекса само по себе не гарантирует, что планировщик выберет его: на маленьком наборе строк последовательное чтение может оказаться дешевле.
GIN ускоряет отбор совпадений, но не отменяет цену обновлений: чем чаще меняются индексируемые тексты, тем важнее измерять не только поиск, но и запись.
Когда нужен поиск с опечатками
Морфологический поиск и исправление опечаток — разные задачи. Если покупатель вводит «красовки», полнотекстовый поиск может не найти «кроссовки». Для поиска похожих строк в PostgreSQL существует расширение pg_trgm: оно сравнивает триграммы — последовательности из трёх символов. Его можно рассматривать как отдельный путь для подсказок или неточных совпадений, но не как бесплатную замену полнотекстовому поиску: придётся выбрать поля, порог похожести и проверить качество выдачи на запросах пользователей.
Не включайте оба механизма без проверки сценариев. Например, основную выдачу можно строить по полнотекстовому совпадению, а подсказки при нулевом результате — оценивать отдельно через триграммы. Это увеличит сложность запроса и потребует отдельного индекса; вначале убедитесь, что проблема с опечатками действительно заметна в поисковых логах.
Что измерить до запуска
Универсальных цифр ускорения здесь нет. Время зависит от объёма и распределения данных, числа обновлений, железа, конкурирующих запросов и того, насколько часто запрос возвращает совпадения. Сравнивайте варианты на копии производственных данных или на репрезентативном наборе, а не на нескольких вручную добавленных товарах.
- Соберите набор характерных запросов: частые, редкие, многословные и без результатов. Проверьте, что ожидаемые товары вообще попадают в выдачу.
- Сравните поиск без индекса и с GIN на одинаковых данных. Для диагностики используйте
EXPLAIN (ANALYZE, BUFFERS)и смотрите на фактическое время, прочитанные страницы и выбранный план. - Измерьте не только удачный поиск, но и запросы, которые возвращают много совпадений, а также пустой результат. Сравните несколько прогонов и одинаковые условия нагрузки.
- Отдельно проверьте цену записи: время обновления товара и объём индекса. При высокой частоте изменений быстрый поиск может обойтись заметно дороже на стороне записи.
EXPLAIN ANALYZE действительно выполняет запрос. Не запускайте тяжёлый диагностический запрос бездумно на производственной базе: сначала проверьте его стоимость и выберите безопасное время или копию данных.
Что ломается и как это заметить
Запрос не находит слово, которое точно есть
Проверьте конфигурацию языка и содержимое вектора. Если вектор построен с одной конфигурацией, а пользовательский запрос — с другой, лексемы могут не совпасть. Также убедитесь, что поле не осталось пустым из-за значения NULL или особенностей импорта.
Индекс создан, а план всё равно показывает последовательное чтение
Это не обязательно ошибка. Для небольшой таблицы или запроса, которому соответствуют многие строки, последовательное чтение может быть дешевле. Смотрите на план и измерения, а не требуйте от PostgreSQL использовать индекс в каждом случае.
Выдача стала хорошей, а обновление товара — медленным
Изменение столбца, входящего в поисковый вектор, требует его пересчёта и обновления индекса. Проверьте частоту таких изменений и стоимость записи под нагрузкой. Если описание обновляется постоянно, а название почти нет, можно пересмотреть состав индексируемых полей или разделить операции — но сначала подтвердите проблему замерами.
Пользователи ждут опечатки, а получают пустой список
Это ограничение выбранного механизма, а не обязательно неисправность индекса. Соберите реальные неуспешные запросы и решите, нужны ли подсказки, триграммное сравнение или словарь синонимов. Не добавляйте исправление опечаток наугад: широкая похожесть может принести в топ нерелевантные товары.
Когда что выбирать
- Небольшой внутренний каталог и проверка гипотезы — начните с простого запроса и измерьте задержку на реалистичном наборе.
- Постоянный поиск по названию и описанию — храните взвешенный вектор и добавьте GIN, если план и тесты показывают пользу.
- Частые опечатки в пользовательских запросах — отдельно проверьте
pg_trgmи качество подсказок; не подменяйте им поиск по смысловым формам слова.
Термин: tsvector — тип PostgreSQL для представления документа как набора нормализованных лексем с позициями и дополнительными метками. По этому представлению база сопоставляет поисковый запрос и рассчитывает релевантность.
Что делать дальше
Начните не с тонкой настройки рейтинга, а с журнала поисковых запросов и списка ожидаемых результатов. Затем внедрите один вариант на копии данных, сравните план выполнения, задержку поиска и цену обновлений. После запуска следите за долей запросов без результатов и временем ответа: именно они подскажут, нужна ли следующая итерация с синонимами или обработкой опечаток.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: PostgreSQL 16: полнотекстовый поиск, PostgreSQL 16: расширение pg_trgm
Частые вопросы
Подойдёт ли полнотекстовый поиск PostgreSQL для русского языка?
Почему GIN-индекс не используется в плане запроса?
Будет ли PostgreSQL находить товары с опечатками?
Что делать, если у меня другая версия PostgreSQL?
Не станет ли поиск медленнее после добавления индекса?
Нужно ли индексировать каждое текстовое поле товара?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.