7. PostgreSQL: Full-Text Search
Full-Text Search (FTS) в PostgreSQL позволяет эффективно искать по текстовым данным с учётом морфологии, релевантности и ранжирования.
Основные концепции
Заголовок раздела «Основные концепции»graph TD A[Text] --> B[to_tsvector] B --> C[TSVector - индексированный документ]
D[Search Query] --> E[to_tsquery / plainto_tsquery] E --> F[TSQuery - поисковый запрос]
C --> G[Поиск @@] F --> G G --> H[Результаты с ранжированием]TSVector и TSQuery
Заголовок раздела «TSVector и TSQuery»TSVector - представление документа
Заголовок раздела «TSVector - представление документа»-- Создание tsvector из текстаSELECT to_tsvector('english', 'The quick brown fox jumps over the lazy dog');-- Результат: 'brown':3 'dog':9 'fox':4 'jump':5 'lazi':8 'quick':2
-- Процесс:-- 1. Токенизация (разбиение на слова)-- 2. Нормализация (приведение к нижнему регистру)-- 3. Удаление стоп-слов (the, over, etc.)-- 4. Стемминг (jumps -> jump, lazy -> lazi)TSQuery - поисковый запрос
Заголовок раздела «TSQuery - поисковый запрос»-- to_tsquery - полный контроль (операторы &, |, !)SELECT to_tsquery('english', 'fox & dog');SELECT to_tsquery('english', 'fox | cat');SELECT to_tsquery('english', 'fox & !cat');
-- plainto_tsquery - простой текстSELECT plainto_tsquery('english', 'fox dog'); -- fox & dog
-- phraseto_tsquery - фраза целикомSELECT phraseto_tsquery('english', 'quick brown fox'); -- 'quick' `<->` 'brown' `<->` 'fox'
-- websearch_to_tsquery - синтаксис Google (PostgreSQL 11+)SELECT websearch_to_tsquery('english', '"quick fox" OR cat -dog');Базовый полнотекстовый поиск
Заголовок раздела «Базовый полнотекстовый поиск»CREATE TABLE articles ( id SERIAL PRIMARY KEY, title VARCHAR(200), content TEXT, author VARCHAR(100), created_at TIMESTAMP DEFAULT NOW());
INSERT INTO articles (title, content, author) VALUES('PostgreSQL Full-Text Search', 'PostgreSQL provides powerful full-text search capabilities...', 'Alice'),('Introduction to Databases', 'Databases are essential for modern applications...', 'Bob'),('Advanced PostgreSQL', 'Learn advanced PostgreSQL features like partitioning...', 'Alice');
-- Простой поискSELECT titleFROM articlesWHERE to_tsvector('english', content) @@ to_tsquery('english', 'PostgreSQL');
-- Поиск по нескольким полямSELECT title, authorFROM articlesWHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', 'PostgreSQL search');Generated Column для FTS
Заголовок раздела «Generated Column для FTS»-- Добавление generated column для поискового вектораALTER TABLE articlesADD COLUMN search_vector tsvectorGENERATED ALWAYS AS ( setweight(to_tsvector('english', coalesce(title, '')), 'A') || setweight(to_tsvector('english', coalesce(content, '')), 'B') || setweight(to_tsvector('english', coalesce(author, '')), 'C')) STORED;
-- Теперь поиск проще и быстрееSELECT title FROM articlesWHERE search_vector @@ plainto_tsquery('english', 'PostgreSQL');Веса:
A: Самая высокая важность (заголовки)B: Высокая важность (основной контент)C: Средняя важность (автор, метаданные)D: Низкая важность
GIN индекс для FTS
Заголовок раздела «GIN индекс для FTS»-- Создание GIN индексаCREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- Теперь поиск использует индексEXPLAIN ANALYZESELECT title FROM articlesWHERE search_vector @@ plainto_tsquery('english', 'PostgreSQL');Ранжирование результатов
Заголовок раздела «Ранжирование результатов»-- ts_rank - базовое ранжированиеSELECT title, ts_rank(search_vector, query) AS rankFROM articles, plainto_tsquery('english', 'PostgreSQL database') queryWHERE search_vector @@ queryORDER BY rank DESC;
-- ts_rank_cd - с учётом расстояния между словамиSELECT title, ts_rank_cd(search_vector, query) AS rankFROM articles, plainto_tsquery('english', 'PostgreSQL database') queryWHERE search_vector @@ queryORDER BY rank DESC;
-- Нормализация ранга (0..1)SELECT title, ts_rank(search_vector, query, 1) AS normalized_rankFROM articles, plainto_tsquery('english', 'PostgreSQL') queryWHERE search_vector @@ queryORDER BY normalized_rank DESC;Подсветка результатов
Заголовок раздела «Подсветка результатов»-- ts_headline - выделение найденных словSELECT title, ts_headline( 'english', content, query, 'StartSel=<b>, StopSel=</b>, MaxWords=50, MinWords=25' ) AS snippetFROM articles, plainto_tsquery('english', 'PostgreSQL search') queryWHERE search_vector @@ query;
-- Результат:-- PostgreSQL provides powerful full-text search capabilities... (with <b> tags)Многоязычный поиск
Заголовок раздела «Многоязычный поиск»CREATE TABLE multilang_articles ( id SERIAL PRIMARY KEY, title VARCHAR(200), content TEXT, language VARCHAR(20), -- 'english', 'russian', 'spanish' search_vector tsvector);
-- Триггер для автоматического обновления search_vectorCREATE OR REPLACE FUNCTION update_search_vector()RETURNS TRIGGER AS $$BEGIN NEW.search_vector := setweight(to_tsvector(NEW.language::regconfig, coalesce(NEW.title, '')), 'A') || setweight(to_tsvector(NEW.language::regconfig, coalesce(NEW.content, '')), 'B'); RETURN NEW;END;$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvector_update BEFORE INSERT OR UPDATE ON multilang_articles FOR EACH ROW EXECUTE FUNCTION update_search_vector();
-- Вставка на разных языкахINSERT INTO multilang_articles (title, content, language) VALUES('PostgreSQL Tutorial', 'Learn PostgreSQL full-text search', 'english'),('Руководство по PostgreSQL', 'Изучаем полнотекстовый поиск в PostgreSQL', 'russian');
-- Поиск на английскомSELECT title FROM multilang_articlesWHERE language = 'english'AND search_vector @@ plainto_tsquery('english', 'PostgreSQL search');
-- Поиск на русскомSELECT title FROM multilang_articlesWHERE language = 'russian'AND search_vector @@ plainto_tsquery('russian', 'полнотекстовый поиск');Продвинутые техники
Заголовок раздела «Продвинутые техники»Fuzzy Search (нечёткий поиск)
Заголовок раздела «Fuzzy Search (нечёткий поиск)»-- Установка расширения pg_trgmCREATE EXTENSION IF NOT EXISTS pg_trgm;
-- GIN индекс для триграммCREATE INDEX idx_articles_title_trgm ON articles USING GIN(title gin_trgm_ops);
-- Поиск с опечатками (similarity)SELECT title, similarity(title, 'PostgreSQl') AS simFROM articlesWHERE title % 'PostgreSQl' -- оператор % использует триграммыORDER BY sim DESC;
-- Комбинация FTS и fuzzy searchSELECT title, ts_rank(search_vector, query) AS fts_rank, similarity(title, 'PostgreSQl') AS fuzzy_rankFROM articles, plainto_tsquery('english', 'PostgreSQL') queryWHERE search_vector @@ query OR title % 'PostgreSQl'ORDER BY fts_rank + fuzzy_rank DESC;Autocomplete / Suggestions
Заголовок раздела «Autocomplete / Suggestions»-- Префиксный поиск с :*SELECT titleFROM articlesWHERE search_vector @@ to_tsquery('english', 'post:*');-- Найдёт: PostgreSQL, posting, postman, etc.
-- TOP suggestions с частотойSELECT word, ndoc AS frequencyFROM ts_stat('SELECT search_vector FROM articles')WHERE word LIKE 'post%'ORDER BY frequency DESCLIMIT 10;Агрегация и группировка
Заголовок раздела «Агрегация и группировка»-- Поиск по агрегированным документамWITH aggregated AS ( SELECT author, string_agg(title || ' ' || content, ' ') AS all_text FROM articles GROUP BY author)SELECT author, ts_rank(to_tsvector('english', all_text), query) AS rankFROM aggregated, plainto_tsquery('english', 'PostgreSQL') queryWHERE to_tsvector('english', all_text) @@ queryORDER BY rank DESC;TypeScript примеры
Заголовок раздела «TypeScript примеры»import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL,});
interface SearchResult { id: number; title: string; snippet: string; rank: number;}
// Полнотекстовый поиск с подсветкойasync function searchArticles( searchQuery: string, limit: number = 10): Promise<SearchResult[]> { const result = await pool.query( ` SELECT id, title, ts_headline( 'english', content, query, 'StartSel=<mark>, StopSel=</mark>, MaxWords=50' ) AS snippet, ts_rank(search_vector, query) AS rank FROM articles, plainto_tsquery('english', $1) query WHERE search_vector @@ query ORDER BY rank DESC LIMIT $2 `, [searchQuery, limit] ); return result.rows;}
// Поиск с фильтрамиasync function advancedSearch(params: { query: string; author?: string; dateFrom?: Date; dateTo?: Date;}) { let sql = ` SELECT id, title, author, created_at, ts_rank(search_vector, plainto_tsquery('english', $1)) AS rank FROM articles WHERE search_vector @@ plainto_tsquery('english', $1) `; const values: any[] = [params.query]; let paramIndex = 2;
if (params.author) { sql += ` AND author = $${paramIndex}`; values.push(params.author); paramIndex++; }
if (params.dateFrom) { sql += ` AND created_at >= $${paramIndex}`; values.push(params.dateFrom); paramIndex++; }
if (params.dateTo) { sql += ` AND created_at <= $${paramIndex}`; values.push(params.dateTo); paramIndex++; }
sql += ' ORDER BY rank DESC LIMIT 20';
const result = await pool.query(sql, values); return result.rows;}
// Autocomplete suggestionsasync function getSuggestions(prefix: string, limit: number = 5) { const result = await pool.query( ` SELECT DISTINCT word FROM ts_stat( 'SELECT search_vector FROM articles' ) WHERE word LIKE $1 ORDER BY ndoc DESC LIMIT $2 `, [`${prefix}%`, limit] ); return result.rows.map((r) => r.word);}
// Fuzzy search для опечатокasync function fuzzySearch(query: string) { const result = await pool.query( ` SELECT id, title, similarity(title, $1) AS sim FROM articles WHERE title % $1 ORDER BY sim DESC LIMIT 10 `, [query] ); return result.rows;}
// Поиск "похожих" статейasync function findSimilar(articleId: number, limit: number = 5) { const result = await pool.query( ` WITH target AS ( SELECT search_vector FROM articles WHERE id = $1 ) SELECT a.id, a.title, ts_rank(a.search_vector, target.search_vector::tsquery) AS similarity FROM articles a, target WHERE a.id != $1 ORDER BY similarity DESC LIMIT $2 `, [articleId, limit] ); return result.rows;}💡 Best Practices
Заголовок раздела «💡 Best Practices»- Используйте generated columns для search_vector
- Создавайте GIN индексы на tsvector колонках
- Устанавливайте веса (A, B, C, D) для разных полей
- Нормализуйте ранги для сравнимых результатов
- Комбинируйте FTS с pg_trgm для устойчивости к опечаткам
- Кэшируйте популярные запросы в приложении
Производительность
Заголовок раздела «Производительность»-- Проверка использования индексаEXPLAIN ANALYZESELECT title FROM articlesWHERE search_vector @@ plainto_tsquery('english', 'PostgreSQL');
-- Статистика по индексуSELECT schemaname, tablename, indexname, idx_scan, pg_size_pretty(pg_relation_size(indexname::regclass))FROM pg_stat_user_indexesWHERE indexname LIKE '%search%';Альтернативы
Заголовок раздела «Альтернативы»Когда FTS PostgreSQL не подходит:
- Elasticsearch: Более мощные возможности, real-time индексация
- Meilisearch: Быстрый typo-tolerant поиск
- Typesense: Open-source альтернатива Algolia
- Algolia: SaaS решение для поиска
Используйте PostgreSQL FTS если:
- Уже используете PostgreSQL
- Простые требования к поиску
- Не хотите поднимать отдельный сервис
- Данные не очень большие (меньше 10M документов)
⚠️ Частые ошибки
Заголовок раздела «⚠️ Частые ошибки»- Забывают создать GIN индекс (медленный поиск)
- Не используют generated columns (пересчёт при каждом запросе)
- Игнорируют веса полей (плохое ранжирование)
- Используют
LIKE '%text%'вместо FTS
Следующий урок: Partitioning в PostgreSQL →