- PVSM.RU - https://www.pvsm.ru -
ACID и CDC [3]
Как это сделать [6]
Вывод [7]
БигДата всегда звучит красиво — пока не нужно по ним искать и за нее платить.
Когда в системе уже не сотни тысяч, а десятки миллионов записей, каждый запрос превращается в штурм базы
Скорость — это не удобство, а свойство архитектуры.
Если система отвечает медленно, значит, архитектура построена неправильно.
Реляционная СУБД - это ядро консистентности, но не оптимальный инструмент для полнотекстового и фасетного поиска. Логично разграничить роли: Postgres отвечает за транзакции и связи, а специализированный поисковый слой - за быстрый отклик и фильтрацию по множеству полей.
Поиск — это отдельная задача, и ей нужен отдельный слой.
Мы вынесли её наружу — туда, где всё строится вокруг скорости отклика.
Так OpenSearch, из инструмента для логов, превратился в поисковый движок, который действительно понимает данные.
7 терабайт перестали быть проблемой: система масштабируется, а пользователи получают ответ мгновенно.
Мы сделали это не ради моды, а ради стабильности.
Архитектура должна быть честной: каждый слой делает то, для чего создан.
OpenSearch стал не надстройкой, а естественной частью системы.
Реляционные СУБД не рассчитаны на интенсивный поиск по множеству полей и связей, особенно если в них присутствует геометрия.
Даже в PostGIS при сложных пространственных вычислениях и агрегациях возможны задержки, особенно если отсутствуют специализированные индексы (GiST/BRIN), за них надо платить, а от сюда следует что целесообразней было бы вынести часть поиска
OpenSearch решает эту задачу, превращая данные в индексированные документы, распределённые по шардам.
Каждый шард — это автономный поисковый сегмент, который хранит нужные срезы и возвращает ответ параллельно.
За счёт этого поиск превращается из линейного в распределённый, а отклик из секунд — в миллисекунды.
До OpenSearch мы долго использовали векторный поиск прямо в PostgreSQL через pgvector и собственную схему генерации эмбеддингов.
Выглядело это современно, но реальность оказалась жёстче.
Что пошло не так:
Объём.
7 ТБ данных - это миллионы объектов. Построение и хранение векторов оказалось дороже, чем сами данные.
Каждое обновление требовало пересчёта эмбеддингов.
Тип данных.
Наши объекты — не тексты, а пространственные сущности и атрибуты.
Векторизация таких структур даёт ложные корреляции.
Недетерминизм.
Результаты поиска “ближайшего соседа” могли отличаться при одинаковом запросе.
Для систем, где важна воспроизводимость и точное соответствие данных, это недопустимо.
Нагрузка.
При десятках миллионов строк индекс pgvector перестал быть realtime-решением.
Приходилось обновлять вручную и делать ребилды.
Нет объяснимости.
Нельзя понять, почему система выбрала именно этот результат.
Он может быть “похож математически”, но не по смыслу.
После этого мы постепенно перешли на OpenSearch: не вероятностный, а детерминированный поиск.
Там, где векторы давали “похоже”, OpenSearch возвращает “точно”.
Скорость осталась, стабильность появилась.
Мы перешли от вероятностной модели к детерминированной архитектуре поиска, где результат всегда повторяем и объясним. OpenSearch оказался естественным выбором — тот же класс индексного движка, но основанный на чёткой логике: индекс отражает структуру и атрибуты, а не вероятностную близость.
Возможно, этот подход можно было бы дожать, и вокруг него можно спорить.
Но практика показала: проще и надёжнее решать задачу индексами, а не вероятностями.
При переходе на внешнюю поисковую систему возник главный вопрос — как сохранить согласованность данных между транзакциями в Postgres и индексами в OpenSearch.
Базовое решение — триггер синхронизации, который срабатывает при изменении объектов в базе.
Он получает идентификатор изменённой записи, формирует JSON‑представление с актуальными атрибутами и передаёт его в OpenSearch API для индексации, обновления или удаления.
Если транзакция откатывается — триггер не активируется, обеспечивая согласованность только для зафиксированных изменений.

Такой подход даёт простую и быструю синхронизацию, при которой пользователь всегда видит то, что реально зафиксировано в базе. Однако при росте нагрузки и числа параллельных операций триггерный механизм становится уязвим к временным сбоям и сетевым ошибкам.
Поэтому в промышленной архитектуре используется промежуточный слой событийной доставки — Kafka.
Postgres публикует изменения в очередь (через лог WAL), а отдельный потребитель обрабатывает их и обновляет индексы в OpenSearch.
Это разрывает жёсткую связанность между базой и поиском, сохраняет порядок событий и гарантирует доставку, даже если один из компонентов временно недоступен.
Таким образом, достигается реальное ACID-поведение на уровне всей связки, но уже с учётом требований к отказоустойчивости и горизонтальному масштабированию.
Ранее синхронизация с поисковым индексом реализовывалась напрямую через триггерную функцию в Postgres.
Триггер срабатывал на каждое изменение в таблице объектов, формировал JSON-документ через PL/pgSQL-функцию и синхронно отправлял его в OpenSearch по HTTP-запросу.
Пример структуры передаваемого документа:
{
"id": 12345,
"user_field1": "Smith",
"user_field2": "John",
"numeric_field": "75",
"category_field": "category A",
"timestamp_field1": "1940-05-15",
"text_field1": "London",
"identifier_field": "UK-123456",
"spatial_data": {
"area_name": "Object A",
"primary_geometry": {
"type": "Polygon",
"coordinates": [[
[-0.1276, 51.5073],
[-0.1274, 51.5073],
[-0.1274, 51.5075],
[-0.1276, 51.5075],
[-0.1276, 51.5073]
]]
}
}
}
Что пошло не так:
Схема оказалась хрупкой:
база и поиск были жёстко связаны;
при недоступности OpenSearch транзакции в Postgres могли не выполняться;
HTTP-вызовы внутри триггера добавляли задержки и создавали лишнюю нагрузку при массовых изменениях.
Решение — переход на CDC (Change Data Capture).
Теперь вместо прямых вызовов используется логическая репликация:
изменения данных фиксируются в WAL, CDC-процесс читает журнал, публикует события в Kafka, а отдельный потребитель обновляет индекс в OpenSearch.
Что это дало:
асинхронность: поиск больше не блокирует транзакции;
гарантированная доставка событий даже при сбоях;
масштабируемость без нагрузки на основную базу;
отказоустойчивость за счёт накопления событий в очереди.

PostGIS отвечает за данные и связи.
Триггер обеспечивает транзакционную синхронизацию.
API служит прослойкой безопасности и формата.
OpenSearch обеспечивает мгновенный поиск.
Индексация асинхронна.
События доставляются гарантированно через очередь.
Геоиндексы (geo_point, geo_shape)
обеспечивают быстрый отбор и фильтрацию по координатам.
Репликация
обеспечивает отказоустойчивость и равномерное распределение нагрузки.
Интервал refresh
настраивается под SLA: обычно от 5 до 30 секунд — в зависимости от частоты изменений и требований к актуальности данных.
Среднее время отклика поискового запроса (по индексу) 25–40 мс при кластере из N узлов. Это время именно поиска, не включая операции синхронизации и обновления индекса.(Время зависит от того как организованны ваши данные)
Единственный минус продолжительная первичная загрузка данных.
Практическая реализация
занимает несколько часов, если следовать логике трёх шагов.
Устанавливаем Java и системные зависимости, создаём отдельного пользователя opensearch.
Далее скачиваем дистрибутив нужной версии с официального сайта OpenSearch [8] и разворачиваем его в каталоге /opt/opensearch.
После распаковки:
даём права пользователю opensearch;
создаём systemd-сервис;
настраиваем автозапуск и логи в /var/log/opensearch;
проверяем работу командой
curl -X GET https://localhost:9200 -u 'admin:admin' --insecure
На этом этапе OpenSearch готов принимать запросы и создавать индексы.
API - это тонкая прослойка между внутренними сервисами и самим OpenSearch.
Она нужна для безопасности, нормализации форматов и разгрузки кластера.
Развёртывание простое:
создаём каталог /opt/opensearch-api;
распаковываем дистрибутив;
добавляем systemd-сервис, который запускает node index.js;
включаем автозапуск и проверяем статус.
Через этот слой проходят все операции добавления, обновления и удаления документов в индексе.
После установки кластера и API нужно подключить OpenSearch к существующим данным.
Для этого:
Проверяем, что Nginx проксирует запросы к opensearch и opensearch-api.
Убеждаемся, что индекс создан:
PUT /vash
{
"settings": {
"index": {
"number_of_shards": 2,
"number_of_replicas": 2
}
},
"mappings": {}
}
Для каждой записи базы вызываем внутреннюю функцию, возвращающую объект в формате JSON.
Полученный JSON отправляется через API в OpenSearch командой:
PUT /vash/_doc/:id
{
...данные объекта...
}
После этого документ становится доступен в поиске.
Чтобы система оставалась согласованной, используется триггер в базе, который реагирует на вставку, изменение и удаление данных.
Он формирует JSON-объект и синхронно передаёт его в OpenSearch API.
Таким образом сохраняется транзакционная целостность: если запись изменилась в БД, она тут же обновляется и в индексе.
После запуска всех сервисов можно проверить:
systemctl status opensearch
systemctl status opensearch-api
и выполнить тестовый запрос:
curl http://<адрес>/opensearchvash
Если возвращается информация об индексе - связка работает.
OpenSearch показал себя не как модная альтернатива, а как инструмент инженерной зрелости.
Векторный поиск был красивым экспериментом, но не выдержал реальности больших систем.
OpenSearch стал надёжным компонентом, ускоряющим поиск и фильтрацию при больших объёмах данных. Он не заменяет СУБД и не обеспечивает транзакционную целостность, но позволяет масштабировать доступ к данным без потери согласованности при грамотной событийной интеграции.
Главный принцип:
База хранит истину. OpenSearch отвечает за скорость. Триггер следит за честностью.
Так система не просто ищет быстро - она ищет правильно.
Отдельная благодарность @Follooower [9] за участие в проекте и вклад в построение архитектуры и реализацию решения.
Автор: TrickyArch
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/postgresql/434939
Ссылки в тексте:
[1] Зачем нужен поисковый слой: #%D0%B7%D0%B0%D1%87%D0%B5%D0%BC-%D0%BD%D1%83%D0%B6%D0%B5%D0%BD-%D1%8D%D1%82%D0%BE%D1%82-%D1%81%D0%BB%D0%BE%D0%B9
[2] Почему не сработал векторный поиск: #%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83-%D0%BD%D0%B5-%D1%81%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D0%BB-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D1%8B%D0%B9-%D0%BF%D0%BE%D0%B8%D1%81%D0%BA
[3] ACID и CDC: #%D0%BA%D0%B0%D0%BA-%D0%BE%D0%B1%D0%B5%D1%81%D0%BF%D0%B5%D1%87%D0%B8%D0%B2%D0%B0%D0%B5%D1%82%D1%81%D1%8F-acid-%D0%BF%D0%BE%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5
[4] Архитектура под капотом: #%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0-%D0%BF%D0%BE%D0%B4-%D0%BA%D0%B0%D0%BF%D0%BE%D1%82%D0%BE%D0%BC
[5] Почему это работает: #%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83-%D1%8D%D1%82%D0%BE-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D0%B5%D1%82
[6] Как это сделать: #%D0%BA%D0%B0%D0%BA-%D1%8D%D1%82%D0%BE-%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C
[7] Вывод: #%D0%B2%D1%8B%D0%B2%D0%BE%D0%B4
[8] официального сайта OpenSearch: https://opensearch.org/downloads.html
[9] @Follooower: https://www.pvsm.ru/users/follooower
[10] Источник: https://habr.com/ru/articles/961114/?utm_source=habrahabr&utm_medium=rss&utm_campaign=961114
Нажмите здесь для печати.