Полнотекстовый поиск в PostgreSQL: ошибки и решения
Поиск в PostgreSQL может молча пропускать нужные документы, тормозить на большой таблице или выдавать странный рейтинг. Разбираем типичные ошибки в конфигурации, запросах и индексах на примерах для PostgreSQL 16.

Полнотекстовый поиск в PostgreSQL часто выглядит рабочим, пока не начинает пропускать слова, сканировать таблицу целиком или ставить нерелевантные результаты выше точных совпадений. В PostgreSQL 16 большинство таких проблем связано не с «плохим поиском», а с несовпадением конфигурации, выражения индекса и запроса — или с ожиданиями, которых полнотекстовый поиск не обещает.
Сначала выберите, где хранить поисковый вектор
PostgreSQL превращает текст в tsvector — набор нормализованных лексем, по которому можно искать. Запрос преобразуется в tsquery. Оператор @@ проверяет, подходит ли документ запросу. Для русского текста важно передавать конфигурацию russian и при построении вектора, и при разборе запроса.
В PostgreSQL 16 есть три практичных варианта хранения вектора. Выбор влияет на сложность обновлений, размер таблицы и то, насколько легко случайно рассинхронизировать индекс с поисковым выражением.
| Подход | Когда подходит | Цена и риск |
|---|---|---|
| Индекс по выражению | Простая схема, вектор нужен только для поиска | Выражение в запросе должно совпадать с выражением индекса; длинное выражение легко случайно изменить |
| Сгенерированная колонка | Вектор полностью выводится из колонок той же строки | Занимает место в таблице; после изменения исходного текста PostgreSQL пересчитывает значение |
Обычная колонка tsvector | Нужно отдельно собирать или обновлять текст из нескольких таблиц | Приложение или триггер должны поддерживать данные в актуальном состоянии |
Для обычной таблицы статей удобна сгенерированная колонка. Предположим, в таблице articles уже есть поля title и body. Команды ниже рассчитаны на PostgreSQL 16:
ALTER TABLE articles
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector(
'russian'::regconfig,
coalesce(title, '') || ' ' || coalesce(body, '')
)
) STORED;
CREATE INDEX articles_search_vector_gin
ON articles USING GIN (search_vector);Здесь coalesce не даёт значению NULL обнулить склеенную строку. GIN — индекс для эффективного поиска по элементам tsvector; сам по себе он не сортирует документы по релевантности.
Типичные ошибки: от конфигурации до выдачи
1. Конфигурация текста не задана явно
Если вызвать функцию преобразования текста без имени конфигурации, результат зависит от настройки default_text_search_config в окружении базы. На одной базе поиск может разбирать русский текст одной конфигурацией, а на другой — другой. Тогда формы слов, стоп-слова и совпадения будут различаться.
Задавайте конфигурацию в обоих местах. Для русскоязычных запросов это, например, plainto_tsquery('russian', ...). Убедитесь, что конфигурация существует в вашей базе: доступные конфигурации можно посмотреть в системном представлении pg_ts_config.
2. Индекс и запрос используют разные выражения
Индекс по выражению не помогает, если условие поиска построено иначе. Например, индекс создан для объединения заголовка и текста с coalesce, а запрос применяет to_tsvector только к body. PostgreSQL не обязан считать эти выражения взаимозаменяемыми и может выбрать последовательное чтение таблицы.
Со сгенерированной колонкой условие проще и его легче не испортить:
SELECT id, title
FROM articles
WHERE search_vector @@ plainto_tsquery('russian', $1);В приложении передавайте текст запроса как параметр, а не склеивайте его с SQL-строкой. После создания индекса проверьте план через EXPLAIN на запросе, который возвращает достаточно мало строк. Наличие индекса в схеме ещё не означает, что планировщик выберет его: решение зависит от оценки количества совпадений и стоимости плана.
3. Пользовательский ввод передают в to_tsquery
to_tsquery ожидает выражение с синтаксисом поискового запроса. Обычная фраза пользователя может содержать символы, которые этот синтаксис трактует особым образом, и запрос завершится ошибкой. Для свободного ввода чаще подходят plainto_tsquery или websearch_to_tsquery. Последняя функция поддерживает привычные конструкции веб-поиска, включая кавычки и операторы, но не превращает ввод в произвольный SQL.
Выбирайте преобразователь по интерфейсу. Если в поле ожидается просто текст — используйте разбор обычного текста. Если интерфейс явно обещает синтаксис поиска — используйте веб-формат и объясните пользователю правила.
4. Ожидают совпадения подстроки и любой формы слова
Полнотекстовый поиск — не то же самое, что LIKE '%слово%'. Он разбирает текст на лексемы, применяет словарь и может нормализовать словоформы. Это помогает искать словоформы, но не гарантирует поиск по произвольной части слова, опечаткам или похожему написанию. Если продукт обещает такие возможности, одного tsvector может быть недостаточно: требования к поиску нужно отдельно проверить на реальных запросах и данных.
Не маскируйте проблему добавлением операторов к каждому запросу. Сначала составьте набор примеров: слово, словоформа, опечатка, часть слова, фраза. Для каждого зафиксируйте, что именно должно считаться совпадением.
5. Считают, что GIN решает сортировку и скорость целиком
GIN помогает отобрать совпадающие документы, но затем PostgreSQL может считать релевантность для результатов и сортировать их. На большой выборке вычисление рейтинга и сортировка тоже стоят ресурсов. Значения ts_rank — не вероятности и не универсальная оценка качества: их полезно настраивать под конкретный интерфейс, а не показывать как «процент совпадения».
Проверьте план и фактическое время на реалистичном наборе данных. В EXPLAIN ANALYZE смотрите, сколько строк прочитано и сколько осталось после фильтра, а также сколько времени заняли сортировка и вычисление рейтинга. Сравнивайте одинаковые запросы на одинаковых данных до и после изменения.
Как выбрать подход и не сломать поиск
Для статей, где заголовок и текст лежат в одной строке, обычно проще начать с сгенерированной колонки и GIN. Если поисковый текст собирается из связанных таблиц, сгенерированная колонка не сможет сама агрегировать их значения: понадобится отдельная логика обновления обычной колонки. Индекс по выражению разумен для компактной схемы, если команда готова поддерживать одинаковое выражение в DDL и запросах.
Перед выпуском проверьте следующие вещи:
- Конфигурация явно задана и одинакова при формировании документа и поискового запроса.
- Запрос соответствует смыслу поля ввода: обычный текст не отправляется в синтаксис
to_tsqueryбез подготовки. - Условия поиска используют ту колонку или то же выражение, для которого создан индекс.
- Проверены слова, формы слов, стоп-слова, пустой ввод и запрос без совпадений.
- План проверен на объёме, близком к рабочему, а не только на десятке тестовых строк.
Индекс ускоряет подходящий запрос, а не исправляет неверное понимание того, что пользователь считает совпадением.
Лексема — нормализованная единица текста, которую PostgreSQL хранит в tsvector после разбора слова и применения языковых правил. Поэтому поисковый вектор — не копия исходного текста: по нему нельзя судить, что именно увидит пользователь без проверки на примерах.
Что делать дальше
Соберите 20–30 характерных запросов из интерфейса и проверьте ожидаемые результаты на тестовой копии данных. Затем выполните EXPLAIN ANALYZE для частых и широких запросов, сравните план с индексом и без него и отдельно измерьте время сортировки по релевантности. Если поиск объединяет данные из разных таблиц или должен находить опечатки, зафиксируйте это как отдельное требование: обычный полнотекстовый индекс его автоматически не решит.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: PostgreSQL 16: Full Text Search, PostgreSQL 16: GIN Indexes
Частые вопросы
Почему PostgreSQL не находит нужное русское слово?
Почему созданный GIN-индекс не используется?
Что лучше: индекс по выражению или отдельная колонка tsvector?
Можно ли искать опечатки и части слова через полнотекстовый поиск?
Можно ли передавать пользовательский текст в to_tsquery?
Как понять, что поиск тормозит именно из-за сортировки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.