← Назад к вопросам

Как работает поиск с морфологией в PostgreSQL?

3.0 Senior🔥 101 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Поиск с морфологией в PostgreSQL

Морфологический поиск в PostgreSQL — это полнотекстовый поиск, который понимает различные формы слова (лемматизация и стемминг). PostgreSQL имеет встроенную поддержку для полнотекстового поиска с русским языком.

Основные компоненты

1. Типы данных

# PostgreSQL поддерживает два типа:
# tsvector — преобразованный в индексируемый формат текст
# tsquery — запрос в формате, понятном engine'у поиска

CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,
    search_vector tsvector  -- индекс для поиска
);

CREATE INDEX idx_search ON articles USING gin(search_vector);

2. Функции морфологического анализа

# to_tsvector() — преобразует текст в tsvector
# Автоматически применяет морфологию (лемматизацию)

# Пример на SQL:
SELECT to_tsvector('russian', 'Кошка красивая');
-- Результат: 'красив':2 'кош':1
-- Слова приведены к базовой форме

SELECT to_tsvector('russian', 'бегут бегал бежит');
-- Результат: 'бежа':1,2,3
-- Все формы привели к базовой

Примеры поиска

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

# Поиск точного совпадения слова
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('russian', 'кошка');
-- Найдёт: кошка, кошки, кошке, кошкой и т.д.

# С оператором &| (OR)
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('russian', 'кошка | собака');
-- Найдёт статьи про кошек ИЛИ про собак

# С оператором & (AND)
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('russian', 'кошка & красивая');
-- Найдёт статьи про красивых кошек

# С оператором ! (NOT)
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('russian', 'кошка & ! серая');
-- Найдёт кошек, но не серых

Рейтинг релевантности

# ts_rank() вычисляет релевантность (от 0 до 1)
SELECT id, title, ts_rank(search_vector, query) as rank
FROM articles,
     to_tsquery('russian', 'красивая кошка') as query
WHERE search_vector @@ query
ORDER BY rank DESC;

# ts_rank_cd() — более сложный алгоритм (cover density)
SELECT id, title, ts_rank_cd(search_vector, query) as rank
FROM articles,
     to_tsquery('russian', 'красивая кошка') as query
WHERE search_vector @@ query
ORDER BY rank DESC;

Интеграция с Python (SQLAlchemy)

from sqlalchemy import Column, Integer, String, Text, func, and_
from sqlalchemy.dialects.postgresql import TSVECTOR
from sqlalchemy.sql import text
from sqlalchemy.orm import Session

class Article(Base):
    __tablename__ = 'articles'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(255))
    content = Column(Text)
    search_vector = Column(TSVECTOR)

class ArticleRepository:
    def __init__(self, session: Session):
        self.session = session
    
    def search(self, query: str, language: str = 'russian'):
        # Создаём tsquery из строки поиска
        tsquery = func.to_tsquery(language, query)
        
        # Поиск с рейтингом
        stmt = self.session.query(
            Article,
            func.ts_rank(Article.search_vector, tsquery).label('rank')
        ).filter(
            Article.search_vector.op('@@')(tsquery)
        ).order_by(text('rank DESC'))
        
        return stmt.all()
    
    def search_advanced(self, query: str, exclude: str = None):
        language = 'russian'
        base_query = query
        
        if exclude:
            # query & !exclude
            base_query = f"{query} & !{exclude}"
        
        tsquery = func.to_tsquery(language, base_query)
        
        return self.session.query(Article).filter(
            Article.search_vector.op('@@')(tsquery)
        ).all()

Обновление индекса

Вариант 1: Триггер

# Автоматическое обновление search_vector при INSERT/UPDATE
CREATE TRIGGER articles_search_update BEFORE INSERT OR UPDATE
ON articles FOR EACH ROW
EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'russian', title, content);

# На Python это означает, что поле обновляется автоматически
db.session.add(Article(
    title="Красивая кошка",
    content="Это статья про кошек..."
))
db.session.commit()  # search_vector обновится автоматически

Вариант 2: Ручное обновление

from sqlalchemy import text

class ArticleService:
    def __init__(self, session: Session):
        self.session = session
    
    def update_search_vector(self, article_id: int):
        # Обновляем вручную
        self.session.execute(
            text("""
                UPDATE articles
                SET search_vector = 
                    to_tsvector('russian', title) ||
                    to_tsvector('russian', content)
                WHERE id = :id
            """),
            {"id": article_id}
        )
        self.session.commit()
    
    def rebuild_all_search_vectors(self):
        # Переиндексируем всю таблицу
        self.session.execute(
            text("""
                UPDATE articles
                SET search_vector = 
                    to_tsvector('russian', title) ||
                    to_tsvector('russian', content)
            """)
        )
        self.session.commit()

Выделение контекста (highlighting)

# ts_headline() выделяет совпадения в тексте
SELECT 
    id,
    title,
    ts_headline('russian', content, query, 'StartSel=<mark>, StopSel=</mark>')
        as snippet
FROM articles,
     to_tsquery('russian', 'кошка') as query
WHERE search_vector @@ query;

# На Python:
from sqlalchemy import func

stmt = self.session.query(
    Article,
    func.ts_headline(
        'russian',
        Article.content,
        func.to_tsquery('russian', query_text),
        'StartSel=<mark>, StopSel=</mark>'
    ).label('snippet')
).filter(
    Article.search_vector.op('@@')(
        func.to_tsquery('russian', query_text)
    )
)

Русский язык и морфология

# PostgreSQL имеет встроенную поддержку русского
SELECT *
FROM pg_ts_dict
WHERE dictname = 'russian_stem';  -- русский стеммер

# Но для лучших результатов используй
# расширение pg_hunspell или словарь Ispell

# Проверка доступных конфигураций:
SELECT cfgname FROM pg_ts_config;
-- russian, russian_stemming и т.д.

Производительность

# 1. Используй GIN индекс
CREATE INDEX idx_articles_search ON articles USING gin(search_vector);

# 2. Статистика для оптимизатора
ANALYZE articles;

# 3. Объединение полей с разными весами
SELECT id, ts_rank(search_vector, query) as rank
FROM articles,
     to_tsquery('russian', 'кошка') as query
WHERE search_vector @@ query
LIMIT 100;  -- ограничивай результаты

Альтернативные подходы

# 1. Простой LIKE (медленно на больших объёмах)
SELECT * FROM articles
WHERE title ILIKE '%кошка%';

# 2. Расширение pg_trgm (триграммы)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_title_trgm ON articles USING gin(title gin_trgm_ops);

SELECT * FROM articles
WHERE title % 'красивая кошка';  # нечёткий поиск

# 3. Elasticsearch (на очень больших объёмах)
# более гибкий и быстрый для полнотекстового поиска

Лучшие практики

  • Используй GIN индекс для tsvector
  • Обновляй search_vector автоматически через триггер
  • Ограничивай результаты (LIMIT) для производительности
  • Кешируй результаты часто используемых поисков
  • Проверяй explain для оптимизации запросов
  • Для сложного поиска рассмотри Elasticsearch
  • Тестируй морфологию на реальных данных

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