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.

Sources

Related topics