PlanetScale представила TIN (Text INdex) — расширение PostgreSQL для полнотекстового поиска с ранжированием BM25 и цифрами производительности, которые выглядят неприлично хорошо на фоне привычного GIN.
Инженерная часть там действительно интересная. Но прежде чем загораться, стоит выяснить один вопрос, который в анонсе не выделен, — и ответ на него для большинства читателей решает всё.
Факты — из анонса PlanetScale (Eric Ridge, Patrick Reynolds, 16 сентября 2026), документации и репозиториев. Разбор и проверка доступности мои.
Какую задачу решает
Полнотекстовый поиск в PostgreSQL существует давно — tsvector плюс индекс GIN. Проблема не в том, что его нет, а в том, что он плохо тянет сочетание требований, возникающее в реальном приложении:
- сложные булевы выражения, фразовые и span-запросы, нечёткий поиск, подстановочные знаки, регулярные выражения;
- ранжирование BM25 — стандарт де-факто в поисковых движках, которого в штатном PostgreSQL нет;
- и всё это одновременно с обычной работой базы: джойны, фильтры по другим колонкам, непрерывные обновления, репликация, бэкапы и корректная видимость транзакций.
Последний пункт — причина, по которой поиск часто выносят в Elasticsearch, а потом годами живут с рассинхронизацией двух хранилищ. TIN пытается закрыть задачу внутри базы.
Синтаксис минималистичный:
CREATE INDEX an_index_name ON table_name USING tin(text_column_name);
SELECT * FROM table_name WHERE text_column_name ==> 'some words';
SELECT * FROM products WHERE description ==> 'stretch denim jeans'
ORDER BY tin.score(ctid) DESC LIMIT 10;Инженерная идея
Здесь самое любопытное, и это стоит разобрать независимо от того, будете вы этим пользоваться или нет.
Идентификатор документа — это ctid. Обычные поисковые индексы ведут собственную нумерацию документов и отдельную структуру, отображающую её на строки таблицы. TIN использует штатный ctid PostgreSQL — 48-битный физический адрес кортежа. Отдельного отображения не нужно вовсе: индекс сразу знает, где лежит строка.
Побочный эффект приятный: результаты естественным образом выходят в порядке страниц кучи, то есть чтение идёт последовательно, а не случайными скачками по диску.
Двухуровневые битовые карты. Данные разложены на карты уровня страницы (256 бит — ровно регистр AVX2) и уровня смещения внутри страницы. Пересечения и объединения множеств документов считаются векторными инструкциями процессора, а не поэлементно.
Видимость через ту же механику. Проверка видимости транзакций — это пересечение постраничной битовой карты с картой видимости PostgreSQL. То есть корректность MVCC не прикручена сбоку, а встроена в ту же операцию, что и поиск.
Сегменты и фоновая работа. Данные лежат в изменяемых и неизменяемых сегментах; фоновые процессы переводят первые во вторые и сливают сегменты между собой. У каждого сегмента своя карта живых записей — через неё дружит с VACUUM. Логика узнаваемая: тот же принцип «пиши в мутабельный буфер, сливай в иммутабельные куски», что и у LSM-подобных движков вроде MergeTree в ClickHouse.
Цифры
Бенчмарк на корпусе Stack Exchange: 85 ГБ, 150 миллионов документов.
| Сценарий | TIN | ParadeDB | GIN |
|---|---|---|---|
| Смешанные запросы, топ-10 | 199 QPS, p99 256 мс | 7.9 QPS | OOM |
| Конъюнкции и фразы, топ-10 | 242 QPS | 24 QPS | 0.4 QPS |
| Дизъюнкции с обновлениями | 125 QPS | 2.2 QPS | OOM |
COUNT по дизъюнкции (Wikipedia) | 10 260 QPS, p99 2 мс | 291 QPS | 1.4 QPS |
Сборка индекса — 8 минут 10 секунд, размер 50.7 ГБ при 32 ГБ оперативной памяти.
Разрыв заявлен от 8 до 541 раз. Оговорка обязательная: это бенчмарки вендора на его же сценариях. Строки с OOM у GIN показательны сами по себе, но проверять такое нужно на своих данных и своих запросах.
Большая оговорка
А теперь то, ради чего стоило проверять источники.
TIN нельзя поставить себе. Это не расширение, которое вы соберёте и загрузите в свой PostgreSQL. Оно доступно только на управляемых базах PlanetScale — как функция сервиса, а не как open source.
Открыт другой проект — lead (AGPL-3.0, Rust). И вот как его описывают сами авторы: это намеренно непродакшновое расширение, чтобы гонять TIN-совместимый SQL в разработке, CI и staging. Оно ставит корректность и компактность выше производительности и сознательно непригодно для продовых нагрузок.
То есть картина такая:
| Где работает | Лицензия | Для прода | |
|---|---|---|---|
| TIN | только PlanetScale | закрыт | да |
| lead | где угодно | AGPL-3.0 | нет, явным текстом |
Логика вендора понятна и по-своему честная: lead позволяет разработчику писать и тестировать те же запросы локально, не имея доступа к проду. Но если вы держите PostgreSQL сами — а это большинство читателей такого блога, — вам эти цифры недоступны.
Что делать, если хостите сами
Три варианта, которые реально можно поставить:
tsvector+ GIN — штатный механизм. Ранжирование слабее BM25, на больших корпусах и сложных запросах упирается в потолок, но он есть везде и ничего не требует.- pg_textsearch от Timescale — BM25-ранжирование, под лицензией PostgreSQL, то есть по-настоящему открытый. В бенчмарке PlanetScale он присутствует и, разумеется, проигрывает TIN, но это работающий вариант для своей инсталляции.
- ParadeDB — тоже фигурирует в сравнении; проверяйте актуальные условия лицензирования под свой сценарий.
И старое доброе решение: вынести поиск наружу, в OpenSearch или аналог, приняв цену синхронизации. Именно от неё TIN и пытается избавить — но только своим клиентам.
Итог
Техническая часть сильная, и идея с ctid вместо собственной нумерации документов красива: индекс перестаёт быть параллельной вселенной со своим учётом строк и начинает говорить с базой на её же языке — физических адресов и карт видимости. Это стоит знать, даже если пользоваться не придётся.
Практический же вывод короткий: это функция управляемого сервиса, а не расширение PostgreSQL. Заголовок «полнотекстовый поиск для Postgres» читается как что-то, что можно поставить себе, — и в этом главная неточность восприятия. Если вы на PlanetScale, попробуйте. Если нет — смотрите pg_textsearch и не ждите тех же цифр.