← назад к разделу

Когда говорят «нам нужен поиск», первая мысль — Elasticsearch. Но это второй кластер, отдельный пайплайн синхронизации данных и новые точки отказа. У PostgreSQL есть встроенный полнотекстовый поиск, и он закрывает большинство задач. Разберём, что когда брать.

Как PostgreSQL ищет по тексту

Раньше поиск по тексту делали через LIKE '%запрос%'. Проблема: база вынуждена просматривать каждую строку целиком — на больших таблицах это медленно, и индексы здесь не помогают.

PostgreSQL решает это иначе. Специальный тип tsvector хранит текст в разобранном виде — список корневых форм слов с их весами и позициями. Например, строка «красный диван угловой» превращается в словарный набор со стеммингом: поиск по «диванов» найдёт «диван».

-- Добавляем поисковую колонку, которая обновляется автоматически
ALTER TABLE product ADD COLUMN search_vector tsvector
    GENERATED ALWAYS AS (
        setweight(to_tsvector('russian', coalesce(title, '')), 'A') ||
        setweight(to_tsvector('russian', coalesce(description, '')), 'B')
    ) STORED;

-- Создаём GIN-индекс — он работает как словарь, поиск O(log N)
CREATE INDEX product_search_idx ON product USING GIN (search_vector);

-- Ищем и сортируем по релевантности
SELECT id, title, ts_rank(search_vector, query) AS rank
FROM product, websearch_to_tsquery('russian', 'красный диван угловой') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;

GIN-индекс хранит обратный словарь: слово → список строк, где оно встречается. Вместо просмотра всей таблицы база сразу идёт к нужным строкам. Миллионы строк — десятки миллисекунд.

Вдобавок расширение pg_trgm умеет искать с опечатками и по подстрокам через триграммы — без полного сканирования таблицы.

Что такое Elasticsearch

Elasticsearch — отдельный сервис, специализированный на поиске. В основе тот же принцип обратного индекса, но возможности шире: алгоритм ранжирования BM25 (учитывает частоту слова в документе и редкость слова в коллекции), фасеты (подсчёт результатов по категориям прямо в поисковом запросе), развитая работа с синонимами, автодополнение, многоязычные анализаторы.

За это платят: Elasticsearch — отдельный кластер со своей эксплуатацией, и данные в нём всегда вторичны относительно PostgreSQL — нужен пайплайн синхронизации.

По каким критериям выбирать

Качество ранжирования

PostgreSQL ранжирует по ts_rank — простое число на основе частоты слов и весов полей. Для каталога с фильтрами обычно достаточно.

Elasticsearch использует BM25 и позволяет подмешивать бизнес-сигналы: понижать старые товары, поднимать популярные, применять разные веса по контексту запроса. Если релевантность выдачи — это продуктовая метрика, которую регулярно улучшают, PostgreSQL не хватит.

Фасеты

Фасеты — это счётчики рядом с фильтрами: «Ноутбуки (234)», «Смартфоны (87)». В PostgreSQL их считают отдельными запросами. В Elasticsearch агрегации возвращают фасеты вместе с результатами в одном запросе — это родной жанр.

Опечатки и автодополнение

pg_trgm закрывает опечатки и префиксный поиск. Словарь синонимов в PostgreSQL подключается, но управлять им неудобно.

Elasticsearch предлагает ready-made: fuzzy-поиск, suggesters для автодополнения, граф синонимов, переиндексацию с другим анализатором без простоя.

Объём данных и нагрузка

GIN-индекс уверенно держит миллионы документов и десятки поисковых запросов в секунду — для большинства продуктов этого достаточно.

Elasticsearch горизонтально масштабируется шардами: десятки миллионов документов, сотни запросов в секунду, требования к задержке выдачи менее 50 мс.

Актуальность данных в поиске

В PostgreSQL поисковый индекс обновляется в той же транзакции: создали товар — сразу видно в поиске. Это сильный аргумент в пользу PostgreSQL там, где важна согласованность.

В Elasticsearch между записью и видимостью в поиске — пайплайн и интервал обновления (refresh interval), обычно секунда. Для каталога это нормально. Для сценария «создал и сразу ищу» — источник проблем.

Комбинирование с обычными фильтрами

PostgreSQL: поиск — просто ещё один фильтр в SQL.

WHERE search_vector @@ query
  AND price < 1000
  AND category_id = 5
  AND in_stock = true

JOIN-ы, вложенные запросы, оконные функции — всё работает привычно.

В Elasticsearch весь запрос описывается в Query DSL — отдельном JSON-формате. Зато есть highlight (подсветка найденных слов), nested-документы и «похожие документы».

Сложность эксплуатации

PostgreSQL: поисковый индекс бэкапится вместе с базой, новых компонентов ноль.

Elasticsearch: кластер с шардами, управление жизненным циклом индексов, снапшоты, мониторинг — плюс отдельный пайплайн синхронизации данных из PostgreSQL (через CDC или события) со своим мониторингом.

Чек-лист: когда Elasticsearch оправдан

Прибавляйте балл за каждое «да»:

  1. Релевантность выдачи — продуктовая метрика, её будут регулярно улучшать.
  2. Нужны фасеты с подсчётами при каждом поиске.
  3. Синонимы, автодополнение, исправление опечаток — требования сейчас, не «потом».
  4. Больше десяти миллионов документов или больше пятидесяти поисковых запросов в секунду.
  5. Допустим лаг индексации в секунды.
  6. Пайплайн CDC или событий уже есть или запланирован.
  7. У поиска будет выделенный владелец.

0–2 балла — PostgreSQL FTS + pg_trgm. Не забудьте GENERATED-колонку и GIN-индекс.

3–4 балла — начинайте с PostgreSQL, но держите поисковую логику в одном месте, чтобы было удобно вынести позже.

5+ баллов — Elasticsearch, и сразу с нормальным пайплайном синхронизации: CDC или доменные события, переиндексация как штатная операция.

Типичные ошибки

Elasticsearch ради LIKE. Поднять кластер Elasticsearch ради поиска по названию в административной панели на сто тысяч строк. pg_trgm с GIN-индексом решает это без дополнительных компонентов.

ILIKE '%запрос%' без индекса. Противоположная ошибка: последовательное сканирование на каждый поиск и вывод «PostgreSQL не умеет искать». Умеет — нужен GIN-индекс по tsvector или триграммам.

Запись в Elasticsearch прямо из обработчика команды. При первом сбое поиск разъезжается с базой. Синхронизация — только через пайплайн (CDC или события) с переиндексацией как штатной операцией.

Elasticsearch как источник правды. Документы живут только в Elasticsearch, PostgreSQL «для транзакций». При смене структуры индекса или потере кластера данные не из чего восстановить. Elasticsearch — производная, которую можно пересоздать из PostgreSQL.

Игнорировать лаг индексации. Лаг — свойство архитектуры, его нужно декларировать в API-контракте и объяснять в интерфейсе, а не скрывать.

Когда используют оба вместе

Зрелый крупный каталог: PostgreSQL — источник правды и точные фильтры (цена, наличие, категория), Elasticsearch — полнотекстовый поиск, ранжирование и фасеты. Поиск из Elasticsearch возвращает идентификаторы, карточки подгружаются из PostgreSQL. Каждый инструмент делает то, что умеет лучше.

Коротко

  • PostgreSQL FTS: tsvector + GIN-индекс + pg_trgm — встроено, транзакционно, без новых компонентов.
  • Поиск в PostgreSQL обновляется в той же транзакции — данные всегда согласованы.
  • GIN-индекс держит миллионы строк и десятки запросов в секунду.
  • Elasticsearch нужен, когда релевантность — продуктовая метрика, нужны фасеты и автодополнение, данных десятки миллионов или нагрузка сотни запросов в секунду.
  • Elasticsearch — всегда производная от PostgreSQL, пересоздаваемая через пайплайн.
  • Запись в Elasticsearch напрямую из обработчика команды — антипаттерн: синхронизация только через CDC или события.
  • ILIKE '%...%' без индекса — частая причина медленного поиска; замена — GIN по tsvector или триграммам.

Что почитать дальше

  • Elasticsearch Fundamentals — как устроен обратный индекс и за что платят кластером.
  • Query DSL и relevance — BM25 и фасеты на практике.
  • PostgreSQL или ClickHouse — параллельная развилка для аналитической нагрузки.
  • Распределённые паттерны — синхронизация двух хранилищ без двойной записи.