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 7 мин

Как мы векторизуем 33.7M судебных решений ЕГРСР через Voyage AI

ЕГРСР — Единый государственный реестр судебных решений Украины — это по сути вся судебная практика в открытом доступе. Сейчас в проде крутится векторизация последней большой когорты — 33.7M гражданских дел через Voyage AI voyage-3.5. Разбираем пайплайн: чанкинг, параллелизм, checkpoint/resume, прод-инцидент с postgres OOM, и сколько это стоит.

Как мы векторизуем 33.7M судебных решений ЕГРСР через Voyage AI

ЕГРСР — Единый государственный реестр судебных решений — это фактически вся судебная практика Украины в открытом доступе. Мы уже векторизовали уголовные, административные, хозяйственные и КоАП-решения. Сейчас в проде крутится векторизация последней большой когорты — гражданских дел (ГПК, justice_kind=1), 33.7 миллиона документов. Разбираем, как это устроено под капотом: какие модели, какой пайплайн, сколько стоит, какие грабли.


Зачем векторизовать суды

Когда юрист ищет "есть ли практика по взысканию с банка комиссии за досрочное погашение" — он не хочет открывать 40 решений и читать их целиком. Он хочет, чтобы система нашла 5 самых релевантных, вытащила ключевые абзацы, показала аргументацию судов. Полнотекстовый поиск (FTS) по ключевым словам этого не даёт — он найдёт все документы, где встречается слово "комиссия", и их будут тысячи.

Для такой семантической задачи нужны векторные представления текста. Модель превращает абзац из решения в точку в 1024-мерном пространстве; семантически близкие абзацы — рядом. Далее kNN-поиск в Qdrant возвращает топ-K ближайших, и LLM формирует ответ на базе именно этих релевантных фрагментов.

Проблема одна: реестр большой. Очень.


Масштаб

В нашей прод-базе лежат полные тексты решений начиная с 2006 года. Разбивка по типу судопроизводства:

В Qdrant-коллекции edrsr_decisions сейчас 76.3M векторов — уже проиндексированные уголовные, админ, хозяйственные, КоАП и первые 3.37M ГПК. После завершения ГПК будет около 195M векторов в одной коллекции.

Для сравнения: типичный RAG-проект содержит 100K — 1M векторов. Наш — на два порядка больше.


Стек

Embedding-модель. voyage-3.5 от Voyage AI. 1024-мерный выход, 6 центов за миллион токенов. Мы тестировали Voyage 3 Large и OpenAI text-embedding-3-large, но выигрыш в качестве для юридических текстов не перекрывал разницы в цене (Voyage 3 Large в 3 раза дороже). На 3.5 у нас уже был индекс предыдущих юрисдикций, поэтому остаёмся на ней для совместимости.

Vector DB. Qdrant, self-hosted в Docker. Одна коллекция edrsr_decisions с HNSW-индексом. Payload содержит doc_id, court_code, judge, cause_num, justice_kind, adjudication_date, judgment_code, category_code, а также chunk_index/total_chunks и текст чанка.

Source-of-truth. PostgreSQL 15, partitioned tables: RANGE по adjudication_date, LIST по adj_year. Полные тексты лежат в edrsr_fulltext, метаданные — в edrsr_documents. JOIN по всем партициям — это 30M+ строк, поэтому пайплайн ходит по году отдельно.

Runtime. Python 3.11, asyncio, aiohttp. Никаких фреймворков — прямой HTTP к Voyage и к Qdrant. 440 строк кода, один файл.


Как нарезаем текст

Судебные решения — длинные. Среднее ГПК-решение — 8-12K символов, самые длинные — до 200K. Voyage принимает до 32K токенов на вход, но качество падает на длинных контекстах, да и один длинный вектор — это плохой retrieval: LLM не поймёт, какой именно абзац релевантен.

Поэтому чанкуем: максимум 2048 символов на чанк, overlap 50 слов между соседями. Разбиваем по абзацам, сохраняя семантическую связность. В среднем одно решение даёт 2.7 чанка.

Каждый чанк в Qdrant получает composite ID (doc_id × 1000 + chunk_index) — без коллизий, и одним payload-filter запросом вытаскиваются все чанки конкретного решения.


Параллелизм и throttling

У Voyage есть rate limit — 2000 RPM на ключ для voyage-3.5. У нас два ключа и round-robin между ними — теоретический потолок 4000 RPM. На практике держим concurrency 50 и получаем стабильно 63 документа в секунду. Это ~170 запросов в минуту на ключ — с большим запасом под rate limit.

Пробовали concurrency 70 — на первых двух миллионах всё ок, дальше процесс зависал на GIL (13% CPU, без прогресса, без ошибок — просто stuck на thread lock). Снизили до 50 — всё пошло ровно, без deadlock и без 429.

Каждая сотня документов триггерит пачку на Voyage (batch_size=500 чанков/запрос), получает эмбеддинги, формирует точки для Qdrant и делает один upsert. При ошибке от Voyage (429, сеть) — exponential backoff с джиттером, максимум 5 ретраев. При ошибке от Qdrant — retry той же пачки.


Checkpoint и resume

На 33.7M документов любой сбой — сеть, OOM, падение контейнера — означает потерю часов работы. Поэтому:

Это уже спасло нас дважды. Первый раз — когда кончилась память у postgres-прод (об этом ниже). Второй — когда Qdrant рестартанулся и потерял API-ключ из env. В обоих случаях мы просто перезапустили с того же checkpoint без дублирования работы.


Прод-инцидент: postgres OOM

На 2.86M документов postgres-прод упал в recovery mode. Причина — несоответствие конфига: shared_buffers=16GB, но контейнерный лимит памяти — 12G. PG пытался аллоцировать больше, чем ему дано, OOM killer убивал процесс.

Фикс в PR #1453: mem_limit: 24G, shm_size: 16g. После перезапуска контейнера с новыми лимитами PG поднялся за 4 секунды и больше не падал. Этот эпизод подсветил важный инфра-паттерн: параметры postgresql.conf (shared_buffers, work_mem, maintenance_work_mem) должны быть согласованы с лимитами контейнера. Иначе система работает до первого всплеска нагрузки, а потом ложится в recovery.

Заодно увеличили swap на локальной dev-машине с 8GB до 24GB — мощная нагрузка на Voyage API генерирует много временных объектов в памяти Python-процесса, особенно когда ещё и Qdrant в фоне перестраивает индекс.


Сколько стоит

Один гражданский документ в среднем даёт 2.7 чанка × 850 токенов = 2300 токенов. При цене voyage-3.5 в 6 центов за миллион токенов один документ стоит 0.014 цента — около 138 микродолларов.

На 10% (3.37M документов) мы потратили 467 долларов за 14.8 часа. Осталось 30.33M документов — это ещё примерно 3,100 долларов и 130 часов (около 5.4 суток непрерывного прогона). Суммарная стоимость полной векторизации ГПК-когорты — около 3,600 долларов.

Для масштаба: за те же деньги на OpenAI text-embedding-3-large мы бы получили только четверть объёма. Voyage выигрывает именно на таких масштабах.


Что это даёт пользователю

Когда гражданская когорта полностью проиндексируется, семантический поиск в LEX AI будет видеть все 195M чанков единой коллекции. Юрист задаёт запрос естественным языком — "судебная практика по признанию недействительным договора купли-продажи из-за недееспособности продавца" — и система возвращает самые релевантные решения из правильной юрисдикции, с извлечением ключевых абзацев, со ссылками на ЕГРСР.

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


TL;DR

Прод крутится в tmux, checkpoint триггерится каждые 1000 документов, мониторинг — tail -1 /tmp/vectorize-cpk.log. Скучная надёжная инженерия вместо героики.