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 — актуальні цифри з продакшн-серверів.


Проблема: державні 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, нова задача не створюється.

Health endpoint

Scheduler надає ендпоінт /health з повною картиною:

{
  "status": "healthy",
  "uptime": "4d 12h 33m",
  "sources": 15,
  "recentFailures": 1,
  "lastFailure": {
    "source": "nipo_trademarks",
    "error": "ECONNRESET",
    "at": "2026-03-27T02:15:00Z"
  }
}

Рівень 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 з відповіді сервера.

Progress tracking без навантаження на базу

Прогрес зберігається в пам'яті і записується в 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

Скрипт підключається до продакшн-бази через 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. pg_stat_user_tables — коли було останнє INSERT/UPDATE
  2. import_tasks / import_log — статус останнього імпорту (success/failed/running)

Реальні проблеми і як ми їх вирішили

Проблема 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

Коли сервер перевантажений, замість JSON деякі API повертають HTML-сторінку з помилкою або порожній рядок.

Рішення: парсинг обгорнуто у 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 records) поки заблоковані через 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