Full-text search: поиск по текстовым полям

Почему обычный LIKE — это медленно и неудобно? 🔍

Представь, что у тебя есть таблица с книгами:

CREATE TABLE books (
    id SERIAL PRIMARY KEY,
    title VARCHAR(100),
    author VARCHAR(100),
    content TEXT  -- полный текст книги, может быть очень большим
);

Хочешь найти все книги, где встречается слово "дракон"? Стандартный подход:

SELECT * FROM books 
WHERE content LIKE '%дракон%';

Проблемы такого подхода:

  1. Медленно — СУБД перебирает весь текст построчно
  2. Нет морфологии — Не найдёт "дракона", "дракону" и т.д.
  3. Нет релевантности — Нельзя отсортировать по "важности" результата

Full-text search спешит на помощь! 🚀

Полнотекстовый поиск — это специальный механизм для быстрого и "умного" поиска по тексту. В PostgreSQL он реализован через:

  1. TSVECTOR — специальный тип данных для хранения "поискового индекса"
  2. TSQUERY — язык запросов для поиска
  3. GIN-индексы — для молниеносного поиска

Создаём поисковый индекс

Добавим в таблицу новое поле и индекс:

-- Добавляем поле для хранения поискового индекса
ALTER TABLE books ADD COLUMN search_index tsvector;

-- Заполняем его данными (можно автоматизировать триггером)
UPDATE books SET search_index = 
    to_tsvector('russian', title || ' ' || author || ' ' || content);

-- Создаём специальный индекс для ускорения поиска
CREATE INDEX idx_books_search ON books USING gin(search_index);

Пишем первые "умные" запросы 🔥

Базовый поиск

Найдём все книги про драконов:

SELECT title, author 
FROM books 
WHERE search_index @@ to_tsquery('russian', 'дракон');

Этот запрос:

  • Найдёт все формы слова ("дракон", "дракона", "драконов")
  • Будет работать в разы быстрее LIKE
  • Может искать по нескольким словам сразу

Расширенные возможности

-- Поиск по фразе (слова должны идти подряд)
SELECT title FROM books 
WHERE search_index @@ to_tsquery('russian', 'красный & дракон');

-- Поиск с обязательным и необязательным условием
SELECT title FROM books 
WHERE search_index @@ to_tsquery('russian', 'дракон & !зеленый');

-- Поиск с расстоянием между словами (до 3 слов между "огненный" и "меч")
SELECT title FROM books 
WHERE search_index @@ to_tsquery('russian', 'огненный <3> меч');

Сортировка по релевантности 🏆

Одна из самых мощных фишек — сортировка результатов по "важности":

SELECT 
    title,
    author,
    ts_rank(search_index, to_tsquery('russian', 'дракон | магия')) AS rank
FROM books
WHERE search_index @@ to_tsquery('russian', 'дракон | магия')
ORDER BY rank DESC
LIMIT 10;

Функция ts_rank() вычисляет "вес" результата на основе:

  • Частоты встречаемости слов
  • Близости искомых слов друг к другу
  • Важности поля (можно настраивать)

Автоматизация обновления индекса ⚡

Чтобы не обновлять search_index вручную, создадим триггер:

CREATE OR REPLACE FUNCTION update_search_index() RETURNS trigger AS $$
BEGIN
    NEW.search_index := 
        to_tsvector('russian', COALESCE(NEW.title, '') || ' ' || 
                    COALESCE(NEW.author, '') || ' ' || 
                    COALESCE(NEW.content, ''));
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER books_search_index_update
BEFORE INSERT OR UPDATE ON books
FOR EACH ROW EXECUTE FUNCTION update_search_index();

Теперь индекс будет обновляться автоматически при любых изменениях данных!

Практический пример: книжный магазин 📚

Допустим, мы делаем поиск для интернет-магазина книг. Вот как это может выглядеть:

-- Расширенный поиск с подсветкой результатов
SELECT 
    title,
    author,
    ts_headline('russian', content, to_tsquery('russian', 'дракон'), 
                'StartSel = <b>, StopSel = </b>') AS snippet,
    ts_rank(search_index, to_tsquery('russian', 'дракон')) AS rank
FROM books
WHERE search_index @@ to_tsquery('russian', 'дракон')
ORDER BY rank DESC
LIMIT 5;

Функция ts_headline() выделяет найденные слова в тексте (отлично подходит для вывода превью).

Производительность и тонкая настройка ⚙️

Для больших проектов важно:

  1. Выбирать правильную конфигурацию — 'russian', 'english' и др.
  2. Настраивать веса полей — title важнее content?

Пример с весами:

UPDATE books SET search_index =
    setweight(to_tsvector('russian', title), 'A') ||
    setweight(to_tsvector('russian', author), 'B') ||
    setweight(to_tsvector('russian', content), 'C');

Теперь совпадения в title будут давать больший "вес", чем в content.


Полнотекстовый поиск — это мощный инструмент, который превращает твою СУБД в интеллектуальную поисковую систему. С ним ты можешь реализовывать сложные поисковые сценарии, которые раньше требовали отдельных решений вроде Elasticsearch!

Скрыть рекламу навсегда

🎥 YouTube: программирование простым языком

Канал, где я спокойно и по шагам объясняю сложные темы — без заумных терминов и лишней теории.

Подходит, если раньше «не заходило», но хочется наконец понять.

▶️ Смотреть курсы на YouTube