Когда говорят «нам нужен поиск», первая мысль — 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 оправдан
Прибавляйте балл за каждое «да»:
- Релевантность выдачи — продуктовая метрика, её будут регулярно улучшать.
- Нужны фасеты с подсчётами при каждом поиске.
- Синонимы, автодополнение, исправление опечаток — требования сейчас, не «потом».
- Больше десяти миллионов документов или больше пятидесяти поисковых запросов в секунду.
- Допустим лаг индексации в секунды.
- Пайплайн CDC или событий уже есть или запланирован.
- У поиска будет выделенный владелец.
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 — параллельная развилка для аналитической нагрузки.
- Распределённые паттерны — синхронизация двух хранилищ без двойной записи.