Миграция с PostgreSQL на MongoDB и оптимизация фронтенда для Ollama-клиента | AiManual
AiManual Logo Ai / Manual.
10 Июн 2026 Гайд

Эволюция клиента для Ollama: миграция с PostgreSQL на MongoDB и оптимизация фронтенда

Практический опыт перехода с PostgreSQL на MongoDB в LLM-клиенте для Ollama. Как решили проблемы зависаний, ускорили streaming и оптимизировали фронтенд.

Реклама
hor_partv1

Начало: когда реляционка ломает LLM

Два года назад мы запускали клиент для Ollama с амбициями: легковесный, быстрый, с поддержкой длинных сессий и streaming. Выбрали PostgreSQL — казалось логичным: ACID, индексы, знакомый ORM. Через полгода это решение аукнулось так, что пришлось переписывать половину бэкенда.

Проблема оказалась не в том, что PostgreSQL плох. Он отличный — для CRM, банков, трекеров задач. Но когда ты хранишь истории чатов с LLM, каждый диалог — это вложенный JSON: промт, ответ, параметры модели, метаданные, цепочки мыслей. Всё это разнородно, меняется от версии модели к версии. Попробуйте натянуть нормальную форму на то, что в каждом сообщении может быть разный набор полей — и вы поймете боль.

Я уже писал о том, как защитить локальные LLM от утечек — тогда мы ещё не знали, что узким местом станет БД. Но давайте по порядку.

PostgreSQL: первый удар по производительности

Основные грабли, на которые мы наступили:

  • JSONB — не панацея. Индексы на вложенные поля работают, но @> оператор для поиска в глубоких структурах тормозит при 100k+ записей. А у нас их миллионы.
  • Блокировки на вставку. Streaming ответа LLM означает частые UPDATEs того же сообщения (добавляются токены). Даже при row-level locking конфликты на высокой конкуренции (10+ одновременных сессий) приводили к дедлокам.
  • Схема кричит о помощи. Каждая новая версия модели добавляла поля. Миграции с ALTER TABLE на 10M строк превращались в многочасовой ад.

Какой вывод? LLM-данные по своей природе — документы, а не таблицы. Реляционная модель добавляет оверлей, который не нужен.

Ошибка: Мы любили PostgreSQL за его надежность и пытались впихнуть документальную модель в реляционную. Не делайте так. Если ваши данные — это JSON с непредсказуемой структурой, берите документную БД с рождения.

MongoDB: почему не Couchbase, не DynamoDB?

Сравнение вариантов было быстрым. Couchbase сложен в настройке кластера. DynamoDB — привязка к AWS, мы за self-hosted. MongoDB 7.x (на момент миграции) давал то, что надо: гибкая схема, репликация, транзакции (да, они появились), агрегация для аналитики. И главное — встроенный TTL-индекс для автоочистки старых логов.

Критерий PostgreSQL MongoDB
Гибкость схемы Низкая (миграции) Высокая (схема на лету)
Производительность streaming обновлений Средняя (блокировки) Высокая (upsert без блокировок)
Индексация вложенных полей Ограничена (GIN) Полноценная (compound + text)
TTL для устаревших данных Нет нативных возможностей Есть (TTL index)

Миграция: как перетащить 5 миллионов документов без даунтайма

1 Подготовка: дуалируем запись

Мы не выключали старую БД. Включили dual-write: каждое изменение писалось и в PostgreSQL, и в MongoDB. Неделю мониторили ошибки, сверяли данные скриптом. Выявили 0.3% расхождений — в основном из-за таймингов streaming. Поправили логику повторных попыток.

2 Батчевый экспорт/импорт

# Пример скрипта миграции (упрощённо)
import psycopg2
from pymongo import MongoClient
from datetime import datetime

pg_conn = psycopg2.connect("dbname=ollama host=...")
mongo_client = MongoClient("mongodb://...")

cursor = pg_conn.cursor("select * from conversations where created_at > '2025-01-01'")
batch = []
for row in cursor:
    doc = {
        "_id": row[0],
        "title": row[1],
        "messages": row[2],  # уже JSON
        "model": row[3],
        "created_at": row[4],
        "updated_at": row[5]
    }
    batch.append(doc)
    if len(batch) >= 1000:
        mongo_client.conversations.insert_many(batch, ordered=False)
        batch = []
if batch:
    mongo_client.conversations.insert_many(batch)

Ключевое: ordered=False — вставка без остановки при дубликатах. Запускали в часы наименьшей нагрузки.

3 Переключение чтения

После проверки целостности перевели read-traffic на MongoDB. Write оставался dual ещё неделю — чтобы откатиться если что. Потом отключили PostgreSQL.

Совет: Не пытайтесь сделать миграцию за один день. Двойная запись на две недели — это не роскошь, это страховка. Я видел проекты, где откат с MongoDB на PostgreSQL после недели тестов стоил команде двух недель нервов.

Фронтенд: когда браузер задыхается от 10k токенов

Отдельная боль — сам клиент. Мы использовали React + Zustand + React Query. Когда модель генерирует длинный ответ (суммаризация документов на 50k токенов), браузер тупил: перерисовка стейта на каждый новый токен вешала UI на секунду. Решение — неожиданное.

Что НЕ работает: тупая отписка по таймеру

Первая мысль: обновлять интерфейс раз в 200 мс, а не на каждый чанк. Но это давало рывки — текст появлялся порциями, нарушая плавность. Пользователи жаловались.

Решение: виртуализация + streaming в canvas

Мы переписали компонент сообщения на @tanstack/react-virtual. Суть: рендерить только видимые сообщения, а потоковые токены — записывать напрямую в canvas через Web Worker. Звучит сложно? На деле — 150 строк кода.

// Упрощённая версия обработчика streaming с отрисовкой через requestAnimationFrame
let pendingTokens = [];

function streamHandler(chunk) {
  pendingTokens.push(chunk);
  if (pendingTokens.length === 1) {
    requestAnimationFrame(flushTokens);
  }
}

function flushTokens() {
  const batch = pendingTokens.splice(0);
  // обновляем store оптом, без перерисовки компонентов
  useChatStore.setState(state => ({
    messages: updateLastMessage(state.messages, batch.join(''))
  }));
  if (pendingTokens.length > 0) {
    requestAnimationFrame(flushTokens);
  }
}

Фокус в том, что React не видит каждый токен отдельно — он получает один батч за кадр. Это снизило количество ререндеров с 100 до 16 в секунду. UI перестал фризить.

Если вам кажется, что это overengineering — вспомните, что EdgeVec + локальная LLM работают в браузере через WebAssembly. Мы пошли чуть проще, но идея та же: не насиловать DOM.

Ошибки, которые мы совершили (и вы, скорее всего, тоже)

  • Не настроили TTL index сразу. В MongoDB мы храним логи запросов к моделям для статистики. Без TTL база разрослась до 200 ГБ за месяц. Пришлось срочно писать скрипт очистки.
  • Забыли про индексы на вложенные массивы. Поиск сообщений по содержимому требовал $text индекс на поле messages.text. Сначала не поставили — запросы висели по 10 секунд.
  • Использовали find().toArray() для пагинации. Без limit и skip при 500k документах — OOM на ноде. Перешли на курсоры.
  • Доверились дефолтному драйверу. pymongo без пула соединений — узкое место. Настроили maxPoolSize=50, и всё встало на место.

Больная мозоль: В одном из проектов коллеги решили не мигрировать, а докрутить PostgreSQL — повесили триггеры на каждое обновление streaming. В итоге latency подскочила до 500 мс. Не пытайтесь лечить симптомы, если болезнь в архитектуре. Если вам интересны альтернативы — посмотрите сравнение vLLM и llama.cpp, там похожая дилемма: старое vs новое.

Что изменилось после миграции?

Цифры говорят сами за себя:

  • Время записи streaming снизилось с 300 мс до 15 мс в среднем.
  • Объём хранимых данных уменьшился на 40% за счёт отказа от нормализации (раньше хранили части в отдельных таблицах).
  • Фронтенд перестал “зависать” на длинных ответах — FPS держится 60 даже при 30k токенов в чате.
  • Добавить новую метрику в лог сессии стало делом 5 минут — не надо создавать миграцию.

Конечно, MongoDB не серебряная пуля. Транзакции у неё не такие надёжные, как в PostgreSQL (хотя в 7.0 стали лучше). Для финансовых данных я бы всё равно выбрал реляционку. Но для LLM-приложений — документная модель побеждает.

Финальный прогноз (без воды)

Через два-три года мы увидим специализированные БД для LLM, которые объединят векторные индексы, гибкую схему и встроенную обработку streaming. Возможно, что-то вроде Pinecone + MongoDB в одном флаконе. Но пока — документные СУБД остаются лучшим выбором.

Не повторяйте наших ошибок: не бойтесь менять стек, если архитектура трещит по швам. И обязательно настройте мониторинг — я писал об этом в статье 5 ключевых метрик для стабильной работы self-hosted LLM. Потому что без метрик вы летите вслепую, а с MongoDB это чревато внезапным падением кластера.

Ну и напоследок — если вы всё ещё думаете, стоит ли переезжать с PostgreSQL на MongoDB для своего Ollama-клиента, ответьте на один вопрос: ваши данные — это таблицы или документы? Если второе — ответ очевиден.

Подписаться на канал