Commit Graph

36 Commits

Author SHA1 Message Date
jze9
26de1b9de1 refactor(ops): массовая заливка — в общий конвейер вместо отдельных скриптов
All checks were successful
Deploy / deploy (push) Successful in 2m28s
Deploy / test (push) Successful in 2m49s
Заливку Википедии и PMC я сделал отдельными скриптами мимо существующей
инфраструктуры: запуск руками через ssh, состояние в файле в /tmp, никакой
видимости. Результат предсказуем — за неделю обе умерли молча (обрыв базы на
20 008 статьях из 2 млн и таймаут сети на 85 тыс. из 100 тыс.), прогресс
потерялся, а узнали мы об этом через неделю. При том что рядом лежит готовый
механизм: parse_sources, прогоны со шкалой, журнал, кнопки, ретраи Celery.

Теперь это обычные типы источника — wikipedia_ru и pmc_bulk:

- заводятся и запускаются из админки, как OpenAlex или КиберЛенинка;
- показывают ту же шкалу, журнал и кнопку остановки;
- падение воркера больше не теряет прогресс: позиция продолжения хранится в
  parse_sources.resume_token (номер статьи в дампе / токен страницы бакета),
  повторный запуск берёт следующую порцию;
- укладываются в бюджет времени таска — заливка идёт порциями, а не одним
  многосуточным процессом.

Чего не хватало конвейеру для миллионов и что добавлено:
- парсеры отдают генератор, а не список: 2 млн статей в память не влезают;
- app/bulk_writer.py — запись пачками через COPY (21 тыс. строк/с против
  6.7 тыс. построчно) с переподключением к базе при обрыве;
- эмбеддинги при массовой заливке не диспатчатся: они на порядок медленнее и
  стали бы узким местом, вектора досчитываются отдельно (reembed_missing.py).

scripts/ops/bulk_ingest_*.py удалены — их работу делает конвейер.

Проверено на проде: оба парсера отдают документы, прогон через run_parser
завершается штатно, позиция продолжения сдвигается (300 → 600), повторный
запуск продолжает с неё.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 20:32:53 +05:00
jze9
f4092e5331 fix(ops): заливки переживают обрывы базы и сети
За неделю обе массовые заливки умерли молча и не возобновились: Википедия на
20 008 статьях из ~2 млн («server closed the connection»), PMC примерно на
85 тыс. из 100 тыс. (таймаут SSL-рукопожатия). Ни ретраев, ни возобновления.

- запись пачки повторяется с переподключением к базе (5 попыток с паузой);
- обрыв листинга бакета стоит паузы, а не всей заливки;
- у Википедии сохраняется позиция в дампе — после сбоя продолжаем с неё,
  а не проматываем с нуля, полагаясь на дедуп по ext_id;
- батч уменьшен (500 статей × 2000 отпечатков = миллион строк в одной
  транзакции — вероятная причина обрыва);
- обрезка отпечатков переведена на равномерную выборку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 19:13:01 +05:00
jze9
780a0a1061 fix(ops): читать дамп Википедии с диска — сетевой поток рвётся на 5.6 ГБ
All checks were successful
Deploy / test (push) Successful in 3m49s
Deploy / deploy (push) Successful in 4s
Wikimedia закрывает долгие потоковые соединения: обрыв пришёлся на 32 МБ из
5.9 ГБ. Скачать файл с докачкой (curl -C -) и читать локально надёжнее, поэтому
у скрипта появился --dump-file; чтение из сети осталось запасным путём.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:54:59 +05:00
jze9
06363c7401 feat(ops): заливка русской Википедии — единственный доступный источник объёма
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / test (push) Has been cancelled
У корпуса катастрофически мало пригодного русского текста: 99 883 документа
КиберЛенинки лежат со средней глубиной 30 отпечатков, то есть заголовок с
аннотацией. Проверил все русскоязычные источники, о которых имело смысл думать:

- Википедия ru — работает: дамп 5.6 ГБ читается потоком (на app-хосте всего
  22 ГБ свободно, поэтому без сохранения на диск), ~919 отпечатков на статью.
  Wikimedia отдаёт 403 без осмысленного User-Agent — учтено;
- КиберЛенинка — блокирует выкачку после ~130 запросов (замер 28.08);
- OpenAlex language:ru + OA — заявлено 395 410 работ, но по прямым pdf_url
  скачалось 2 из 10, остальное 403 издателей; метка языка ненадёжна — в выдаче
  англоязычные журналы. Массово не годится;
- eLIBRARY.RU — только по договору, Math-Net.Ru — 403 на архиве.

Википедия формально не научный источник, но студенты копируют из неё чаще, чем
из статей, а по объёму связного русского текста альтернатив среди доступного нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:53:10 +05:00
jze9
e4ca4a7150 docs: сервер эмбеддингов переехал на .1.40 + честные итоги проверок
All checks were successful
Deploy / test (push) Successful in 2m59s
Deploy / deploy (push) Successful in 3s
Прод переключён на новый сервер эмбеддингов (VM 210 «embedding-cpu»,
192.168.1.40, хост pve2). Ключевое, чего не было в исходном плане переключения:
OLLAMA_URL живёт в Infisical, и deploy.sh генерирует .env из него на каждом
запуске — правка .env на сервере откатилась бы первым же деплоем.

Совместимость векторов проверена прямым сравнением, а не на слово: косинус
0.9999997, расхождение 1e-4 (округление AVX2 против AVX-512) — переиндексация
93 тыс. векторов не понадобилась.

Документация приведена в соответствие с фактами:
- ARCHITECTURE/DIAGRAM/README/CREDENTIALS: новый сервер, старый помечен как
  выведенный; убрано упоминание CUDA — GPU в проекте нет;
- DR-HA: эмбеддинги больше не висят на хосте .254, чьё падение 28.08 разом
  унесло брокер, прокси и секреты;
- INGESTION: исправлено собственное враньё про КиберЛенинку — «48 часов на
  100 тысяч» опровергнуто практикой, сайт блокирует выкачку после ~130 статей;
  снапшот OpenAlex вычеркнут как путь к миллионам (там только метаданные).

bulk_ingest_pmc.py: снята пометка «не закончен» — бага не было, первый прогон
упал уже после успешной вставки, а второй корректно пропустил дубли. Проверено:
100 статей, 183 090 отпечатков. Реальный темп 0.3 ст/с записан честно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:06:41 +05:00
jze9
2d38dfd1f4 feat(ops): путь к миллионам статей — массовая заливка из бакета PMC (не закончен)
All checks were successful
Deploy / test (push) Successful in 3m12s
Deploy / deploy (push) Successful in 1m56s
Разведка под задачу «нужны миллионы»: постраничные API дают 1-2 статьи в
секунду и упираются в rate limit, а OpenAlex-дамп содержит только метаданные —
миллионы аннотаций бесполезны, что уже доказано на КиберЛенинке (30 отпечатков
на документ, 0 глубоко проиндексированных).

Найден и проверен рабочий источник: AWS Open Data бакет pmc-oa-opendata открыт
без ключа, у каждой статьи лежит готовый извлечённый текст (33-130 КБ) и json с
метаданными. Замер: 12 статей/с в 10 потоков — миллион за сутки.

Замеры узких мест на проде (для планирования масштаба):
- winnowing 95 док/с на ядро — не ограничение;
- поиск L1 31 мс по 500 отпечаткам при 113 млн строк;
- вставка отпечатков построчно 6.7 тыс. строк/с → миллион статей это 2.5 млрд
  строк и четверо суток только на запись, поэтому в скрипте COPY;
- объём: 106 байт на строку → ~400 ГБ на миллион статей с индексами.

Скрипт НЕ закончен: документы вставляются верно, но шаг отпечатков ошибочно
пропускает свежие документы (пробные 200 записей удалены из базы). Ограничение
задокументировано прямо в скрипте, чтобы его не запустили на объёме.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:58:17 +05:00
jze9
d7c004af76 feat(ops): углубление индексации КиберЛенинки + общий store_full_text
Проверка глубины вскрыла главную слабость корпуса: 99 883 документа
КиберЛенинки (56% базы) имеют в среднем 30 отпечатков — заголовок с
аннотацией. Списывание из тела русской статьи L1 не находит, хотя сервис
рассчитан именно на русских студентов. У PMC и arXiv глубина 97% и 94%.

Отдельно: мерить глубину по minio_key оказалось неверно — PMC кладёт тело
статьи в отпечатки при заливке, не сохраняя файл, поэтому «27.5% с полным
текстом» занижало картину для одних источников и скрывало провал у другой.
Честный показатель — число отпечатков на документ, по нему всё и пересчитано.

- backfill_cyberleninka_pdf.py: PDF берётся прямым адресом {url}/pdf (докачка
  по обычному url бесполезна — там HTML, замер 0 из 8). Проверено на 50:
  углублено 44, в среднем 23 тыс. символов, глубина 30 → 1453 отпечатка;
- backfill_pmc_fulltext.py: сохранение тела статьи из API в MinIO (отпечатки
  там уже глубокие — скрипт нужен для отчётов и подсветки, не для детекции);
- store_full_text вынесен из index.enrich_full_text: один путь «текст получен →
  MinIO + пересчёт L1 + обновление L2» для задачи докачки и для бэкфиллов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:45:48 +05:00
jze9
d8630ca0f9 fix(admin): покрытие L3 мерить по индексу, а не по отметкам в базе
Проверка на живых данных вскрыла, что панель отладки врала в мою же пользу:
показывала 154 046 «документов с эмбеддингом» (87%), тогда как в FAISS реально
93 053 вектора (52.5%). Колонка documents.faiss_id для этого непригодна —
отметка остаётся после пересоздания индекса (смена модели: 768 → 1024) и после
сбоев worker-gpu. Выборочная проверка: у 8 из 20 «отмеченных» вектора нет.

- gpu.index_stats — новая задача, отдаёт реальное содержимое активного
  векторного бэкенда; API спрашивает её для панели отладки;
- панель показывает «векторов в индексе» и отдельно предупреждает о ложных
  отметках (их 60 993), потому что такие документы молча выпадают из L3:
  в индексе их нет, а на пересчёт они не попадут — reembed ищет faiss_id IS NULL;
- scripts/ops/faiss_reconcile.py — сверяет отметки с индексом и обнуляет ложные,
  после чего reembed_missing.py отправляет их на пересчёт. Проверен вживую
  (dry-run на проде: 93 053 в индексе, 60 993 ложных отметок).

Документация: зафиксирована реальная глубина корпуса — полный текст только у
27.5% документов, у 79% меньше 500 отпечатков (уровень аннотации). Система
ловит списывание из того, что есть целиком; это граница, а не поломка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:17:41 +05:00
jze9
99bf14fe6a feat(admin): честная длительность прогона + добор эмбеддингов
Разбор итогов массовой заливки показал в отладке «среднюю длительность прогона»
в 2.8 часа там, где парсинг занимал 50 секунд: started_at пишется в момент
постановки в очередь, а очередь из 173 источников разбирается часами. Теперь
момент реального старта пишется отдельно (run_started_at, миграция 006), а
схема отдаёт обе величины — сколько ждал очереди и сколько работал.

Плюс scripts/ops/reembed_missing.py: документ попадает в корпус сразу, а вектор
для L3 считает отдельная задача; когда worker-gpu или Ollama недоступны, эти
задачи теряются и документ остаётся невидимым для семантического поиска. Скрипт
находит faiss_id IS NULL и переотправляет пачками (dry-run по умолчанию) —
сейчас таких 23 101 из 177 147.

Документация: актуальные цифры корпуса, дубли при повторном прогоне, лимит
OpenAlex, и главное — гипервизор .254 зафиксирован в DR-HA как самая широкая
единая точка отказа (брокер, эмбеддинги, секреты и прокси на одном железе;
подтверждено аварией 28.08).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:51:25 +05:00
jze9
f1a8d07cb3 fix(openalex): пауза между страницами и живой backoff — заливка тонула в 429
All checks were successful
Deploy / test (push) Successful in 4m3s
Deploy / deploy (push) Successful in 5s
Массовый запуск показал: лимит вежливого пула OpenAlex (10 req/s) общий на
mailto, а не на процесс. Четыре воркера с паузой 0.1с получали сплошные 429,
и каждый прогон уходил в 900с бесполезного backoff, не забрав ничего.

- RATE_LIMIT_DELAY 0.1 → 1.0с (≈4 req/s на четырёх воркерах);
- backoff спит кусками по 5с и отчитывается через progress_cb: прогон больше
  не выглядит зависшим в админке и отменяется во время ожидания, а не после.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:54:42 +05:00
jze9
78e27806b9 feat(admin): шкала загрузки источников, отладка и загрузка работ в корпус
All checks were successful
Deploy / test (push) Successful in 3m56s
Deploy / deploy (push) Successful in 31s
Заливка корпуса была чёрным ящиком: у источника только last_status
(idle/running/done/error), без «сколько из скольки», без причины падения и без
способа остановить начатое. Теперь каждый запуск создаёт строку parse_runs,
куда воркер раз в ~2с пишет стадию, счётчики и журнал событий.

Админка:
- шкала загрузки у каждого источника (0→50% выборка, 50→100% индексация),
  раскрытая строка — журнал прогона по шагам с таймингами;
- «Запустить всё» / «Остановить всё» и остановка по одному источнику
  (кооперативная отмена: воркер останавливается сам, не рвя запись в базу);
- пакетное добавление источников (тип + список тем), тип pmc в форме;
- загрузка PDF/DOCX/TXT прямо в базу сравнения (index.ingest_upload);
- страница «Отладка»: воркеры Celery и их текущие таски, очереди RabbitMQ,
  покрытие корпуса эмбеддингами, зависшие и упавшие прогоны, конфиг бэкендов.

Защита от краш-лупа по consumer_timeout RabbitMQ (docs/DR-HA.md §6), без неё
массовый запуск 170+ источников гарантированно ронял воркер:
- PARSER_TIME_BUDGET_S (1500с) — прогон закругляется сам и помечается partial;
- worker_prefetch_multiplier=1 — таймаут считается от ДОСТАВКИ сообщения, и с
  дефолтным префетчем очередь долгих run_parser убивала канал на задачах,
  которые ещё не начинались.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:40:23 +05:00
jze9
c152fafa70 feat(deploy): переключить .env на проде на Infisical вместо ручного файла
All checks were successful
Deploy / test (push) Successful in 3m23s
Deploy / deploy (push) Successful in 13s
deploy.sh теперь на каждом запуске логинится в Infisical (Machine Identity
Universal Auth) и генерирует .env из окружения prod перед синком кода и
поднятием контейнеров. Если Infisical недоступен или вернул подозрительно
мало ключей — деплой падает раньше, не трогая рабочий .env на сервере.

Добавлен шаг compose up -d без --build для всех бэкенд-сервисов после
сборки изменившихся — иначе правка секрета без изменения кода не попадала
бы в уже запущенные контейнеры (docker compose пересоздаёт только то, чей
эффективный конфиг реально изменился, остальное не трогает).
2026-08-27 16:25:28 +05:00
jze9
1dd3f1569f fix(indexer): не превышать consumer_timeout RabbitMQ в retry-бэкоффе OpenAlex
All checks were successful
Deploy / test (push) Successful in 3m12s
Deploy / deploy (push) Successful in 3s
5 попыток (1860с суммарно) были чуть больше дефолтного consumer_timeout
RabbitMQ (1800с) — брокер рвал канал раньше, чем таск успевал сдаться и
подтвердиться, воркер падал, docker его перезапускал, сообщение
редоставлялось и весь цикл забега начинался заново с попытки 1 —
бесконечный краш-луп, блокирующий весь пул воркера (concurrency=4).
2026-08-26 20:08:16 +05:00
jze9
344f78b795 fix(openalex): ограничить retry на 429 и добавить экспоненциальный backoff
All checks were successful
Deploy / test (push) Successful in 2m58s
Deploy / deploy (push) Successful in 2s
Было: на 429 плоское ожидание 60с и retry без счётчика — при нескольких
параллельных источниках (4 воркера) каждый продлевал общий rate limit
сам, получался самоподдерживающийся затык без единого успешного запроса
по 20+ минут. Теперь: до 5 попыток с экспоненциальным backoff
(60/120/240/480/960с), после — источник сдаётся с тем, что успел
собрать, вместо вечного retry.
2026-08-26 16:32:02 +05:00
jze9
729b41bbfb feat(corpus): добавить прикладные/техникумовские темы в русский сидинг
All checks were successful
Deploy / test (push) Successful in 3m5s
Deploy / deploy (push) Successful in 2s
Исходный список из 29 дисциплин был чисто вузовски-научным (экономика,
право, психология и т.д.) — ни одной темы уровня СПО/колледжей
(полиграфия, строительство, сварка, туризм и т.п.), хотя реальные
дипломы студентов именно на них. +29 новых дисциплин, идемпотентно
(старые 30 не трогает).
2026-08-25 15:52:12 +05:00
jze9
9f35ae8de1 feat(auth): вход через Google и Яндекс (OAuth2 authorization code flow)
All checks were successful
Deploy / test (push) Successful in 2m48s
Deploy / deploy (push) Successful in 23s
Реализовано без authlib, на голом httpx (AsyncClient — синхронный httpx
блокировал бы event loop API на время внешнего запроса), по образцу двух
провайдеров:

- Миграция 004: hashed_password → nullable (OAuth-юзеры без пароля),
  oauth_provider/oauth_id + уникальный индекс на пару.
- app/core/security.py: verify_password защищён от hashed=None (иначе TypeError
  при попытке OAuth-юзера войти по паролю — нашёл при ревью, не баг-репорт).
- app/core/oauth.py: get_authorize_url()/exchange_code() — единый интерфейс для
  google/yandex. Redirect URI: <APP_URL>/api/auth/<provider>/callback.
- app/api/auth.py: GET /auth/{provider}/login (редирект на согласие, state в
  httponly-cookie от CSRF) и /callback (обмен code, find-or-create юзера по
  oauth_id → по email для привязки существующего аккаунта → новый без пароля,
  is_verified=email_verified от провайдера). Токен фронту — через URL-фрагмент
  #token=..., не query (не уходит в логи/Referer).
- Фронтенд: OAuthButtons (Login/Register), страница /oauth/callback (читает
  фрагмент → GET /auth/me → setAuth → редирект в кабинет).
- 6 юнит-тестов чистой логики сборки ссылок (app/core/oauth.py) — первый тест-
  контур для api/ в этой сессии (pytest.ini/conftest/requirements-test по
  образцу остальных сервисов), добавлен в общий run_tests.sh + mypy-гейт.

GOOGLE_CLIENT_ID/SECRET уже в .env (юзер создал OAuth-клиент), YANDEX_* пусты —
эндпоинты в этом случае отвечают 503, не падают. .env.example документирует обе
пары. Тестов всего: 118 (было 112).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 17:59:10 +05:00
jze9
b471ec767a feat: две доработки в духе коммерческих систем — цитаты и авто-корпус
Some checks failed
Deploy / test (push) Failing after 4m15s
Deploy / deploy (push) Has been skipped
Ответ на "неужели больше нет" / "мы ведь можем это исправить": закрывает
две дыры относительно Антиплагиат.ру/Turnitin, о которых договорились.

1. Различение цитаты и голого плагиата (app.scoring.is_cited, worker-gpu):
   эвристика — фрагмент считается процитированным, если обрамлён кавычками
   («…», "…") либо сразу за ним (в пределах ~150 симв.) идёт скобочная ссылка
   с годом: [Иванов, 2023], (Smith, 2020) — совпадает и с нашим же форматом
   ГОСТ 7.0.5. aggregate_results теперь принимает full_text, размечает
   match["cited"] и считает uncited_similarity (доля БЕЗ похожих на цитаты —
   ближе к тому, что коммерческие системы называют "% некорректных
   заимствований") отдельно от overall_similarity (как было, для совместимости).
   Фронтенд: бейдж "Цитата" на совпадении + строка с разбивкой в отчёте
   (аддитивные опциональные поля в типах — старые задачи не ломаются).

2. Автопополнение корпуса проверенными работами (как у коммерческих систем —
   так ловится списывание у предыдущих потоков). Раньше загруженная на проверку
   работа складывалась в StagedWork и ждала РУЧНОГО одобрения админом — де-факто
   не пополняла базу для сравнения. Теперь index.auto_approve_submission
   (диспатчится из gpu.check_plagiarism ПОСЛЕ сохранения результата — чтобы
   работа не сматчилась сама с собой) добавляет её в documents автоматически,
   под настройкой AUTO_APPROVE_SUBMISSIONS (default True). Ручное
   approve/reject в админке остаётся рабочим (идемпотентно — auto-approve
   пропускает уже не-pending записи), пригодится при AUTO_APPROVE=False.
   Конвертация StagedWork→doc_data вынесена в чистый app/staging.py (без
   Celery/SQLAlchemy/MinIO) — тестируется изолированно, идентична ручному
   пути в admin.py (POST /admin/staging/{id}/approve).

Тестов добавлено 16 (scoring 8→18, новый staging.py — 6). Оба mypy-гейта
расширены (scoring.py, staging.py). Тестов всего: 112 (было 82 в последнем
подсчёте README — таблица давно отставала, заодно поправил на актуальные цифры
по всем сервисам, включая забытый в прошлый раз Qdrant).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 15:09:43 +05:00
jze9
92a1ce0f57 feat(ingestion): скрипт бэкфилла full_text для CyberLeninka из OCR
Оставался незакоммиченным с предыдущего шага (докачка full-text). Переспрашивает
59 уже засеянных cyberleninka-тем из parse_sources, матчит raw-статьи с уже
залитыми documents по ext_id, и там где minio_key ещё не проставлен — пересчитывает
Winnowing-отпечатки из OCR full_text (не короткой annotation) и сохраняет текст в
MinIO. Идемпотентно (пропускает уже обработанные). Не трогает faiss_id/эмбеддинги.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 15:09:18 +05:00
jze9
2aac40ed3e feat(parsers): новый источник PubMed Central (PMC) — реальный полный текст
Отвечает на "неужели больше нет библиотек" — добавлен четвёртый источник.
PMC Open Access Subset (NCBI E-utilities) — крупнейший биомедицинский открытый
архив, бесплатный API без обязательного ключа (NCBI_API_KEY — опционально,
поднимает лимит 3→10 запросов/сек). В отличие от остальных источников отдаёт
РЕАЛЬНЫЙ полный текст статьи (bodyJATS XML) прямо в ответе efetch — 17-53К
символов на статью в проверке вживую, не аннотацию и не 700-символьный OCR-
фрагмент. Никаких новых зависимостей — xml.etree.ElementTree (stdlib) + httpx,
по образцу arxiv.py.

- scripts/parsers/pmc.py: esearch (open access[filter]) → efetch (батчи по 20,
  JATS XML) → плоские словари → unified schema. Год-фильтр, свой User-Agent.
- Зарегистрирован в index.run_parser (services/worker-indexer/app/tasks/index.py)
  и в CLI run_parser.py — доступен как source_type="pmc" наравне с остальными.
- 7 юнит-тестов на реальных JATS XML-фрагментах (без сети): извлечение полей,
  фильтр contrib-type=author (не editor), пустой/битый XML, батч из 2 статей.

Рассмотрены и НЕ добавлены: Semantic Scholar (общий rate-limit исчерпан без
API-ключа, ключ — самостоятельная регистрация юзера) и CORE.ac.uk (обязателен
ключ). Google Scholar/ResearchGate — намеренно не трогаем, скрейпинг нарушает ToS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 14:47:15 +05:00
jze9
cc87a11f07 feat(parsers): CyberLeninka full_text из OCR-фрагментов поиска (без доп. запросов)
Поисковый API КиберЛенинки отдаёт поле "ocr" — список OCR-фрагментов текста
статьи (начало + фрагмент с ключевыми словами), прямо в ответе search. Раньше
transform() игнорировал его (full_text всегда None), и Winnowing-отпечатки (L1)
считались только по короткой annotation (~150 симв.) либо не считались вовсе,
если аннотации не было.

Теперь ocr (список) склеивается, чистится (<b>/сущности) и идёт в full_text —
add_document уже умеет: `text = full_text or abstract`. Даёт заметно более точные
L1-отпечатки для ВСЕХ будущих CyberLeninka-документов бесплатно — 0 доп. HTTP-
запросов, работает и там, где annotation вообще пустая (проверено вживую).

2 новых теста (ocr→full_text, отсутствие ocr→None). Тестов в файле: 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 14:37:12 +05:00
jze9
92e23b5209 feat(ingestion): расширение охвата корпуса — 64 новые дисциплины
Продолжение seed_ru_sources.py: все 30 базовых RU-тем + 26 EN-тем (OpenAlex) +
4 категории arXiv уже отработаны (done). Этот скрипт добавляет НОВОЕ поверх —
углубление RU (специализированные подотрасли права/экономики/IT, 29 тем) и
расширение EN на области, которых не было (право, лингвистика, история,
искусство, науки о Земле и т.д., 20 тем) + 15 новых тем arXiv. Идемпотентно
(префиксы ru2:/oa3:/arxiv2: не пересекаются с уже существующими).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 13:53:01 +05:00
jze9
6099ef621f fix(parsers): ленивый импорт bs4 в CyberLeninka + добавить в зависимости
Some checks failed
Deploy / test (push) Failing after 2m35s
Deploy / deploy (push) Has been skipped
Прод-воркер падал на index.run_parser с 'No module named bs4': парсер импортил
beautifulsoup4 на верхнем уровне, а в образе worker-indexer его нет. Но bs4 нужен
только для fetch_article_details (детали статьи), а заливке (fetch+transform) — нет.

- импорт bs4 сделан ленивым (внутри fetch_article_details) → заливка работает даже
  без bs4 в образе;
- beautifulsoup4 добавлен в worker-indexer/requirements.txt (для деталей статьи).

Это была вторая причина, почему CyberLeninka никогда не наполняла базу (первая —
GET вместо POST, 405). Проверено вживую: fetch без bs4 в пути работает.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 21:10:55 +05:00
jze9
9d005486df fix(parsers): починить CyberLeninka + подготовить русскую заливку корпуса
Корпус на 99.4% английский, 0 русских источников — при том что сервис для русских
студентов. Корень: парсер CyberLeninka был сломан (слал GET на /api/search → HTTP
405) и ни разу не наполнял базу.

- cyberleninka.py: GET→POST с JSON-телом (mode=articles); authors теперь из списка
  (API отдаёт список, не строку); чистка <b>-подсветки и HTML-сущностей (&quot;).
  Проверено вживую: 5/5 студенческих тем возвращают реальные русские статьи.
- Юнит-тесты парсера (scripts/parsers/tests/, 5 шт.) + обвязка; run_tests.sh обобщён
  на пути → парсеры теперь в тест-гейте CI. Всего тестов: 87.
- scripts/seed_ru_sources.py: сидер parse_sources по 30 студенческим дисциплинам
  (dry-run по умолчанию, --apply для записи). НЕ запускает заливку — готовит задания.
- docs/INGESTION.md: runbook (текущее состояние, шаги запуска, проверка, масштаб).

Прод-путь index.run_parser уже поддерживает cyberleninka и openalex(lang=ru).
Заливку не запускал — это отдельный go (ресурсоёмко: GPU-эмбеддинги, рост БД).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 21:02:16 +05:00
jze9
d32a07d0a8 feat(ops): DR-тест восстановления бэкапа PG + runbook HA/DR
Закрыта слепая зона «бэкапы есть, но восстановление не проверялось».

- scripts/ops/pg_restore_verify.sh — берёт последний дамп из MinIO backups/pg/,
  restore в ЭФЕМЕРНЫЙ postgres:16, проверяет ключевые таблицы (users/documents/
  tasks). Прод не трогает, всё в одноразовом контейнере. Под cron раз в неделю.
- docs/DR-HA.md — runbook: бэкапы, проверка восстановления, потоковая репликация
  PG, Redis-реплика+Sentinel, таблица SPOF со статусом митигаций.

Провижн реплик PG/Redis — на Proxmox (нужен новый LXC), это работа на железе, не
в репозитории; runbook даёт конкретные шаги. Векторный SPOF уже снимается Qdrant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:40:03 +05:00
jze9
ccc3521e74 refactor(gost): вынести сборку списка литературы в app.bibliography + 7 тестов
Сортировка (кириллица→латиница), нумерация и выбор форматтера жили внутри
Celery-задачи с БД и не тестировались — хотя это порядок и вид готового списка
литературы, который видит студент. Вынес в чистый app.bibliography.build_bibliography:

- нумерация сквозная с 1; total = число записей;
- сортировка по фамилии первого автора, кириллица раньше латиницы;
- стиль 7.1 → полное описание (format_full), иначе 7.0.5 → краткая ссылка;
- doc_id сохраняется в каждой записи; пустой список → total 0.

ORM→dict конверсия осталась в задаче (она из БД), поведение сохранено 1:1.
Добавлен в mypy-гейт. Тестов всего: 73 (indexer 24, gost 24, gpu 25).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:20:01 +05:00
jze9
b2d2061a83 refactor(indexer): вынести разбиение на фрагменты в app.fragments + 5 тестов
_split_into_fragments жила в тяжёлой Celery-задаче (celery/sqlalchemy/minio) и
не тестировалась, хотя именно она определяет, ЧТО проверяется на плагиат.
Вынес в чистый app.fragments.split_into_fragments — тестируется изолированно:

- пустой/пробельный текст → [];
- текст короче 20 слов → отбрасывается порогом длины;
- короткий текст → один фрагмент на весь объём, start=0;
- скользящее окно: перекрытие ровно (window-step) слов, хвост==голова соседа;
- start/end — корректные символьные офсеты в исходный текст.

Поведение сохранено 1:1. Добавлен в mypy-гейт. Тестов всего: 66.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:17:33 +05:00
jze9
4c15f11efa refactor(gpu): вынести скоринг плагиата в чистый app.scoring + 8 тестов
Итоговый процент схожести (тот, что видит студент) и дедупликация совпадений
жили внутри Celery-задачи check_plagiarism и не тестировались. Вынес в чистую
функцию app.scoring.aggregate_results (без Celery/БД/сети) — правильная «высота»
(доменная логика отдельно от оркестрации) и теперь покрыто тестами:

- процент = доля УНИКАЛЬНЫХ помеченных позиций от всех фрагментов;
- одна позиция с несколькими источниками считается один раз (не раздувает %);
- точный дубль (source_title:pos) дедуплицируется;
- деление на ноль фрагментов; кап на 100%; округление до 2 знаков; by_method.

Поведение сохранено 1:1 (лог задачи переведён на result[...]). scoring.py добавлен
в mypy-гейт. Тестов всего: 61 (indexer 19, gost 17, gpu 25).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:13:27 +05:00
jze9
e79f254af6 chore(lint): добавить mypy в CI-гейт — старт градуальной типизации
Ruff уже стоял; теперь рядом mypy как проверка типов. Прагматичный конфиг
mypy.ini (ловит реальные несовпадения типов/обращения к None/неверные аргументы,
но не требует аннотаций везде и не шумит на сторонних либах) — чтобы гейт был
зелёным и расширяемым.

Область на старте — только чистая логика, которая уже типобезопасна:
worker-indexer/app/algorithms (L1 winnowing, L2 minhash) и
worker-gost/app/formatters (ГОСТ 7.1 / 7.0.5). Запуск per-service, чтобы
резолвился локальный пакет app. faiss_manager вне области пока — требует
Optional-рефактора _index; на следующий заход.

run_lint.sh теперь гоняет ruff + mypy в одном контейнере; шаг CI переименован.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 20:07:44 +05:00
jze9
2daaa8c8a4 chore(lint): ruff-гейт в CI + фиксы (0 находок) — блокирует кривой деплой
Второй CI-гейт после тестов: ruff как статический анализатор всего Python-кода
(services + scripts). Раньше ни линта, ни проверки типов в CI не было вовсе.

Конфиг ruff.toml: правила E/F/W/I/UP/B/SIM/C4, line-length 100. Осознанно
выключены E501 (длину держит форматтер; длинные RU-комментарии — норма),
B008 (Depends()/Query() в дефолтах — идиома FastAPI, не баг) и UP042
((str, Enum)→StrEnum меняет __str__/сериализацию — не трогаем).

Починено под ноль находок:
- B904 (11): raise ... from exc / from None — читаемые цепочки исключений в
  Celery-ретраях и HTTPException, ошибки обработки не маскируют исходные.
- SIM105 (5): try/except/pass → contextlib.suppress (faiss remove_ids, lsh.remove,
  сброс кэша, ws-disconnect, парс года).
- C416/SIM108/B905/F841/UP035/UP017/F401/I001: dict(rows), тернарник, zip strict,
  мёртвая переменная, устаревшие импорты, timezone.utc→UTC, чистка/сортировка.

Обвязка: scripts/run_lint.sh (ruff в изолированном python:3.11-slim), шаг «Линт»
в job test перед юнит-тестами (падаем раньше). make lint / make lint-fix.
Все 41 юнит-тест по-прежнему зелёные, изменённые файлы компилируются.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 17:20:24 +05:00
jze9
ecc10a4413 test: юнит-суит L1/L2/L3 + ГОСТ (41 тест) и гейт в CI перед деплоем
Первый настоящий автоматический тест-суит проекта — раньше регрессии ловились
руками. Покрыта чистая логика детекции и форматирования (без БД/Redis/GPU/Ollama):

- worker-indexer: L1 Winnowing (точные совпадения, идемпотентность отпечатка,
  диапазон signed int64) и L2 MinHash LSH (шинглы, Jaccard, near-duplicate +
  upsert через in-memory-фолбэк).
- worker-gpu: L3 FAISS — возврат doc_id из PostgreSQL (IndexIDMap2),
  идемпотентность add_vectors (remove-before-add, без дублей), self-match ≈ 1,
  ранжирование. Прямо стережёт баги, из-за которых индекс переписывался.
- worker-gost: ГОСТ 7.1-2003 и 7.0.5-2008 — авторы (≤3 / 4+ «и др.»/et al.),
  статья/книга/web, DOI, порядок сортировки кириллица→латиница, стр. в ссылке.

Обвязка: per-service pytest.ini/conftest/requirements-test. scripts/run_tests.sh
гоняет тесты в изолированных python:3.11-slim контейнерах (не засоряя хост),
через Tsinghua-зеркало. CI: job `test` теперь гейтит `deploy` (needs: test) —
падение тестов блокирует прод-деплой. make test / make test-one SVC=...

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 17:11:08 +05:00
jze9
9b7becf4d1 feat(ingest): is_oa-фильтр OpenAlex + PDF-ссылка arXiv → покрытие full-text ↑
Some checks failed
Deploy / deploy (push) Failing after 4m37s
Корпус набивался с ~8% полного текста — по широким темам мало доступных PDF.
- OpenAlex: параметр open_access_only (is_oa:true), включён по умолчанию для
  заливки (INGEST_OPEN_ACCESS_ONLY). A/B: покрытие 20% → 45%.
- arXiv: url теперь прямая ссылка на PDF (был на HTML-страницу аннотации),
  теперь enrich_full_text реально качает текст (у arXiv PDF почти у 100%).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 15:04:03 +05:00
jze9
8dc51a4f65 ci: пересобирать только изменённые сервисы + полный клон для diff
All checks were successful
Deploy / deploy (push) Successful in 3s
Первый прогон вслепую пересобирал все 5 бэкенд-образов, включая worker-gpu
(torch/faiss ~2 ГБ) — на медленном канале это ~1-2 часа впустую, хотя коммит
менял только CI-файлы. Теперь deploy.sh по git-diff с прошлого деплоя
(маркер .last_deploy_sha) собирает лишь сервисы с изменённым кодом; фронт
пересобирается только при правках services/frontend; правка compose = полная
пересборка. Workflow клонирует репозиторий полностью (нужен лог для diff).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 13:34:30 +05:00
jze9
34fa495fb1 ci: авто-деплой в прод через Gitea Actions (push в main)
Some checks failed
Deploy / deploy (push) Failing after 8m6s
Self-hosted act_runner в host-режиме на app-хосте 1.32 (label "deploy").
При пуше в main workflow клонирует коммит и запускает scripts/deploy.sh:
синхронизация кода → пересборка/перезапуск бэкенд-сервисов → миграции →
сборка фронтенда → выкладка статики на CT 102 (reverse-proxy) с бэкапом.

Доступ к Proxmox-хосту для CT 102 — через секрет PVE_PASSWORD.
Больше ручных rsync/rebuild — деплой по git push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 13:23:04 +05:00
jze9
ef2723e1d3 feat(indexer): скачивание полного текста статей + батчинг эмбеддингов
Раньше корпус состоял только из метаданных: парсеры отдают full_text=None,
fingerprints/эмбеддинги считались из аннотаций, MinIO статьями не наполнялся
вовсе. Проверка плагиата шла против абстрактов, а не тел статей.

Добавлено:
- app/fulltext.py: best-effort скачивание PDF по url источника (стрим с
  лимитом размера, детект PDF по content-type/magic), извлечение текста
  через PyMuPDF.
- index.enrich_full_text: новая задача — качает полный текст, кладёт в MinIO
  (documents/corpus/{id}.txt), пересчитывает fingerprints по полному тексту,
  обновляет MinHash. Диспатчится из add_document при FETCH_FULL_TEXT=true.
- openalex: предпочитаем прямую ссылку на PDF (best_oa_location.pdf_url)
  вместо лендинга — покрытие full-text выросло с 17% до 33% на выборке.
- run_parser батчит эмбеддинги (EMBED_BATCH_SIZE) вместо диспатча по одному
  документу: worker-gpu кодирует пачку разом и реже переписывает FAISS-индекс.

Покрытие ~33% (прямые OA-PDF: arxiv/usenix/springer/techscience и т.п.);
для остального остаётся фолбэк на аннотацию. Управляется FETCH_FULL_TEXT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:42:02 +05:00
jze9
9894fa9320 feat: админ-панель, лицензия, тестовый стек на распределённой инфраструктуре
Админ-панель (доступ: is_admin + секретный код сессии в /admin/<code>):
- разделы: дашборд (здоровье сервисов, статистика), клиенты, работы,
  база документов, хранилище MinIO, источники парсинга, отстойник работ
- backend: модели ParseSource/StagedWork/AdminSession, миграция 003,
  core/admin (авторизация), роутер api/admin
- frontend: AdminLayout + 7 вкладок, adminApi, вход из кабинета
- Celery index.run_parser (фоновый парсинг), автосбор проверенных работ
  в отстойник с ручным одобрением → индексация в базу

Исправления:
- openalex parser: тип article (не journal-article), убран is_oa-фильтр
- winnowing: приведение xxh64 к signed int64 (фикс bigint out of range)
- bcrypt пин 4.0.1 (совместимость с passlib 1.7.4)
- alembic: prepend_sys_path + asyncpg-драйвер
- frontend: контракт Task (public_id вместо id), react-dropzone

Инфраструктура:
- docker-compose.test.yml: api+frontend+воркеры локально, БД/кэш/хранилище/
  брокер/LLM — на удалённых серверах через .env
- .gitignore: исправлено ошибочное игнорирование кода services/*/app/models/
- LICENSE: проприетарная

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 20:45:53 +05:00
jze9
7758315632 feat: initial microservices project structure
Services:
- api: FastAPI gateway with JWT auth, async endpoints, WebSocket
- worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM
- worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction
- worker-notifier: SMTP email notifications
- worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting

Infrastructure:
- docker-compose.yml (production) + docker-compose.dev.yml (hot reload)
- Nginx reverse proxy + WebSocket support
- PostgreSQL 16 with Alembic migrations
- Elasticsearch 8 with Russian/English analyzers
- MinIO, RabbitMQ, Redis, Ollama

Frontend:
- React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5
- 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register

Scripts:
- Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:42:39 +05:00