Опирается на правила: PG-FTS-001PG-FTS-094 из PostgreSQL Style Guide → раздел Полнотекстовый поиск.

Важно знать

  • PG FTS закрывает 90% задач «поиск по статьям/товарам/контенту» без Elasticsearch.
  • Хватает: ≤ 10M документов, ≤ 100 RPS, базовое ranking.
  • Не хватает: ≫ 10M + ranking, fuzzy с typo tolerance, multi-language авто, aggregations.
  • tsvector + tsquery + @@ оператор.
  • Generated column tsvector ... STORED + GIN-индекс.
  • russian конфигурация для стемминга.
  • websearch_to_tsquery (PG11+) — Google-style с -, кавычками.
  • setweight A/B/C/D для важности полей.
  • ts_headline — подсветка совпадений.
  • pg_trgm дополняет — fuzzy + LIKE '%substring%'.

PostgreSQL FTS из коробки решает большинство search-задач без отдельного Elasticsearch.

Когда хватает PG FTS

PG-FTS-001..002:

Хватает:

  • Корпоративный/админский поиск.
  • E-commerce среднего масштаба.
  • Поиск по комментариям, тикетам, документации.
  • ≤ 10M документов, ≤ 100 RPS.

Не хватает (→ Elasticsearch):

  • ≫ 10M с ranking-нагрузкой.
  • Сложный fuzzy с typo tolerance.
  • Multi-language с автоопределением.
  • Aggregations + facets + analytics.

Базовые типы

PG-FTS-010..011:

SELECT to_tsvector('russian', 'Покупатели выбирают товары в каталоге');
-- 'выбира':2 'каталог':5 'покупател':1 'товар':3

SELECT to_tsquery('russian', 'покупатель & каталог');
-- 'покупател' & 'каталог'

SELECT to_tsvector('russian', '...') @@ to_tsquery('russian', 'покупатель & каталог');
-- t (true)
  • tsvector — токенизированный документ.
  • tsquery — запрос.
  • @@ — оператор поиска.

Хранение tsvector

PG-FTS-020..022:

Generated column (PG12+, рекомендуется)

CREATE TABLE article (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title       text NOT NULL,
    body        text NOT NULL,
    search_doc  tsvector GENERATED ALWAYS AS (
        setweight(to_tsvector('russian', coalesce(title, '')), 'A') ||
        setweight(to_tsvector('russian', coalesce(body, '')), 'B')
    ) STORED
);

CREATE INDEX ix_article_search_doc ON article USING gin (search_doc);

setweight('A') для важных полей (title), 'B'/'C'/'D' для менее важных.

PG автоматически пересчитывает search_doc при каждом INSERT/UPDATE.

Триггер (PG <12)

CREATE TRIGGER article_search_update
BEFORE INSERT OR UPDATE ON article
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_doc, 'pg_catalog.russian', title, body);

Не вычислять на лету — без stored column каждый запрос будет to_tsvector(...) @@ ... без индекса.

Запросы

PG-FTS-030..033:

SELECT id, title, ts_rank(search_doc, q) AS rank
FROM article, to_tsquery('russian', 'покупатель & товар') q
WHERE search_doc @@ q
ORDER BY rank DESC
LIMIT 20;

plainto_tsquery — для user input

WHERE search_doc @@ plainto_tsquery('russian', $1);
-- 'купить новый товар' → 'купи' & 'новый' & 'товар'

Безопаснее — экранирует синтаксис.

websearch_to_tsquery — Google-style

WHERE search_doc @@ websearch_to_tsquery('russian', $1);
-- 'покупатель -ребёнок "новый каталог"'
-- → minus, фразы в кавычках

Ranking

  • ts_rank — стандарт.
  • ts_rank_cd — cover density (учитывает близость слов).

Веса через setweight обычно достаточны.

Подсветка ts_headline

PG-FTS-040:

SELECT
    id,
    title,
    ts_headline('russian', body, q,
        'StartSel=<mark>, StopSel=</mark>, MaxFragments=2, MaxWords=20')
        AS snippet
FROM article, websearch_to_tsquery('russian', $1) q
WHERE search_doc @@ q
ORDER BY ts_rank(search_doc, q) DESC
LIMIT 20;

Конфигурации

PG-FTS-050..052:

SELECT cfgname FROM pg_ts_config;
-- simple, english, russian, german, ...
  • simple — без стемминга, только lowercase.
  • russian — стемминг по русскому языку («покупатель» и «покупателя» = один токен).

Кастомная конфигурация с синонимами:

CREATE TEXT SEARCH DICTIONARY my_synonyms (
    template = synonym,
    synonyms = 'my_synonyms'
);

CREATE TEXT SEARCH CONFIGURATION ru_extended (COPY = russian);
ALTER TEXT SEARCH CONFIGURATION ru_extended
    ALTER MAPPING FOR word, asciiword
    WITH my_synonyms, russian_stem;

В $SHAREDIR/tsearch_data/my_synonyms.syn:

postgresql postgres
postgres postgres pg

GIN vs GiST

PG-FTS-060..061:

GINGiST
Размербольшеменьше
Buildмедленнеебыстрее
Searchбыстреемедленнее
Updateмедленнеебыстрее
Lossyнетда (false positive)

В 95% случаев — GIN.

fastupdate — буфер pending-list ускоряет вставки. Default on. Для read-heavy с редкими вставками:

ALTER INDEX ix_article_search_doc SET (fastupdate = off);

pg_trgm — fuzzy

PG-FTS-070..071:

CREATE EXTENSION pg_trgm;

CREATE INDEX ix_customer_name_trgm
    ON customer USING gin (full_name gin_trgm_ops);

-- LIKE '%substring%'
SELECT * FROM customer WHERE full_name ILIKE '%иван%';

-- fuzzy с опечатками
SELECT * FROM customer WHERE similarity(full_name, 'иванв') > 0.4
ORDER BY similarity(full_name, 'иванв') DESC LIMIT 10;

Комбинируй: FTS для длинного контента (тело статьи), pg_trgm для коротких полей с опечатками (имя автора, бренд).

Пагинация

PG-FTS-080..081:

OFFSET + LIMIT страдает на глубоких страницах — PG ranks все, потом дропает offset.

Keyset pagination по (rank, id):

SELECT id, title, ts_rank(search_doc, q) AS rank
FROM article, websearch_to_tsquery('russian', $1) q
WHERE search_doc @@ q
  AND (ts_rank(search_doc, q), id) < ($prev_rank, $prev_id)
ORDER BY rank DESC, id DESC
LIMIT 20;

Не использовать ts_rank в WHERE — считается на каждом совпадении.

Что запрещено

АнтипаттернПравилоЧто взамен
to_tsvector(body) в WHERE без stored columnPG-FTS-090generated column + GIN
simple для русского текстаPG-FTS-091russian со стеммингом
Без setweightPG-FTS-092A/B/C/D приоритеты
FTS для ≫ 10M документов с rankingsPG-FTS-093Elasticsearch
pg_trgm на полях > 100 символовPG-FTS-094FTS
GiST для read-heavyPG-FTS-060GIN
to_tsquery для user inputPG-FTS-031plainto_tsquery
OFFSET для глубокой пагинацииPG-FTS-080keyset

Куда дальше

  • PG → FTS — нормативные формулировки.
  • Типы индексов — GIN, GiST детали.
  • JSONB — GIN на jsonb параллельно.
  • Расширения — pg_trgm установка.
  • Materialized views — pre-computed search-индекс.
  • Антипаттерны типов — сводка.