LEX — AI Legal Platform for Law Firms

AI-powered legal analysis platform for law firms and corporate counsel.

Features

Resources

Blog Articles

Technology

Built on AWS (EC2, Bedrock Claude AI, ALB, WAF, S3, ACM, KMS). PostgreSQL, Redis, Qdrant vector database. TypeScript, React, Node.js.

Start free — 50 credits on registration. Sign up

TECH 15 мин

Как мы синхронизируем 380M+ записей из 40+ источников данных, которые постоянно падают

Multi-IP импорт, автоматический scheduler, freshness-мониторинг, международная экспансия — инженерия data pipeline для открытых данных. От первого 404 до стабильного обновления 110+ таблиц каждую ночь.

Как мы синхронизируем 380M+ записей из 40+ источников данных, которые постоянно падают

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

Эта статья — инженерный разбор того, как мы построили полностью автоматизированный pipeline синхронизации для 380+ миллионов записей из 40+ источников. От архитектуры multi-IP импорта до cron-scheduler'а, системы мониторинга freshness и международной экспансии на 6 юрисдикций.

Обновлено: май 2026 — актуальные цифры с production-серверов.


Проблема: государственные API — это не Stripe

Когда вы работаете с API data.gov.ua, НАИС, УИПВ или spending.gov.ua, вы сталкиваетесь с реальностью:

Мы не можем позволить себе ручной импорт. Юристы рассчитывают на актуальность данных: реестр разыскиваемых лиц должен обновляться ежедневно, а не ежемесячно.


Архитектура: три уровня надёжности

Наш pipeline состоит из трёх независимых компонентов:

┌─────────────────────────────────────────┐
│  opendata-sync (Docker container)       │
│  ├─ node-cron scheduler                 │
│  ├─ 26 источников по расписанию          │
│  └─ Triggers → backend / openreyestr    │
└───────────┬─────────────────┬───────────┘
            │                 │
            ▼                 ▼
┌───────────────────┐ ┌──────────────────┐
│  ImportTaskService │ │  OpenReyestr     │
│  (mcp_backend)     │ │  sync-registry   │
│  ├─ 10 source IP   │ │  ├─ ZIP download │
│  ├─ round-robin    │ │  ├─ XML parsing  │
│  ├─ retry logic    │ │  └─ UPSERT       │
│  └─ progress track │ │                  │
└────────┬──────────┘ └────────┬─────────┘
         │                     │
         ▼                     ▼
┌─────────────────────────────────────────┐
│  PostgreSQL: 110+ data таблиц (1.26 TB) │
│  Мониторинг: db-status.py + freshness   │
└─────────────────────────────────────────┘

Уровень 1: Scheduler — opendata-sync

Первый уровень — лёгкий Node.js микросервис, который не загружает данные сам. Он отвечает только за расписание и триггеры.

Конфигурация источников

Каждый источник описан декларативно:

{
  name: 'mvs_wanted_persons',
  title: 'МВД — Лица в розыске',
  cron: '0 3 * * *',           // 03:00 ежедневно
  target: 'backend',           // куда отправить триггер
  sourceName: 'mvs_wanted_persons',
  enabled: true
}

Расписание синхронизации

Время Источники Целевой сервис
03:00 ежедневно МВД розыск, МВД пропавшие, МВД авто, МВД недействительные паспорта, НАЗК коррупционеры, НАЗК правонарушители backend
03:30 ежедневно Статусы дел, расписание заседаний, адвокаты, люстрация, госпомощь, крупные налогоплательщики, должники по зарплате backend
04:00–05:00 ежедневно Арбитражные управляющие, банкротство, исполнительные, должники openreyestr
Воскресенье 02:00 УИПВ патенты, марки, модели, образцы backend
Понедельник 02:00–05:00 Нотариусы, судебные эксперты, спецбланки, улицы, АТУ openreyestr

Защита от дублирования

Перед каждым триггером scheduler проверяет, не запущен ли уже импорт этого источника. Если статус — running, новая задача не создаётся.


Уровень 2: ImportTaskService — multi-IP импорт

Это сердце pipeline. Когда scheduler отправляет триггер, ImportTaskService берёт на себя всю работу по загрузке.

Три режима импорта

Государственные источники используют разные форматы, поэтому мы поддерживаем три стратегии:

Режим Источники Как работает
api_paginated УИПВ (патенты, марки) Постраничный обход API, 1100ms между запросами
json_array МВД, НАЗК Один HTTP-запрос → массив JSON объектов
file_download НАИС реестры ZIP → XML → парсинг → UPSERT

Multi-IP: 10 адресов × 5 потоков = 50 параллельных загрузок

Для источников с rate limits по IP-адресу мы используем пул из 10 сетевых интерфейсов (AWS ENI). Страницы распределяются round-robin:

Страница 1  → IP 172.31.x.1
Страница 2  → IP 172.31.x.2
...
Страница 10 → IP 172.31.x.10
Страница 11 → IP 172.31.x.1  (снова первый)

С 5 потоками на каждый IP получаем 50 параллельных соединений. Для УИПВ с rate limit 1100ms/запрос это даёт ~45 страниц/секунду вместо 0.9.

Retry с exponential backoff

Каждый запрос имеет до 5 попыток с нарастающей задержкой:

Попытка 1: сразу
Попытка 2: через 2 секунды
Попытка 3: через 4 секунды
Попытка 4: через 8 секунд
Попытка 5: через 16 секунд

Для ошибки 429 (Too Many Requests) — отдельная логика: ждём Retry-After из ответа сервера.

Отслеживание прогресса без нагрузки на базу

Прогресс хранится в памяти и записывается в PostgreSQL каждые 100 страниц:

// В памяти — обновление каждую страницу (микросекунды)
taskProgress.set(taskId, {
  pagesDone: 4521,
  recordsImported: 45210,
  currentPage: 4522,
  lastError: null
});

// В базу — flush каждые 100 страниц
// UPDATE import_tasks SET pages_done=$2, records_imported=$3 WHERE id=$1

Это даёт точный real-time прогресс через API без нагрузки на базу тысячами UPDATE-запросов.

MCP-инструменты для управления

Весь процесс управляется через 4 MCP-инструмента:

Инструмент Назначение
list_import_sources Каталог всех источников: URL, тип, таблица, rate limit
start_import Запуск фоновой задачи: source_name → task_id
get_import_status Прогресс: %, ETA, скорость, ошибки
cancel_import Остановка через AbortController с сохранением прогресса

Это означает, что AI-ассистент может сам запустить импорт, следить за прогрессом и уведомить юриста, когда данные обновлены.


Уровень 3: Мониторинг freshness

Данные без мониторинга — это тикающая бомба. Мы построили систему, показывающую насколько свежие данные в каждой таблице.

Матрица ожидаемой частоты

Частота Таблиц Примеры
Ежедневно (1д) 24 Розыск МВД, недействительные паспорта, коррупционеры НАЗК, должники, исполнительные, статусы дел, адвокаты
Еженедельно (7д) 48 Патенты, марки, санкции OpenSanctions, депутаты, судьи, законопроекты
Ежемесячно (30д) 8 Графики заседаний, крупные налогоплательщики, судебные эксперты, спецбланки

Индикаторы freshness

🟢 в пределах нормы (freq × 1.5)      — всё работает
🟡 немного просрочено (freq × 1.5–2.5) — стоит проверить
🟠 просрочено (freq × 2.5–4)           — что-то пошло не так
🔴 критично (> freq × 4)               — нужно вмешательство
⛔ импорт завершился с ошибкой
🔄 импорт работает сейчас

Dashboard: db-status.py

Скрипт подключается к production-базе через SSH и показывает полную картину:

═══════════════════════════════════════════════════════════════
  📦 SecondLayer (основная) — 110+ таблиц, 1.26 TB всего
═══════════════════════════════════════════════════════════════
  #   Таблица                            Строк  Размер  Норма  Давность
  ──────────────────────────────────────────────────────────────────────
  1   opendata_vehicle_registrations   19.6M  5.9 GB    7д   3д назад   🟢
  2   spending_acts                     9.45M  8.3 GB    7д   5д назад   🟢
  3   opendata_invalid_passports        2.89M  1.0 GB    1д   2мин назад 🟢
  4   opendata_court_case_status        1.25M  846 MB    1д   12мин назад🟢
  5   opensanctions_entities            1.25M  522 MB   30д   8д назад   🟢
  6   opendata_trademarks                382K  4.3 GB    7д   3д назад   🟢
  7   opendata_patents                   345K  5.0 GB    7д   3д назад   🟢
  8   opendata_missing_persons           117K  119 MB    1д   12мин назад🟢
  9   opendata_wanted_persons             71K   49 MB    1д   2мин назад 🟢
  10  opendata_corruption                 58K  106 MB    1д   3ч назад   🟢
  ...

Реальные проблемы и как мы их решили

Проблема 1: Docker не может bind к ENI IP

json_array источники (МВД, НАЗК) — это один HTTP-запрос, не пагинация. Когда мы передавали ENI IP для bind, Docker-контейнер получал EADDRNOTAVAIL — он не видит host-сеть.

Решение: multi-IP нужен только для пагинированных источников. Для json_array — обычный fetch без bind.

Проблема 2: URL исчезают без предупреждения

data.gov.ua периодически обновляет resource ID для МВД и НАЗК. Старые URL возвращают 404.

Решение: URL хранятся в таблице import_source_catalog, а не захардкожены. Обновление URL — один UPDATE-запрос, без пересборки кода.

Проблема 3: NULL bytes в PDF/XML

Некоторые реестры содержат \x00 символы, которые PostgreSQL отклоняет:

ERROR: invalid byte sequence for encoding "UTF8": 0x00

Решение: strip null bytes на этапе парсинга, до INSERT.

Проблема 4: Ответ — не JSON

Когда сервер перегружен, некоторые API возвращают HTML-страницу ошибки или пустую строку вместо JSON.

Решение: парсинг обёрнут в try/catch с проверкой Content-Type. Если ответ не JSON — retry с следующего IP.

Проблема 5: Утечка памяти на больших импортах

Импорт 9.45M записей spending_acts держал все записи в памяти.

Решение: streaming парсинг — обработка chunk'ами по 1000 записей, UPSERT, освобождение памяти.


Цифры

Метрика Значение
Общий объём данных 380M+ записей, 1.26 TB (2 базы)
Количество источников 26 в import_source_catalog + 20 международных импортёров
Количество таблиц 110+ data-таблиц (31 opendata + 20 spain + 43 openreyestr + 50+ ЕГРСР партиций)
MCP-инструментов для поиска 30+ (opendata + spending + registries + international)
Ежедневная синхронизация 12 источников (03:00–05:00 UTC)
Еженедельная синхронизация 14 источников (выходные)
Параллельных соединений до 50 (10 IP × 5 потоков)
Время полного импорта УИПВ ~45 мин (345K записей)
Время импорта МВД розыск ~30 сек (71K записей, один запрос)
Крупнейшая таблица enforcement_proceedings: 29.4M записей, 19 GB
Международные юрисдикции 6 (Испания, Ирландия, Нидерланды, Швейцария, Люксембург, ЕС)

Международная экспансия: от 15 украинских источников до 40+ глобальных

С марта 2026 pipeline вышел далеко за пределы украинских реестров. Вот что добавилось:

ICIJ Offshore Leaks — 4.9M записей

Полная база Panama Papers, Paradise Papers, Pandora Papers. 814K entities, 771K officers, 2.9M relationships, 402K addresses. Импорт из CSV за ~2 минуты, данные обновляются при каждом новом leak.

Испания — 20 таблиц, 780K записей

Самый сложный международный импорт. 14 источников: Tribunal Constitucional (27K решений), BOE (48K объявлений + 12K законов), BORME (276K компаний), EUR-Lex (8.6K актов), CENDOJ (2.3K уголовных решений). CENDOJ оказался geo-blocked для non-EU IP — пришлось использовать Playwright + auto IP rotation (81 ротация EIP, 3 параллельных EC2 workers).

Нидерланды — 1.1M судебных решений

Rechtspraak Open Data API — 1,106,921 решение. Один из самых чистых API среди всех источников: XML с чёткой схемой, пагинация работает, rate limits документированы.

Швейцария — 661K судебных решений

Entscheidsuche.ch — федеральные и кантональные суды. Zefix (1.7M компаний) и SHAB (2.18M HR записей) пока заблокированы из-за 403/timeout.

Ирландия — 812K компаний

Companies Registration Office (CRO) — полный реестр ирландских компаний.

Люксембург — 3.3M записей

GLEIF LEI — Global Legal Entity Identifier. 3,282,067 записей международных юридических лиц.

OpenSanctions — 1.25M записей

Агрегированный санкционный список: 1,020K физических лиц, 108K компаний, 71K юридических лиц. 330 уникальных датасетов со всего мира.


Что дальше

✅ Сделано из предыдущего плана

🔜 Следующие шаги

  1. ЕГРСР fulltext gap 2022-2026 — 32.9M документов без полного текста, активный backfill через /Review/ endpoint (~4M уже восстановлено)
  2. Qdrant hybrid search — векторы ЕГРСР (103M+ points) таймаутят на 60с, нужен tune HNSW или ожидание завершения индексации
  3. Испания Tier 2 — ещё 12 импортёров: Plataforma Contratación (~5-8M тендеров), Congreso votes (~25M), CENDOJ non-penal, Catastro INSPIRE
  4. Швейцария — 12 импортёров на ~9.2M записей: kantonsblatt.ch, fedlex, parlament.ch, Zefix, opendata.swiss
  5. data.gov.ua OSINT — обнаружено 150+ новых датасетов категорий P0-P2, постепенная интеграция
  6. Alerting — Telegram-бот для уведомлений о failed imports

Вывод

Построить pipeline для открытых данных — это не про fetch → insert. Это про инженерию надёжности: retry, rate limit, multi-IP, мониторинг freshness, graceful degradation. А когда pipeline выходит на международный уровень — это ещё и про Playwright для geo-blocked сайтов, EIP rotation для обхода бан-листов и парсинг XML-схем 6 разных юрисдикций.

Каждый из 40+ источников — это отдельная история с уникальными проблемами. Но когда pipeline работает стабильно, юрист задаёт вопрос в чат и получает актуальные данные из МВД, НАЗК, УИПВ, НАИС, spending.gov.ua, ICIJ, Rechtspraak и CENDOJ — даже не задумываясь, сколько инженерной работы стоит за каждым ответом.


Регистрация: legal.org.ua