Полнотекстовый поиск

Рейтинг: 61% · 6 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Полнотекстовый поиск

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск (вы здесь)
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 25. Полнотекстовый поиск

Обычный LIKE и оператор ILIKE годятся, чтобы найти подстроку, но они не понимают язык. Запрос ILIKE '%самолет%' не найдет строку со словом самолеты, а уж тем более со словом самолетов или самолетным, потому что для базы это просто разные наборы байт. Полнотекстовый поиск (full text search) в postgresql решает другую задачу: найти документы по смыслу слов, с учетом морфологии, без оглядки на падежи и окончания. В этом уроке разберем, как PostgreSQL превращает текст в нормализованные лексемы, как строится поисковый запрос, как ранжировать результаты по релевантности, как ускорить все это GIN-индексом и генерируемым столбцом, и чем добить опечатки через нечеткий поиск pg_trgm. Поиск по-русски в фокусе.

Изображение

Как это работает

В основе лежат два типа данных. Первый - tsvector: это документ, разобранный на лексемы (нормальные формы слов) с позициями. Второй - tsquery: это сам поисковый запрос, тоже в виде лексем, но связанных логическими операторами И, ИЛИ, НЕ. Поиск - это проверка оператором @@, попадает ли tsquery в tsvector.

Чтобы превратить сырой текст в tsvector, PostgreSQL прогоняет его через конфигурацию поиска. Конфигурация определяет, какой парсер режет текст на токены и какие словари приводят токены к нормальной форме. Для русского языка нужна конфигурация russian: она знает стоп-слова (и, в, на - их выкидывают, они не несут смысла) и подключает словарь снежного стеммера, который отрезает окончания. Поэтому самолет, самолеты, самолетам сворачиваются в одну лексему самолет, и все три формы становятся взаимозаменяемыми при поиске.

Функция to_tsvector(config, text) делает документ, а для запроса есть три входа. Функция to_tsquery принимает уже размеченный синтаксис с операторами и амперсандами, она строгая и кидает ошибку на кривой ввод. Функция plainto_tsquery берет простую фразу от пользователя и сама склеивает слова через И. Функция phraseto_tsquery дополнительно сохраняет порядок слов (оператор расстояния), то есть ищет фразу как последовательность. С PostgreSQL 11 есть еще websearch_to_tsquery - она понимает кавычки для фраз и минус для исключения, как в поисковой строке, и не падает на любом мусорном вводе, поэтому для пользовательского поля ввода берите именно ее.

Найденное надо отсортировать. Функция ts_rank считает релевантность по частоте и положению лексем, а ts_rank_cd учитывает плотность совпадений (cover density) - насколько кучно искомые слова стоят рядом. Это не абсолютная истина, а эвристика, но для сортировки выдачи ее обычно хватает.

SQL и примеры

Сначала посмотрим, что делает конфигурация russian с обычной фразой. Видно, как слова нормализуются, а предлог выкидывается.

Код: Выделить всё

SELECT to_tsvector('russian', 'Рейсы из Москвы во Владивосток');
-- 'владивосток':5 'москв':3 'рейс':1
-- предлоги из/во отброшены, Москвы -> москв, Рейсы -> рейс
Теперь практика на демобазе Авиаперевозки (схема bookings). Поищем аэропорты, в названии или городе которых встречается слово относящееся к морю/морской - проверим оператор @@ напрямую по выражению.

Код: Выделить всё

SELECT airport_code, airport_name, city
FROM bookings.airports
WHERE to_tsvector('russian', airport_name || ' ' || city)
      @@ plainto_tsquery('russian', 'морской');
Сделаем осмысленный поиск пассажиров по имени в билетах с ранжированием. plainto_tsquery склеит слова запроса через И, ts_rank даст оценку релевантности для сортировки.

Код: Выделить всё

SELECT ticket_no, passenger_name,
       ts_rank(to_tsvector('simple', passenger_name),
               plainto_tsquery('simple', 'IVAN')) AS rank
FROM bookings.tickets
WHERE to_tsvector('simple', passenger_name)
      @@ plainto_tsquery('simple', 'IVAN')
ORDER BY rank DESC
LIMIT 10;
-- для латинских имен берем конфигурацию simple: без стемминга, просто регистр и токены
Гонять to_tsvector на каждой строке при каждом запросе дорого. Правильнее хранить готовый tsvector в генерируемом столбце (с PostgreSQL 12) и навесить на него GIN-индекс. GIN - это инвертированный индекс: для каждой лексемы он держит список строк, где она встречается, поэтому проверка @@ становится почти мгновенной.

Код: Выделить всё

ALTER TABLE bookings.aircrafts_data
  ADD COLUMN model_tsv tsvector
  GENERATED ALWAYS AS
    (to_tsvector('russian', model ->> 'ru')) STORED;

CREATE INDEX aircrafts_tsv_gin ON bookings.aircrafts_data
  USING gin (model_tsv);

SELECT aircraft_code, model ->> 'ru' AS model
FROM bookings.aircrafts_data
WHERE model_tsv @@ to_tsquery('russian', 'боинг | аэробус');
А теперь нечеткий поиск для опечаток. Полнотекстовый поиск не спасет, если человек написал Влодивосток. Тут помогает расширение pg_trgm: оно бьет строку на триграммы (тройки символов) и меряет похожесть. Оператор % находит похожие строки, similarity дает число от 0 до 1.

Код: Выделить всё

CREATE EXTENSION IF NOT EXISTS pg_trgm;

SELECT city, similarity(city, 'Влодивасток') AS sim
FROM bookings.airports
WHERE city % 'Влодивасток'
ORDER BY sim DESC
LIMIT 5;
-- найдет Владивосток несмотря на две опечатки
Для ускорения такого поиска и для ускорения LIKE/ILIKE по подстроке у pg_trgm есть свой GIN-индекс с классом операторов gin_trgm_ops.

Код: Выделить всё

CREATE INDEX airports_city_trgm
  ON bookings.airports USING gin (city gin_trgm_ops);
Частые грабли
  • Поиск ничего не находит, потому что конфигурация по умолчанию english, а текст русский. Всегда указывайте 'russian' явно в to_tsvector и to_tsquery, либо задайте default_text_search_config.
  • to_tsquery падает с ошибкой syntax error на пользовательском вводе вроде пробела или скобки. Для сырого ввода используйте plainto_tsquery или websearch_to_tsquery, они не ломаются.
  • Конфигурация документа и запроса разные. Если документ собран как 'russian', а запрос как 'simple', лексемы не совпадут и поиск промахнется. Конфигурация должна быть одна и та же с обеих сторон.
  • Индекс GIN построен по одному выражению, а в WHERE другое (другой порядок конкатенации, другой cast). Планировщик не сможет применить индекс - выражение должно совпадать байт в байт. Генерируемый столбец снимает эту проблему.
  • Забыли, что GIN-индекс не помогает ts_rank сортировать. Индекс ускоряет отбор по @@, но ранжирование все равно считается по отобранным строкам, поэтому LIMIT и хороший фильтр важны.
  • pg_trgm и оператор % зависят от порога pg_trgm.similarity_threshold (по умолчанию 0.3). Слишком высокий порог теряет совпадения с опечатками, слишком низкий тащит мусор.
  • Триграммный индекс gin_trgm_ops большой и медленнее на запись. На горячих таблицах с частыми вставками взвешивайте стоимость.
Мини-лаба
  • Создайте таблицу docs(id serial primary key, body text) и вставьте 5-6 строк с русскими предложениями, где встречаются разные формы одного слова (рейс, рейсы, рейсов).
  • Выполните SELECT to_tsvector('russian', body) FROM docs и убедитесь, что формы свернулись в одну лексему.
  • Найдите строки через WHERE to_tsvector('russian', body) @@ plainto_tsquery('russian', 'рейс') - проверьте, что нашлись все формы.
  • Добавьте генерируемый столбец body_tsv типа tsvector через GENERATED ALWAYS AS ... STORED и навесьте GIN-индекс.
  • Сравните планы EXPLAIN ANALYZE для поиска по выражению и по индексированному столбцу.
  • Установите pg_trgm, добавьте строку с опечаткой и найдите ее через оператор % и similarity.
  • Поиграйте с set pg_trgm.similarity_threshold и посмотрите, как меняется набор результатов.
Контрольные вопросы
  • Чем tsvector отличается от tsquery и что делает оператор @@?
  • Зачем нужна конфигурация russian и что произойдет при поиске по русскому тексту с конфигурацией english?
  • В каких случаях вы возьмете plainto_tsquery, а в каких to_tsquery или websearch_to_tsquery?
  • Почему генерируемый tsvector-столбец с GIN-индексом быстрее, чем to_tsvector в WHERE на каждой строке?
  • Что считают ts_rank и ts_rank_cd и почему результат поиска все равно нужно сортировать отдельно?
  • Когда полнотекстовый поиск бессилен и чем тут помогает pg_trgm?
👍3 ❤️1 🔥 😄 🤔3
Аватара пользователя
torchwhale
Сообщения: 1
Зарегистрирован: 19 май 2026, 01:10

Re: Полнотекстовый поиск

Сообщение torchwhale »

А обязательно держать tsvector в отдельном столбце? У меня таблица небольшая, тысяч 50 строк, может проще to_tsvector прямо в where оставить и не плодить колонки
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
semyon2024
Сообщения: 1
Зарегистрирован: 03 июн 2026, 07:27

Re: Полнотекстовый поиск

Сообщение semyon2024 »

Поймал грабли с websearch_to_tsquery: оно по дефолту склеивает слова через И, а мне надо было ИЛИ. В итоге запрос из двух слов находил почти ноль строк, пока не понял в чем дело
👍 ❤️1 🔥 😄 🤔2
Ответить
← Предыдущая глава
Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
Следующая глава →
Роли и пользователи

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: Amazon [Bot] и 2 гостя