Полнотекстовый поиск BM25 в PostgreSQL: разбор расширения pg_textsearch
Долгое время полнотекстовый поиск считался слабым местом СУБД PostgreSQL. Встроенный функционал текстового поиска и функция ts_rank используют упрощенный алгоритм ранжирования, уступающий специализированным поисковым движкам. В результате инженерам приходилось разворачивать внешнюю инфраструктуру (Elasticsearch, Algolia), оплачивать серверные мощности и поддерживать сложные пайплайн-процессы для постоянной синхронизации данных.
Альтернативой выступает новое open-source расширение pg_textsearch от компании Timescale (ранее известное под кодовым именем Tapir). Инструмент добавляет в PostgreSQL нативную поддержку математического алгоритма ранжирования BM25 (Best Matching 25) — стандарта, используемого в Elasticsearch и Lucene.
Установка, активация и особенности работы планера
Расширение поставляется в виде бинарных сборок для Linux и macOS (поддерживаются PostgreSQL 17 и 18) или собирается из исходников с помощью make && make install. Пошаговый процесс подключения включает следующие этапы:
- Предварительная загрузка библиотеки: добавить
pg_textsearchв параметр конфигурацииshared_preload_librariesв файлеpostgresql.confи перезапустить сервис PostgreSQL. - Активация в базе данных: выполнить однократную команду
CREATE EXTENSION pg_textsearch;в целевой базе. - Создание таблицы и индекса: разметить текстовую колонку индексом типа
bm25.
При выполнении тестовых запросов через EXPLAIN на малых объемах данных планер PostgreSQL может отдать предпочтение последовательному сканированию таблицы (sequential scan). Для тестирования индекса его можно принудительно включить через SET enable_seqscan = off;. При этом важно учитывать, что даже при выборе последовательного сканирования оператор <@> использует индекс BM25 для получения статистических метрик корпуса (общее число документов и их средняя длина), необходимых для корректного расчета весов.
Особое внимание следует обратить на использование поиска внутри процедурного языка PL/pgSQL. Поскольку неявный синтаксис text <@> 'query' полагается на планерные хуки (planner hooks), которые не вызываются внутри функций и блоков DO, в хранимых процедурах необходимо передавать имя индекса явно через конструкцию to_bm25query('query', 'docs_idx') или явное приведение типа 'docs_idx:query'::bm25query.
Синтаксис и настройка гиперпараметров BM25
Поиск в pg_textsearch выполняется с помощью лаконичного SQL-оператора <@>:
SELECT * FROM documents
ORDER BY content <@> 'database search'
LIMIT 10;
Оператор возвращает отрицательные значения оценок BM25, поскольку сканирование индексов в Postgres выполняется в порядке возрастания (чем ниже отрицательное число, тем выше релевантность).
При создании индекса можно гибко настраивать гиперпараметры BM25:
CREATE INDEX docs_idx ON documents
USING bm25(content)
WITH (text_config='english', k1=1.5, b=0.8);
Параметр k1 (по умолчанию 1.2) управляет насыщением частоты терминов (term frequency saturation), а параметр b (по умолчанию 0.75) отвечает за нормализацию длины документа относительно средней длины по всей коллекции.
Индексы выражений, частичные индексы и мультиязычность
pg_textsearch поддерживает индексирование выражений, что особенно полезно при поиске по JSONB-полям, объединении нескольких колонок или предварительной трансформации текста:
-- Индексирование JSONB-поля
CREATE INDEX ON events USING bm25 ((data->>'description'))
WITH (text_config='english');
-- Мультиколоночный поиск
CREATE INDEX ON articles USING bm25 ((coalesce(title, '') || ' ' || coalesce(body, '')))
WITH (text_config='english');
Поддерживаются частичные индексы (WHERE status = 'published'), уменьшающие размер индекса. Для мультиязычных таблиц создаются отдельные частичные индексы с языковыми стеммерами:
CREATE INDEX docs_en_idx ON docs USING bm25 (content) WITH (text_config='english') WHERE lang = 'en';
CREATE INDEX docs_ru_idx ON docs USING bm25 (content) WITH (text_config='russian') WHERE lang = 'ru';
-- Вызов с явным указанием индекса через to_bm25query
SELECT * FROM docs
WHERE lang = 'ru'
ORDER BY content <@> to_bm25query('поисковый запрос', 'docs_ru_idx')
LIMIT 10;
Взаимодействие фильтрации WHERE и индексов BM25
При выполнении запросов с условиями WHERE существуют две основные стратегии фильтрации:
- Pre-filtering (Предварительная фильтрация): использует сторонние индексы (например, B-tree по
category_id) для отбора строк до начала ранжирования. Наиболее эффективна при высокой селективности условия (отсеивается >90% строк). - Post-filtering (Пост-фильтрация): сначала выполняет сканирование индекса BM25, а затем фильтрует полученные результаты. Если условие отсеивает много записей, итоговая выборка может содержать меньше строк, чем запрошено в
LIMIT. Решение — увеличение внутреннего лимита в подзапросе.
Архитектура дисковой мемтаблицы и оптимизация Block-Max WAND
Внутри pg_textsearch использует дисковую L0-мемтаблицу, расположенную непосредственно в файле индекса. Все изменения логируются через стандартный WAL-механизм GenericXLog, обеспечивая отказоустойчивость при сбоях и нативную потоковую репликацию на реплики без необходимости модификации ядра СУБД.
Сброс мемтаблицы на диск (auto-spill) управляется двумя триггерами:
memtable_pages_threshold: сбрасывает данные при превышении цепочкой 64 страниц (~512 КБ).bulk_load_threshold: сбрасывает данные при транзакционном накопителе 100 000 терминов при массовых операцияхCOPY.
Высокая скорость обработки Top-K запросов достигается благодаря алгоритму Block-Max WAND (BMW). При наличии предложения LIMIT алгоритм оценивает верхние границы релевантности в блоках индекса и полностью пропускает группы документов, которые заведомо не смогут попасть в топ результатов.
Оптимизация памяти и настройка индексов в продакшене
При настройке maintenance_work_mem >= 64MB построение индексов BM25 на больших таблицах автоматически распараллеливается по нескольким фоновым воркерам. Инженеры могут управлять параметрами параллелизма через max_parallel_maintenance_workers.
Для консолидации накопившихся сегментов индекса после массовых вставок или регулярного обновления записей применяется специализированная процедура фоновой оптимизации:
SELECT bm25_force_merge('docs_idx');
Слияние сегментов снижает фрагментацию дисковых блоков и повышает производительность выборки Top-K результатов. Сжатие сегментов включено по умолчанию (pg_textsearch.compress_segments = on), что сокращает объем ввода-вывода при чтении страниц.
Расширение pg_textsearch идеально сочетается с векторным расширением pgvector. Инженеры могут выполнять традиционный полнотекстовый поиск по ключевым словам и семантический векторный поиск в рамках единой СУБД, выстраивая гибридные RAG-пайплайны для AI-приложений без привлечения сторонних баз данных.
Технические ограничения и обработка крупных документов
При эксплуатации следует учитывать ряд архитектурных ограничений:
- Отсутствие нативного фразового поиска: индекс хранит частоты терминов, но не их точные позиции. Для точного поиска фраз применяется комбинация BM25 с пост-фильтрацией по
ILIKE. - Секционированные таблицы (Partitioning): статистика IDF рассчитывается локально внутри каждой секции, поэтому итоговые оценки релевантности между разрозненными секциями могут отличаться по шкале.
- Обработка крупных документов и CJK-языков: документы с объемом токенов более 1 МБ автоматически разбиваются на 256-килобайтные чанки по границам пробелов. Для языков без пробельных разделителей (китайский, японский) или очень крупных документов рекомендуется использовать разбиение на массивы
text[]на уровне приложения в комбинации со специализированным стеммером (например,zhparser), посколькуpg_textsearchподдерживает индексацию массивов поэлементно.

