<- записи

Как я выбирал базу данных для Letopis

вечно живо

С того момента, как я узнал о существовании MongoDB, я в неё влюбился — в документную модель, в гибкую схему, в сам синтаксис. При любой подходящей задаче я тянулся к Mongo и почти никогда не жалел: на старте это ноль боли с миграциями и максимум скорости. Поэтому, когда я сел строить Letopis — личную библиотеку знаний, которая живёт в облаке, сама всё упаковывает, ищет по смыслу и раскладывает по полочкам, — я был уверен, что в основе будет Mongo.

В проде стоит PostgreSQL. Это не компромисс, на который я пошёл скрепя сердце, а решение, которое я теперь готов защищать с цифрами. Дальше — как я к нему пришёл, включая места, где Postgres проигрывает: их я не прячу.

Сначала не «какая база», а «чья»

Первое решение я принял ещё до того, как открыл хоть один прайс-лист: база будет у внешнего провайдера. Managed, не свой сервер на VPS.

За этим — простое разделение того, чем я готов заниматься, а чем нет. Я хочу писать продукт, а не дежурить у pg_dump в три ночи, когда у виртуалки отвалится диск. Managed даёт мне две вещи, ради которых всё и затевалось:

  • Меньше администрирования. Бэкапы, обновления версий, отказоустойчивость и лёгкий переезд — на провайдере. Разочарует — я перетащу дамп, а не буду пересобирать инфраструктуру с нуля.
  • База переживёт сервер. Если VPS умрёт или его взломают, база останется жива и невредима — она физически отдельно от него. То, что с ней всё будет хорошо, теперь на совести провайдера, а не на моей :)

Есть причина поважнее: разделённая ответственность за данные пользователей. Когда подписываешь с европейским провайдером DPA со стандартными договорными условиями (SCC), он как процессор берёт на себя инфраструктурную и юридическую часть хранения персональных данных. На мне остаётся прикладной слой: разграничение доступа, удаление по запросу, вычистка персональных данных перед отправкой в LLM. Не «весь GDPR на мне», а та его половина, которую закрывает мой код. Для соло-разработчика это делает разницу.

Где разместить, и почему не СНГ

Дальше география. Я сразу смотрел в сторону ЕС или США: туда смотрит мой рынок (западные славяне — Польша, Чехия, Словакия — и Восточная Европа), там понятные правила с GDPR.

Из любопытства я заглянул к провайдерам СНГ, и вот что оттолкнуло окончательно. Беру Yandex Cloud, их «Managed Service for MongoDB». Оказывается, под капотом не настоящий MongoDB, а StoreDoc — собственная Mongo-совместимая СУБД, которая лишь говорит по протоколу Mongo версий шесть-восемь. Меня это честно удивило: думаешь, что берёшь managed Mongo, а берёшь переименованный движок на протоколе постарше. Постоянного бесплатного тарифа нет, платишь с первого дня и с первого мегабайта — есть только пробный грант на пару месяцев, но это триал, а не freemium.

И «managed» там managed только на словах. Многое делается руками через SSH; поставить нужное расширение можно, но возни выходит немногим меньше, чем поднять базу у себя локально. Я хотел уйти от администрирования, а мне предложили его же, только за деньги.

Но добил меня не прайс и не SSH, а функционал. Моей задаче нужен векторный поиск, а в этот Mongo его не добавить: нативного $vectorSearch на доступных там версиях просто нет, и приделать его сбоку нельзя. Не «дороже» и не «версия постарше» — нужной возможности у движка нет в принципе.

И это не каприз одного провайдера. Даже там, где всё осознанно строят на российской инфраструктуре под законы о локализации данных, вектора всё равно выносят в отдельный PostgreSQL с pgvector, а Mongo оставляют только под тексты. Вывод простой: как только задаче нужны вектора, документная база перестаёт справляться сама, и дорога ведёт к pgvector. Запомните эту мысль, она вернётся.

Итог: ЕС, Франкфурт, GDPR-резидентность из коробки. И уже на этом шаге видно, что выбор юрисдикции — решение в первую очередь юридическое, а уже потом техническое.

Что вообще нужно от базы

Чтобы понять остальное, надо знать мою нагрузку.

Letopis не просто ищет похожие куски текста. Он вытаскивает из заметок сущности и связи между ними и строит из них граф знаний — так система понимает контекст глубже и отвечает на вопросы, где надо пройти по связям, а не просто найти похожий абзац. Граф знаний означает, что мне нужна графовая работа с данными. (Собрано это на LightRAG, но это деталь реализации, а не суть.)

С точки зрения хранилища нагрузка распадается на четыре слоя: кэш и исходные тексты, эмбеддинги для семантического поиска, сама топология графа и статусы индексирования документов.

Под высокую нагрузку зоопарк был бы разумен: Redis под кэш, Pinecone под вектора, Neo4j под граф — каждый инструмент в своей весовой категории. Но у такого решения своя цена: налог на мультибазы (задержки на сетевых прыжках между сервисами), вечная синхронизация и три облачных счёта вместо одного. Я выбрал не упарываться над архитектурой ради архитектуры, а довести продукт до рабочего состояния — лучше одна живая база, чем красивый летающий замок, который не взлетит. Кандидатов с реальным опытом у меня было два: MongoDB и PostgreSQL в лице Supabase.

Я хотел Mongo. Три удара

Честно: я начинал с MongoDB Atlas. Бесплатный тариф есть, Mongo я знаю как свои пять пальцев. А потом начал считать и читать, и получил три удара по нарастающей.

Удар первый: бесплатный тариф

Бесплатный кластер Atlas (M0) — это полгигабайта диска, сотня операций в секунду и запрет писать временные данные на диск в агрегациях. Широко цитируется ещё и лимит на число поисковых и векторных индексов на shared-тарифе, порядка трёх; если он держится, я упираюсь в потолок сразу, потому что моей архитектуре нужно минимум три векторных индекса (под чанки, сущности и связи) на ещё пустой базе. Но даже без спора про индексы связки «полгига плюс сто операций в секунду плюс запрет на временный диск» хватает: графовый RAG на этом не живёт.

Удар второй: цены

Порог входа в платное (на июнь 2026)$/мес
Supabase Pro 25
MongoDB Atlas M10 57

Прыжок с бесплатного на первый нормальный выделенный тариф Atlas (M10) — примерно 57 долларов в месяц, и это базовая цифра: в комьюнити лежит зафиксированный счёт на пятьсот с лишним за тот же M10, когда сверху набегают бэкапы, трансфер и поиск. У Supabase переход на Pro — 25 долларов. Порог входа вдвое ниже. А в serverless-модели Atlas, где платишь за операции, всплывает совсем неприятное: запись стоит примерно в десять раз дороже чтения, а индексирование графа знаний — это сплошная запись. Я строю систему, которая постоянно дописывает в базу знаний; для меня это приговор.

Удар третий: Postgres оказался быстрее

А вот это сломало мне картину мира. Я был уверен, что для графа нужна специализированная графовая база. Я собрал бенчмарк на своей нагрузке и прогнал три бэкенда: рекурсивные CTE прямо в Postgres, Neo4j и Apache AGE (графовое расширение Postgres).

Как мерил, чтобы было честно: реальный граф документов на восемь тысяч узлов и двадцать пять тысяч рёбер, запросы в один хоп (расширение соседства, ровно как ходит моя система), десять параллельных воркеров; для контроля повторил на синтетическом графе в восемь тысяч узлов и сорок тысяч рёбер — картина та же. Снимал и пропускную способность, и время отклика.

Бэкенд графаRPS, синтетика (8k/40k)RPS, реальный (8k/25k)p50 / p95, реальныйЗависимости
Postgres · рекурсивные CTE12 7769 1690.7 / 1.4 мслюбой PostgreSQL 14+, ноль внешних зависимостей
Neo4j1 6841 6934.2 / 13.2 мсвыделенный граф-сервер или DBaaS AuraDB
Apache AGE1751594.6 / 239 мскастомный образ БД с компиляцией AGE
MongoDB ☠нативного графа нет; только $graphLookup

Чистый SQL в обычном Postgres обогнал выделенный Neo4j в пять с лишним раз и прибил Apache AGE почти в шестьдесят. На моём уровне графов.

Эту оговорку, «на моём уровне графов», я обязан проговорить. Строй я систему, которая ищет кратчайшие пути в графах на миллионы узлов с глубокими обходами, Neo4j разнёс бы Postgres: он хранит связи как прямые указатели между узлами, и глубокий обход для него почти бесплатен — на дереве в треть миллиона узлов рекурсивный CTE отрабатывает за 47 секунд там, где нативный графовый движок укладывается в 227 миллисекунд. Но моя нагрузка не такая: граф ходит на один-два хопа от узлов, которые уже нашёл векторный поиск. Ровно там, где Postgres силён, а Neo4j избыточен.

Внутри Postgres я рад, что вовремя выбрал правильный вариант. Очевидный путь — Apache AGE, но его почти нигде нет в каталогах managed-облаков: пришлось бы тащить кастомный Docker-образ на свою машину и потерять весь zero-ops, плюс он ловит взаимные блокировки под нагрузкой. Я выбрал рекурсивные CTE — просто SQL поверх двух обычных таблиц, без единого внешнего расширения.

А у Mongo на графовом поле всё ещё хуже, поэтому в таблице у него вместо чисел — череп. Нативного графа нет вообще, есть только обходной оператор $graphLookup. В каноническом бенчмарке NoSQL от ArangoDB от него пришлось вовсе отказаться — настолько медленно он отрабатывал, что кратчайшие пути на Mongo даже не стали замерять; сама MongoDB в доках признаёт, что $graphLookup «может быть не таким производительным, как выделенная графовая БД». LightRAG формально умеет работать и с MongoDB как бэкендом, но цифры выше ставят целесообразность этого под сомнение.

Честная сноска про цифры: бенчмарк выше — мой, на интеграции LightRAG, а не независимая лаборатория. Я привожу его как «вот что я намерил у себя», а не как истину в последней инстанции.

Оказалось, Supabase — это больше, чем база

Пока я копал в сторону Postgres, всплыли вещи, на которые я даже не закладывался.

Вектора. Расширение pgvector делает семантический поиск прямо в базе. Современные модели эмбеддингов выдают вектора такой размерности, что обычный индекс их не берёт, и тут выручает тип halfvec (16-битные числа вместо 32-битных): он режет память вдвое ценой примерно одного пункта recall, а на больших размерностях вообще оказывается единственным способом построить индекс.

По пропускной способности Postgres-стек с векторным расширением на больших объёмах обгоняет даже выделенные векторные базы в разы — но честно: на хвостовой латентности выделенный Qdrant его всё же обходит.

Метрика (recall 99%, 50М векторов, 768d)Postgres + векторное расширениеQdrant
Throughput472 QPS41 QPS
Латентность p5031 мс31 мс
Латентность p95 (меньше лучше)60 мс37 мс

Оговорюсь честно: эти кратные цифры throughput — про оптимизированное векторное расширение (pgvectorscale в бенчмарке Tiger Data), а не про «голый» pgvector, который стоит у меня.

Но на масштабе MVP — тысячи и десятки тысяч фрагментов — этого запаса с головой хватит не только мне, а большинству продуктов на старте. Платить за отдельный векторный сервис ради хвостовой латентности, которую я не почувствую, не мой случай.

Аутентификация. Supabase приносит встроенный Auth: 50 тысяч активных пользователей бесплатно, 100 тысяч на Pro. Для соло это минус целый сервис — мне не надо поднимать и поддерживать свой провайдер входа.

И главное — всё в одной базе. Сами байты заметок, вектора, граф, полнотекстовый поиск — в одном PostgreSQL, с ACID-консистентностью. Это не теория: я проверил это кодом на боевом Supabase ещё на прототипе. Один движок закрывает все четыре слоя. Mongo так не умеет, а Yandex с его отсутствующим векторным поиском тем более.

Помните мысль из второй главы, «как только нужны вектора, дорога ведёт к pgvector»? Вот она и замкнулась.

Чем кончилось

Фаворит определился сам: Supabase, Франкфурт, Pro за 25 долларов в месяц. Вектора — pgvector с типом halfvec. Граф — рекурсивные CTE, без Apache AGE. Auth — встроенный. Один счёт, ноль ops, EU-резидентность из коробки.

Я не разочаровался в NoSQL. Похоже, нереляционные базы по-настоящему раскрываются на большом масштабе, когда данных столько, что реляционная модель начинает трещать; а пока я там не живу, Postgres в лице Supabase в очередной раз выигрывает. Без обиды на Mongo: просто инструмент под текущий масштаб. Я уехал на Postgres не потому, что разлюбил Mongo, а потому что для этой конкретной задачи он оказался лучшим выбором. Иногда самый скучный инструмент в комнате и есть правильный ответ.


Цены и лимиты — на июнь 2026. Бенчмарки графовых бэкендов — мои собственные измерения на интеграции LightRAG. Если строите похожее, не верьте на слово, прогоните на своей нагрузке.

просмотров · 26

Комментарии

пока тихо

· · ·Будьте первым — анонимно или под ником.
-> отобразится как аноним#…
или войти, чтобы писать под своим #id