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

«Нужен поиск — ставим Elasticsearch» — рефлекс, который стоит второго stateful-кластера и пайплайна синхронизации. У PostgreSQL есть полнотекстовый поиск (tsvector/tsquery), триграммы (pg_trgm) и этого хватает большему числу проектов, чем принято думать. Но у PG-поиска есть честный потолок — и упереться в него посреди роста хуже, чем спланировать переход.

Что умеет сам PostgreSQL

Чтобы развилка была честной, сначала — возможности встроенного:

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;

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;

Морфология (стемминг русского из коробки), веса полей, ранжирование ts_rank, поисковый синтаксис websearch_to_tsquery. Плюс pg_trgm — опечатки и поиск по подстроке с GIN-индексом вместо мёртвого ILIKE '%...%'. Всё это — внутри транзакции, без синхронизации, без второго кластера.

Восемь критериев

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

PG хватает, если результат — «найти все подходящие и отсортировать по дате/цене/весу поля». ts_rank примитивен, но для каталога с фильтрами этого достаточно.

ES, если релевантность — продукт: BM25, boosting по свежести и популярности, function score, подмешивание бизнес-сигналов. Тюнинг выдачи — ежедневная работа, а не разовая настройка.

2. Фасеты и агрегации по выдаче

PG: фасетов нет или они считаются отдельными запросами по фильтрам — терпимо при простых каталогах.

ES: фасеты по категориям/брендам/цене с подсчётом в один запрос — родной жанр aggregations (разбор).

3. Опечатки, синонимы, автодополнение

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

ES: fuzzy, suggesters, synonym graph, переиндексация со сменой анализатора — поисковая лингвистика как управляемая система.

4. Объём и нагрузка

PG: миллионы документов и десятки поисковых запросов в секунду GIN-индекс держит уверенно.

ES: десятки миллионов документов, сотни RPS, требования к латентности выдачи — горизонтальное масштабирование шардами.

5. Свежесть индекса

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

ES: между записью в PG и видимостью в поиске — пайплайн и refresh interval, секунды. Для каталога — норма; для «создал и сразу ищу» — источник багов и костылей.

6. Сложность запросов к поиску

PG: поиск — один из фильтров в обычном SQL (WHERE search_vector @@ query AND price < 1000 AND store_id = 5) — и он комбинируется с JOIN-ами бесплатно.

ES: весь запрос живёт в Query DSL; зато multi-field, nested, highlight, «more like this».

7. Эксплуатация

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

ES: кластер, шарды, ILM, snapshots, мониторинг (operations) + пайплайн синхронизации (CDC/события) со своим мониторингом.

8. Команда и горизонт

PG: поиск — вспомогательная функция продукта, выделенного владельца не будет.

ES: поиск — ядро продукта (маркетплейс, контент-платформа), и у него будет хозяин, который тюнит выдачу.

Чек-лист «возьми балл»

Балл Elasticsearch за каждое «да»:

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

0–2 — PG FTS + pg_trgm, и не забудьте GENERATED-колонку с GIN. 3–4 — начинайте с PG, проектируйте модель так, чтобы поиск было легко вынести (поисковые поля и логика индексации — в одном месте). 5+ — Elasticsearch, и сразу с честным пайплайном (Spring Data ES + CDC).

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

  • ES ради LIKE. Кластер Elasticsearch ради поиска по названию в админке на 100 тысяч строк. pg_trgm решает это одним индексом.
  • ILIKE '%query%' по миллионам строк без индекса. Обратная ошибка: seq scan на каждый поиск, «PG не умеет искать». Умеет — GIN по tsvector или триграммам.
  • Dual write в ES из Handler-а. Поиск разъезжается с базой при первом сбое; синхронизация — только пайплайном (CDC или события), с переиндексацией как штатной операцией.
  • ES как источник правды. Документы живут только в ES, PG «для транзакций». При смене mapping или потере кластера данные не из чего пересобрать. ES — производная, пересоздаваемая из PG.
  • Игнорировать согласованность. «Создал товар — в поиске нет» без объяснения в UX и контракте API. Лаг — свойство архитектуры, его декларируют, а не скрывают.

Когда оба — нормально

Зрелое состояние крупного каталога: PG — источник правды и точные фильтры, ES — полнотекст, релевантность и фасеты, между ними пайплайн. Поиск из ES возвращает id, карточки добираются из PG — каждый делает то, что умеет лучше.

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

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