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

Полнотекстовый поиск в PostgreSQL: нужен ли Elasticsearch

Практика3 мин

Коротко

Большинству сайтов хватает PostgreSQL: tsvector с GIN-индексом даёт релевантный поиск по словам, pg_trgm прощает опечатки, а Elasticsearch окупается только при высокой нагрузке, сложном ранжировании или богатом фасетном поиске.

Короткий ответ

На старте Elasticsearch, скорее всего, не нужен. В PostgreSQL есть встроенный полнотекстовый поиск: он разбивает текст на нормализованные слова (tsvector), сопоставляет их с запросом (tsquery), ранжирует результаты и работает быстро благодаря GIN-индексу. Расширение pg_trgm добавляет нечёткое сравнение для опечаток и частей слов. Вместе они закрывают большинство каталогов, блогов, баз знаний и админок — без второй системы, которую надо синхронизировать.

Шаг 1. Колонка для поиска

Генерируемая колонка обновляет поисковый вектор автоматически. Веса поднимают совпадения в заголовке выше совпадений в тексте.

ALTER TABLE articles
ADD COLUMN search tsvector
GENERATED ALWAYS AS (
  setweight(to_tsvector('russian', coalesce(title, '')), 'A') ||
  setweight(to_tsvector('russian', coalesce(body, '')), 'B')
) STORED;

CREATE INDEX articles_search_idx ON articles USING GIN (search);

'russian' — встроенная конфигурация со стеммингом: «статьи» находит «статья». Для английского — 'english'.

Для узбекского встроенной конфигурации нет. Используйте 'simple' (нижний регистр без стемминга) и удаляйте варианты апострофа, чтобы o‘zbek, o'zbek и ozbek совпадали:

to_tsvector('simple', regexp_replace(coalesce(title_uz, ''), '[‘’ʻʼ`'']', '', 'g'))

Ту же нормализацию применяйте к запросу пользователя.

Шаг 2. Запрос и ранжирование

websearch_to_tsquery понимает то, что пользователи реально вводят: обычные слова, фразы в кавычках и - для исключения слова.

SELECT id, title,
       ts_rank(search, q) AS rank,
       ts_headline('russian', body, q, 'MaxFragments=2') AS snippet
FROM articles,
     websearch_to_tsquery('russian', 'настройка сервера') AS q
WHERE search @@ q
ORDER BY rank DESC
LIMIT 20;
  • @@ отбирает совпавшие строки и использует GIN-индекс.
  • ts_rank сортирует по релевантности с учётом весов.
  • ts_headline строит фрагмент с подсветкой слов. Он относительно дорогой, поэтому вызывайте его только для строк, которые показываете.

Для поиска по префиксу, как в подсказках при вводе, используйте to_tsquery('russian', 'настр:*').

Шаг 3. Опечатки и частичные совпадения через pg_trgm

Полнотекстовый поиск сравнивает целые нормализованные слова, поэтому «postgers» не найдёт «postgres». Триграммы решают это, сравнивая фрагменты из трёх букв.

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX articles_title_trgm_idx ON articles USING GIN (title gin_trgm_ops);

SELECT id, title, similarity(title, 'postgers') AS sim
FROM articles
WHERE title % 'postgers'
ORDER BY sim DESC
LIMIT 10;
  • % возвращает строки выше порога похожести (pg_trgm.similarity_threshold).
  • Тот же индекс ускоряет запросы ILIKE '%текст%'.
  • Для длинных полей word_similarity и оператор <% сравнивают запрос с наиболее похожим фрагментом текста.

Частая связка: сначала полнотекстовый поиск, а если он ничего не нашёл — триграммный поиск по заголовкам с подсказкой «Возможно, вы имели в виду».

Частые ошибки

  • Вызов to_tsvector в WHERE без подходящего индекса — каждый запрос превращается в полный перебор.
  • Разные конфигурации поиска при индексации и при запросе.
  • ILIKE '%слово%' на больших таблицах без триграммного индекса.
  • Забытый coalesce: одно поле NULL превращает весь склеенный вектор в NULL.

Когда отдельный поисковый движок оправдан

ЗадачаPostgreSQLElasticsearch или OpenSearch
Поиск по словам со стеммингомДаДа
Устойчивость к опечаткамpg_trgmВстроено, гибко
Ранжированиеts_rank, прощеРелевантность BM25, бусты, function score
Фасеты по многим полям на больших данныхВозможно, но тяжелоСильная сторона
Автодополнение под высокой нагрузкойВозможноСпециальные типы полей
Синонимы, много языковСловари, больше ручной работыБогатая цепочка анализаторов
Стоимость эксплуатацииНичего дополнительноОтдельный кластер и синхронизация

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

Частые вопросы

Да. Держите отдельную колонку tsvector (или отдельный индекс по выражению) для каждого языка со своей конфигурацией и выбирайте колонку по языку интерфейса пользователя.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.