Files
codex 826c1d8ca5
CI / tests-and-images (pull_request) Successful in 2m47s
Runner smoke test / runner-smoke (pull_request) Successful in 3s
Merge current main into documentation consolidation
2026-11-03 08:54:25 +03:00

20 KiB
Raw Permalink Blame History

Backend Акылдаш

Версия загрузчика minjust_cbd.py: 0.9.3

Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в ingestion/minjust_cbd.py: его можно запускать как CLI сейчас и импортировать из будущего планировщика backend без запуска отдельного процесса. Для передачи корпуса в production обязательна отдельная процедура CORPUS_HANDOFF.md; она проверяет source/staging, inventory и checksum, решение по code 6, публикацию и готовность индекса. Этот runbook — release gate; команды передачи здесь не дублируются.

Пробная загрузка

Из корня репозитория:

python3 backend/ingestion/minjust_cbd.py --limit 10

В интерактивном терминале отображаются процент, количество документов, ошибки, скорость и примерное оставшееся время.

Полная загрузка выполняется без --limit. По умолчанию архив сохраняется в data/minjust-cbd, который исключён из Git. Повторный запуск пропускает уже загруженные документы; --refresh принудительно проверяет их заново. Если временный идентификатор списка API истечёт или запрос страницы исчерпает повторы во время многодневной загрузки, скрипт пересоздаст список на текущей странице и продолжит автоматически.

В версии 0.1.0 обновление существующих документов выполняется полной проверкой через --refresh. Инкрементальную проверку по lastmod из sitemap следует добавить вместе с backend-планировщиком, когда будет определена частота запуска.

Защита API

Запросы выполняются последовательно, по умолчанию не чаще одного в секунду. Timeout одного ответа — 60 секунд, число попыток — 5, задержка между повторами растёт экспоненциально. При HTTP 429 учитывается заголовок Retry-After. Лимит действует на один процесс, поэтому одновременно следует запускать только один экземпляр загрузчика.

Более осторожный режим:

python3 backend/ingestion/minjust_cbd.py --requests-per-second 0.5

Вызов из будущего backend

from ingestion.minjust_cbd import sync_archive

result = sync_archive(output_path)

Планировщик, очередь задач и PostgreSQL пока не добавлены: модуль не зависит от выбора будущего backend-фреймворка.

Текущий API Минюста и отдельный staging

Старый OpenData endpoint может возвращать 404; для отдельной ограниченной выгрузки официальный сайт предоставляет /api/v1/GetDocument и /api/v1/GetEdition. Адаптер ingestion.minjust_current получает один указанный документ и сохраняет его в отдельное поколение staging:

PYTHONPATH=backend python3 -m ingestion.minjust_current \
  --output /tmp/minjust-current-code6 \
  --document-code 6

Продолжайте прерванную выгрузку только с тем же каталогом и --resume. По умолчанию лимит скачанного текста одного документа — 4 GiB, лимит запросов — один в секунду; каталог не должен совпадать или пересекаться с data/minjust-cbd и data/minjust-normalized. Адаптер сохраняет исходные ответы и checkpoints, публикует папку документа атомарно и не заменяет существующий код в staging. Каталог staging должен быть отдельной директорией режима 0700, принадлежащей текущему пользователю; его родитель должен быть существующим доверенным каталогом. Символические ссылки в generation, checkpoints и каталогах документов отклоняются. Legacy-корни вычисляются от корня репозитория, а не от текущего рабочего каталога. Для /GetEdition?lang=kg официальный ответ помещает кыргызский HTML в contentRu при contentKg=null; архивный язык определяется запрошенным lang, а не именем поля ответа.

Исторические редакции запрашиваются только с exact=true; без этого параметра официальный endpoint возвращал ID последней редакции даже для другого запрошенного ID. Адаптер требует совпадения response ID и запрошенного ID, сверяет названия RU/KY с metadata GetDocument и сохраняет request contract в checkpoint. Старые staging generations без этого contract нельзя возобновить. exact=true и совпадение ID проверены на двух контрольных редакциях; это не подтверждает полноту всего источника.

Порядок редакций сохраняется по ответу /GetDocument и используется как официальный порядок при нормализации и выборе текущей редакции в поисковом индексе. Вложения и встроенные изображения этим адаптером не подтверждаются: staging остаётся неполным до отдельной проверки медиа и не заменяет корпус. Получение полного списка документов не является частью этой ограниченной процедуры.

Нормализация архива

Нормализатор читает исходный архив без изменений и создаёт отдельный набор данных для будущих поиска, API и RAG:

python3 backend/normalization/minjust_cbd.py --limit 10
python3 backend/normalization/minjust_cbd.py

По умолчанию источник читается из data/minjust-cbd, а результат записывается в data/minjust-normalized. Пути можно изменить параметрами --input и --output; --refresh принудительно обрабатывает неизменившиеся документы, --log-level задаёт уровень журнала. Каталоги --input и --output не должны совпадать, содержать друг друга или пересекаться через разрешённые абсолютные пути.

data/minjust-normalized/
  manifest.sqlite3
  documents/<code>/document.json
  documents/<code>/editions/<edition>/edition.json
  documents/<code>/editions/<edition>/<lang>/content.html
  documents/<code>/editions/<edition>/<lang>/content.txt
  documents/<code>/editions/<edition>/<lang>/fragments.json

content.html содержит только разрешённую безопасную разметку, content.txt — текст для поиска, а fragments.json — адресуемые блоки с детерминированными ID и SHA-256. Манифест пропускает документы с неизменившимися исходниками и повторяет документы, обработка которых завершилась ошибкой.

Первая версия не выполняет OCR, перевод, юридические выводы о редакциях, сопоставление фрагментов, загрузку в PostgreSQL/OpenSearch и построение RAG. Внешние и встроенные data:-изображения из HTML удаляются; сведения и пути к локальным изображениям исходного архива сохраняются в edition.json.

Проверка

PYTHONPATH=backend python3 -m unittest backend/test_minjust_cbd.py -v
PYTHONPATH=backend python3 -m unittest backend/test_minjust_normalization.py -v
PYTHONPATH=backend python3 -m unittest backend/test_minjust_opensearch.py -v

Подготовка индекса OpenSearch

Mapping поискового индекса находится в search/minjust-fragments-index.json. Для кыргызского текста он использует icu_analyzer, поэтому в OpenSearch должен быть установлен плагин analysis-icu.

Потоковый экспорт в формат Bulk API без внешних Python-зависимостей:

python3 backend/search/minjust_opensearch.py

По умолчанию создаётся data/opensearch/minjust-fragments.ndjson. Экспорт атомарный и детерминированный; для проверки можно передать --limit 1. Для прямой загрузки без большого промежуточного файла используется --url:

python3 backend/search/minjust_opensearch.py \
  --url http://127.0.0.1:9200 \
  --index akyldash-fragments-dev-v1 \
  --limit 1

Запросы Bulk API ограничены 25 МБ и не разрывают пару action/source. Для полного прохода убрать --limit и выбрать новое имя версионного индекса. После проверки production-индекса следует переключать alias, чтобы удалённые фрагменты не оставались в поиске.

Alias переключается только после refresh, точного сравнения полного fragment count с inventory, обеих языковых canary-проверок и совпадения ожидаемого текущего индекса. Перед cutover обязательно укажите RU/KY запросы и точное имя старого target:

python3 backend/search/minjust_opensearch.py \
  --url http://127.0.0.1:9200 \
  --index akyldash-fragments-v2 \
  --alias akyldash-fragments-current \
  --expected-old-index akyldash-fragments-v1 \
  --canary-ru 'утверждённый русский запрос' \
  --canary-ky 'бекитилген кыргызча суроо'

Wildcard removal, пустой индекс и alias с несколькими target запрещены. Если старый target успел измениться после проверки, OpenSearch отклонит атомарную операцию удаления точного старого alias. --resume повторно проверяет весь ожидаемый inventory, а не только количество документов текущего запуска. Если ответ на alias request потерян после успешного переключения или процесс остановился до записи завершённого checkpoint, повторный запуск проверит фактическую цель alias. Уже опубликованный candidate оставляется на месте, и checkpoint завершается; старый target позволяет безопасно повторить cutover, а любой другой target останавливает операцию.

После каждого принятого Bulk-пакета загрузчик атомарно сохраняет checkpoint и печатает код безопасного возобновления. При временных HTTP 429/5xx, timeout и обрыве соединения запрос повторяется автоматически. Прерванную загрузку можно продолжить без ручного выбора документа:

python3 backend/search/minjust_opensearch.py \
  --url http://127.0.0.1:9200 \
  --index akyldash-fragments-v1 \
  --resume

По умолчанию checkpoint хранится в data/opensearch/<index>.checkpoint.json; путь можно изменить через --checkpoint. Checkpoint привязан к URL, cluster UUID, index UUID, --limit, --alias и SHA-256 нормализованного manifest. При --resume передавайте то же значение --alias; старый checkpoint без alias можно продолжить только без него. Resume отклоняется при любом несовпадении: для обновлённого корпуса или пересозданного индекса нужно создать новый версионный индекс, проверить его и переключить alias. Это не оставляет удалённые trailing-фрагменты старых документов.

HTTP API v1

Запустите публичный read-only API поверх текущего alias и нормализованного корпуса:

PYTHONPATH=backend python3 -m search.api

Он публикует OpenAPI в GET /openapi.json и поддерживает GET /search, /search/filters, /documents/{code}, /documents/{code}/editions и /documents/{code}/editions/{edition}. Значения фильтров возвращаются со стабильным кодом справочника v1 и подписями RU/KY; применяйте code как параметр поиска. API не подменяет отсутствующий язык документа.

Локальный OpenSearch

Стенд использует один узел OpenSearch без Dashboards, устанавливает analysis-icu, выделяет JVM 8 ГБ и доступен только на 127.0.0.1:9200. Индекс хранится в data/opensearch-node на диске проекта.

sudo sysctl -w vm.max_map_count=262144
docker compose -f deploy/local-opensearch/compose.yaml up -d --build
curl http://127.0.0.1:9200/_cluster/health

Security plugin отключён только для локальной разработки; этот compose нельзя публиковать в сеть или использовать в production. Mapping локального стенда также задаёт одну shard и ноль replicas; для production число shard следует рассчитать по размеру корпуса и настроить не менее одной replica.

Оценка качества поиска

search/relevance-set-v1.template.json содержит заготовку для 25 русских и 25 кыргызских запросов. Для каждого запроса человек должен указать реальную формулировку и коды всех релевантных документов; пустая или неполная разметка должна быть отклонена при ручной проверке, а технически некорректная — самим оценщиком. Критерии выбора запросов, релевантности и двойной проверки описаны в search/RELEVANCE_ANNOTATION.md. Рабочую копию следует хранить в игнорируемом каталоге data/, пока набор не проверен и не разрешён к публикации.

Baseline использует поля названия и текста соответствующего языка, оставляет в выдаче один результат на документ и вычисляет макро-средние Recall@10 и MRR@10:

mkdir -p data/search
cp backend/search/relevance-set-v1.template.json data/search/relevance-set-v1.json
PYTHONPATH=backend python3 -m search.evaluate_relevance \
  data/search/relevance-set-v1.json \
  > data/search/baseline-v1.json

Менять веса или анализаторы следует только после фиксации этого baseline и разбора ошибок выдачи.

Диагностический отчёт с измерениями, подтверждёнными ошибками и ограничениями текущего корпуса: search relevance findings.

Внутренняя лаборатория релевантности

Создайте первую учетную запись администратора:

PYTHONPATH=backend python3 -m search.api --users-db data/search-users.sqlite3 --create-admin

Команда запросит и подтвердит пароль admin в терминале. Затем запустите Search API на localhost и откройте http://127.0.0.1:8080/review:

PYTHONPATH=backend python3 -m search.api \\
  --reviews-db data/search-reviews.sqlite3

Лаборатория показывает фактический порядок выдачи OpenSearch, позволяет открыть текст редакции, поставить результату оценку от 0 до 3 и сохранить снимок с комментариями. Пустую выдачу можно сохранить как query-level оценку с обязательным общим комментарием. Поиск и API доступны только после входа. Пользователь admin управляет учетными записями; новые пользователи должны заменить временный пароль при первом входе. Хранилище пользователей data/search-users.sqlite3 нужно резервировать отдельно от поискового индекса. Экспорт доступен авторизованным пользователям через GET /search-reviews/export.

При утере пароля admin используйте PYTHONPATH=backend python3 -m search.api --users-db data/search-users.sqlite3 --reset-admin-password; следующий вход потребует сменить временный пароль.

Для проверки текущей выдачи без будущего HTTP API используйте CLI:

PYTHONPATH=backend python3 -m search.query "как открыть ОсОО" --language ru
PYTHONPATH=backend python3 -m search.query "ЖЧК ачуу тартиби" --language ky

Акылдаш · Search API v0.13.4 · Frontend v0.3.4 · Telegram-бот v0.2.2