DanLevy.net

Руководство по текстовому поиску в Postgres 2026

Инструменты поиска, которые уже есть в вашей базе данных, и когда каждый из них окупается.

Большинство команд используют один инструмент поиска в Postgres. Команды, которые знают все три, выпускают лучший поиск с меньшей сложностью — и избегают дорогого обходного пути к выделенному поисковому сервису, который им ещё не нужен.

Это руководство охватывает полный набор нативных для Postgres вариантов: что делает каждый, когда он подходит, и как их комбинировать.


Три инструмента

Полнотекстовый поиск (tsvector / GIN-индекс) — лексический. Он разбивает текст на лексемы, стеммирует их и сопоставляет запросы с индексом. «Running» и «runs» сводятся к одной лексеме. То же самое с «dog» и «dogs». Функция ранжирования (ts_rank) учитывает частоту появления терминов запроса и их заметность.

Триграммы (pg_trgm) разбивают строки на перекрывающиеся трёхсимвольные срезы и измеряют, сколько срезов две строки имеют общих. «Dan» → " da", "dan", "an ". «Micheal» и «Michael» имеют большинство общих триграмм, поэтому сходство высокое. Это делает pg_trgm отличным инструментом для нечёткого поиска имён, устойчивости к опечаткам и автодополнения — той области, где FTS работает плохо.

Индексы точного совпадения (B-tree, hash) обрабатывают первичные ключи, адреса электронной почты, идентификаторы, артикулы и всё, где ответ бинарный: совпадает или нет. Это не ощущается как «поиск», но относится к разговору, потому что худший паттерн — использовать нечёткий или семантический поиск для задач, у которых есть правильные ответы.

Выбор не о продвинутости. Он о том, чтобы сопоставить инструмент с формой запроса.

Карта инструментов поиска PostgresСравнение pg_trgm, полнотекстового поиска, pgvector и гибридного поиска по форме ввода и намерению запроса.Выбирайте поисковый примитив по форме вводаОдна таблица Postgres может поддерживать все четыре. Трюк — сопоставить запрос с текстом.Важны точные словаВажен смыслКороткий / структурированный текстДлинная проза / блокинечёткийpg_trgmИмена, адреса, заголовки, опечатки,автодополнение, частичные строки.Орфографическое сходство: расстояние в написании.похожийpgvectorПохожие элементы, дубликаты тикетов,рекомендации по коротким описаниям.Сходство эмбеддингов: расстояние по смыслу.лексическийПолнотекстовый поискСтатьи, документация, логи, контент поддержки,где должны появляться слова запроса.Лексемы, стемминг, ранжирование, булевы фильтры.гибридныйFTS + pgvectorТехническая документация и RAG, где пользователизадают концептуальные вопросы плюс точные символы.Запустите оба, объедините ранги через RRF.Начните с намерения запроса, затем проверьте форму текста
Четыре поисковых примитива Postgres, сопоставленных по намерению запроса (точный против семантического) и форме текста (структурированный против прозы). Одна таблица может нести все четыре индекса — выбор делается для каждого запроса, а не для таблицы.

Когда побеждает полнотекстовый поиск

Поиск ключевых слов в прозе. Блог-посты, документация, описания продуктов, тикеты поддержки, юридические документы. FTS создан именно для такого типа контента: индексированная, ранжированная выборка по тексту на естественном языке.

Пользовательские запросы на основе ключевых слов. Пользователи вводят поисковый термин, фильтруют по тегу или просматривают по ключевому слову. FTS обрабатывает это намерение нативно, без какой-либо инфраструктуры эмбеддингов.

Ранжированные результаты без внешних зависимостей. Индексы FTS быстры, детерминированы и не требуют вызовов API. Сигнал релевантности исходит из частоты терминов, взвешенной по позиции в поле.

Булева фильтрация вместе с поиском. FTS естественно комбинируется с вашей существующей логикой запросов:

SELECT * FROM posts
WHERE search_vector @@ to_tsquery('english', 'postgres & performance')
AND category = 'tutorial'
AND published_at > NOW() - INTERVAL '6 months';

Настройка FTS

-- Генерируемый столбец автоматически поддерживает индекс в актуальном состоянии
ALTER TABLE posts ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(body, '')), 'B')
) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- Запрос
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('english', 'postgres & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;

setweight присваивает важность: A (заголовок) ранжируется выше B (тело). Это вся модель релевантности для большинства сценариев поиска по контенту.

Что FTS обрабатывает плохо


Когда побеждают триграммы (pg_trgm)

pg_trgm охватывает неловкую середину, с которой FTS постоянно спотыкается.

FTS разбивает текст на лексемы и стеммирует их. Для прозы это правильно. Для имён и коротких идентификаторов — часто нет:

pg_trgm также не зависит от языка, что важно для имён из разных лингвистических групп. FTS требует настройки словаря для каждого языка.

Нечёткий поиск имён

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm_idx ON users USING GIN (name gin_trgm_ops);
-- Находит «Micheal Jordan» при поиске «Michael Jordan»
SELECT id, name, similarity(name, $1) AS score
FROM users
WHERE name % $1 -- оператор % = порог сходства (по умолчанию 0.3)
ORDER BY score DESC
LIMIT 10;

Оператор % использует pg_trgm.similarity_threshold (по умолчанию 0.3, диапазон 0–1). Для поиска имён 0.3–0.4 ловит опечатки, сохраняя низкий уровень шума.

Автодополнение, префиксный поиск и поиск по содержимому

-- Префиксное совпадение для автодополнения. Триграммный GIN-индекс может помочь,
-- но B-tree индекс по паттерну может быть лучше для чисто лево-якорных префиксов.
SELECT name FROM users
WHERE name ILIKE $1 || '%'
ORDER BY name
LIMIT 10;
-- word_similarity для частичных совпадений внутри более длинных строк
-- («Johnson» внутри «Andrew Johnson III»)
SELECT id, name, word_similarity($1, name) AS score
FROM users
WHERE $1 <% name
ORDER BY score DESC
LIMIT 10;

Триграммный GIN-индекс особенно полезен для запросов на содержание ILIKE '%pattern%' и устойчивого к опечаткам сопоставления — паттернов, которые без триграммного индекса обычно приводят к полному сканированию таблицы.

Когда обращаться к pg_trgm вместо FTS

СценарийИспользуйте
Поиск имён людей/компаний с опечаткамиpg_trgm
Автодополнение / префиксный поискpg_trgm (или FTS с префиксными запросами)
Короткие строки, идентификаторы, кодыpg_trgm
Проза-статьи, документация, тикетыFTS
Лог-сообщения для ключевых словFTS
Многоязычный поиск имёнpg_trgm (не зависит от языка)

Когда побеждает точное совпадение в SQL

Некоторые задачи «поиска» — это вообще не поиск.

«Найти пользователя с email dan@example.com» — это проверка на равенство. «Найти заказ ORD-12345» — это поиск по первичному ключу. «Список постов в категории tutorial, отсортированный по дате» — это фильтрованный запрос. Всё это относится к B-tree или hash-индексам.

Использование FTS или триграмм здесь добавляет сложность без улучшения корректности — а для точных идентификаторов приближённое совпадение хуже, чем его отсутствие.

CREATE INDEX users_email_idx ON users (email);
-- Точный поиск: быстрый и однозначный
SELECT id, name FROM users WHERE email = $1;

Более широкий урок: приближённый поиск для задач с правильными ответами — это ошибка категории. Он вернёт что-то — что может быть уверенно неверным.


Комбинирование инструментов

Эти инструменты чисто комбинируются. Не обязательно выбирать ровно один.

FTS + pg_trgm для поисковой строки, устойчивой к опечаткам в ключевых словах:

-- Триграммное сходство по заголовку ловит опечатки; ts_rank обрабатывает релевантность тела
SELECT id, title,
ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank,
similarity(title, $1) AS trgm_score
FROM posts
WHERE search_vector @@ to_tsquery('simple', $1)
OR title % $1
ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESC
LIMIT 10;

FTS + unaccent для интернационального контента:

-- Убираем диакритические знаки, чтобы «José» совпадал с «Jose»
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE TEXT SEARCH CONFIGURATION public.simple_unaccent (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.simple_unaccent
ALTER MAPPING FOR hword, hword_part, word
WITH unaccent, simple;
ALTER TABLE posts ADD COLUMN search_vector tsvector;
CREATE TRIGGER posts_search_vector_refresh
BEFORE INSERT OR UPDATE OF title, body ON posts
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'public.simple_unaccent', title, body);

unaccent + pg_trgm для интернационального поиска имён:

ALTER TABLE users ADD COLUMN name_search text;
CREATE FUNCTION users_name_search_refresh()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.name_search := unaccent(coalesce(NEW.name, ''));
RETURN NEW;
END;
$$;
CREATE TRIGGER users_name_search_refresh
BEFORE INSERT OR UPDATE OF name ON users
FOR EACH ROW EXECUTE FUNCTION users_name_search_refresh();
CREATE INDEX users_name_search_trgm_idx
ON users USING GIN (name_search gin_trgm_ops);
SELECT id, name
FROM users
WHERE name_search % unaccent($1)
ORDER BY similarity(name_search, unaccent($1)) DESC
LIMIT 10;

Примеры с триггерами избегают использования unaccent() внутри выражений генерируемых столбцов или индексов, где важны правила неизменяемости PostgreSQL. Если вы обернёте unaccent() в собственную неизменяемую функцию, документируйте, что принимаете риск при обновлении/конфигурации.


Примечательные расширения

pg_trgm поставляется с большинством дистрибутивов Postgres, но требует явного включения. Основа нечёткого строкового поиска в Postgres.

unaccent убирает диакритические знаки перед индексированием и запросами. Хорошо сочетается как с pg_trgm, так и с FTS для контента на европейских языках. Поставляется с Postgres.

pg_bigm расширяет триграммный подход до биграмм (двухсимвольные срезы), что значительно улучшает результаты для языков CJK (китайский, японский, корейский), где pg_trgm работает хуже. Устанавливается отдельно; не входит в комплект.

pg_search (от ParadeDB) заменяет стандартный стек GIN / tsvector на BM25-индекс на базе Tantivy. Это даёт BM25-скоринг (часто лучше, чем ts_rank), нечёткое сопоставление внутри FTS-запросов, фасетный поиск и значительно более быстрое индексирование на больших таблицах. Это путь обновления без переделки, когда стандартный FTS начинает показывать ограничения ранжирования или производительности.

-- pg_search: BM25 полнотекстовый поиск с нечётким сопоставлением
CREATE INDEX posts_bm25_idx ON posts
USING bm25 (id, title, body)
WITH (key_field = 'id', text_fields = '{"title": {}, "body": {}}');
-- Запрос с BM25-скорингом + нечётким сопоставлением (ловит «javascipt»)
SELECT id, title, paradedb.score(id) AS rank
FROM posts
WHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')
ORDER BY rank DESC
LIMIT 10;

pgvector добавляет хранение плотных векторов и поиск по сходству. Это правильный инструмент, когда пользователи описывают то, что хотят, а не называют это — семантический поиск, RAG, рекомендации похожего контента, многоязычные запросы. Подробно рассмотрено в Семантический векторный поиск и гибридные стратегии.


Таблица решений

Что ищетеРекомендация
Проза-статьи, документация, тикетыFTS
Имена людей/компаний с опечаткамиpg_trgm
Автодополнение, префиксный поискpg_trgm
Короткие коды, идентификаторыpg_trgm
Лог-сообщения для ключевых словFTS
Международные именаpg_trgm + unaccent
Большой контент, лучшее ранжированиеpg_search (ParadeDB BM25)
Первичные ключи, точные email, IDB-tree индекс
Даты, диапазоны, отсортированные спискиB-tree индекс
Разрешения, категории, фильтрыОбычное предложение WHERE
Вопросы, парафразы, концепцииpgvector (см. следующую статью)

Если сомневаетесь: короткие строки с вариациями написания → триграммы. Длинная проза для запросов по ключевым словам → FTS. Структурированные идентификаторы → обычные индексы. Концептуальные или естественно-языковые запросы → pgvector.


Гибридный поиск: два сигнала, один ранг

Когда запрос вроде "withRetry timeout errors" попадает в поисковую строку, он несёт два вида намерения: точные имена символов, которые пользователь знает (withRetry), и концептуальное описание (timeout errors). Ни один примитив не покрывает оба. Запуск FTS и векторного поиска параллельно — а затем объединение их ранжированных списков через Reciprocal Rank Fusion — делает это.

RRF оценивает каждый результат как 1 / (60 + rank) в каждом списке и суммирует по спискам. Константа 60 смягчает преимущество верхних рангов, так что результат, занявший второе место в обоих списках, может победить результат, выигравший один список и полностью пропустивший другой. Критически важно: RRF никогда не усредняет сырые оценки по методам — ранг FTS и косинусное расстояние — разные валюты и не могут быть объединены арифметически.

Гибридный поиск с Reciprocal Rank FusionЗапрос расходится к полнотекстовому и векторному поиску, каждый выдаёт ранги, а Reciprocal Rank Fusion объединяет их в один список результатов.Гибридный поиск — это два честных сигнала, затем один объединённый рангНе усредняйте сырые оценки. Ранг FTS и косинусное расстояние — разные валюты.Запрос пользователя”withRetrytimeout errors”FTS / BM25Точные символы и слова1. Справочник API2. Гайд по retrypgvectorКонцептуальные соседи1. Сетевые сбои2. Гайд по retryОбъединение RRFКаждый результат получаеткредит за место в каждом списке.1 / (60 + rank)Итоговые результатыЛучший результат — где точные терминыи семантический смысл совпадают.
Запрос расходится к FTS и pgvector параллельно. Каждый выдаёт свой ранжированный список. RRF оценивает каждый документ по его позиции в каждом списке и суммирует оценки — результат выводит документы, с которыми согласны оба сигнала.
-- Гибридный поиск: FTS + pgvector, объединённые через RRF
WITH fts AS (
SELECT id, ts_rank(search_vector, query) AS score,
ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank
FROM docs, to_tsquery('english', 'withRetry & timeout') query
WHERE search_vector @@ query
LIMIT 60
),
vec AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY embedding <=> $embedding) AS rank
FROM docs
ORDER BY embedding <=> $embedding
LIMIT 60
)
SELECT COALESCE(fts.id, vec.id) AS id,
(COALESCE(1.0 / (60 + fts.rank), 0) +
COALESCE(1.0 / (60 + vec.rank), 0)) AS rrf_score
FROM fts FULL JOIN vec ON fts.id = vec.id
ORDER BY rrf_score DESC
LIMIT 10;

Пул из 60 документов-кандидатов на ветку (LIMIT 60) — распространённая стартовая точка. Расширьте, если полнота низкая; сузьте для скорости.


Что дальше

Текстовый поиск Postgres покрывает много задач, но у него есть потолок. Когда пользователи описывают то, что хотят, вместо того чтобы называть это — «что-то, чтобы спать в самолёте», «статьи об уверенности в отладке для нового инженера» — и лексический, и триграммный поиск оба проигрывают.

Это территория векторных эмбеддингов, семантического поиска и гибридных архитектур. Рассмотрено в Семантический векторный поиск и гибридные стратегии.