PostgreSQLMiddleTechnical
Что такое full-text search в Postgres (tsvector, tsquery, to_tsvector)? Когда хватает Postgres, а когда уходить в Elasticsearch?
tsvector хранит лексемы документа, tsquery — поисковый запрос; оператор @@ ищет совпадения через GIN-индекс. Postgres FTS хватает для простых задач до ~10M документов; Elasticsearch нужен для fuzzy, автодополнения, фасетов и горизонтального масштабирования.
Как устроен full-text search в PostgreSQL
PostgreSQL реализует FTS через два типа данных:
- tsvector — нормализованный документ: набор лексем с позициями. Хранится в столбце таблицы или вычисляется на лету.
- tsquery — поисковый запрос: термины с операторами
&(AND),|(OR),!(NOT),<->(фраза).
-- Базовый пример
SELECT to_tsvector('russian', 'PostgreSQL предоставляет мощный поиск по тексту');
-- 'мощн':4 'поиск':5 'предоставля':2 'текст':7
SELECT to_tsquery('russian', 'поиск & текст');
-- 'поиск' & 'текст'
-- Поиск с рангом
SELECT
id,
title,
ts_rank(search_vector, query) AS rank
FROM articles,
to_tsquery('russian', 'PostgreSQL & поиск') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
Хранение tsvector и индекс
-- Добавить вычисляемый столбец (PostgreSQL 12+)
ALTER TABLE articles
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('russian', coalesce(title, '')), 'A') ||
setweight(to_tsvector('russian', coalesce(body, '')), 'B')
) STORED;
-- GIN-индекс для @@ оператора
CREATE INDEX ON articles USING GIN(search_vector);
-- Поиск фразой (слова рядом)
SELECT title FROM articles
WHERE search_vector @@ to_tsquery('russian', 'машинное <-> обучение');
websearch_to_tsquery — удобный парсер пользовательского ввода
-- Принимает "обычный" поисковый запрос без спецсимволов
SELECT websearch_to_tsquery('russian', 'PostgreSQL full text search');
-- 'postgresql' & 'full' & 'text' & 'search'
-- С минусом для исключения
SELECT websearch_to_tsquery('russian', 'PostgreSQL -MySQL');
Когда PostgreSQL FTS достаточно
- Небольшие и средние объёмы (до ~10 млн документов) с умеренной нагрузкой на поиск.
- Поиск по строго определённым языкам с настроенным словарём (russian, english).
- Нет требований к подсказкам (autocomplete), опечаткам (fuzzy), фасетам по произвольным полям.
- Поиск нужен как дополнительная фича, а не основная — не хочется поднимать отдельный сервис.
- Нужна транзакционная согласованность: поиск по только что вставленной строке сразу после INSERT.
Когда уходить в Elasticsearch (или Meilisearch, Typesense)
- Fuzzy search / опечатки: Postgres не умеет искать «питон» по запросу «пиотн». Elasticsearch использует BM25 + Levenshtein из коробки.
- Автодополнение и suggester: edge n-gram / completion suggester в ES; в Postgres придётся делать pg_trgm + GIN, что хуже по качеству.
- Многоязычный поиск без явного указания языка: Postgres требует задать словарь; ES определяет язык автоматически.
- Фасетная навигация с агрегациями: счётчики по категориям, диапазоны цен — Postgres считает это полным seq scan.
- Масштаб > 50 млн документов с sub-100ms latency: ES горизонтально масштабируется, Postgres — нет.
- Relevance tuning: настройка весов полей, field boosting, синонимы, стоп-слова — в ES всё это конфигурируется без кода.
Подводные камни
- GENERATED ALWAYS AS STORED пересчитывается при каждом UPDATE строки — при массовых обновлениях это может быть дороже ожидаемого. Альтернатива: триггер или фоновая задача.
- GIN-индекс медленно строится при большом объёме данных и блокирует VACUUM на таблице. Используйте CREATE INDEX CONCURRENTLY.
- Слово «не» в русском тексте: стандартный словарь russian удаляет его как стоп-слово, что ломает поиск отрицаний. Нужно кастомизировать словарь.
- to_tsvector без явного языка использует default_text_search_config (обычно english) — русские слова не нормализуются, поиск не работает.
- ts_rank не учитывает свежесть документа — нужно комбинировать с датой вручную: rank * exp(-age_days / 30.0).
- Поиск по числам и кодам (артикулам, UUID) — FTS их разбивает по дефисам и точкам. Лучше использовать обычный LIKE или pg_trgm.
- Большой tsvector в STORED-колонке увеличивает размер таблицы и замедляет vacuum. Следите за pg_relation_size() до и после добавления колонки.
Common mistakes
- Искать через
ILIKE '%term%'на больших таблицах. - Не выбирать языковую конфигурацию.
- Уходить в Elasticsearch без требований.
What the interviewer is testing
- Просит объяснить tsvector/tsquery.
- Проверяет GIN index.
- Уточняет границу Postgres vs search engine.