Заливка корпуса была чёрным ящиком: у источника только 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>
20 KiB
Архитектура — Академический помощник
Каноничное описание системы. Обновляется вместе с кодом; при расхождении верить коду, а не этому файлу. Визуальная схема — DIAGRAM.md. Смежные документы: DR-HA.md (отказоустойчивость), INGESTION.md (наполнение корпуса), ../README.md (быстрый старт и команды).
1. Назначение
Веб-сервис для студентов: ввёл тему → система находит открытые академические источники, делает изложения, форматирует список литературы по ГОСТ и проверяет текст на плагиат. Всё асинхронно: пользователь отправляет задачу, закрывает браузер и получает результат на email (или в реальном времени через WebSocket, если вкладка открыта).
2. Общая схема
┌────────────────────────────┐
│ Frontend (React SPA) │ CT 102 → nginx
│ Home/Search/Check/Cabinet/ │
│ Bibliography/Settings/Admin │
└──────────────┬──────────────┘
│ HTTPS (/api, /ws)
┌──────────────▼──────────────┐
│ API Gateway (FastAPI) │ 1.32:8000
│ JWT · rate-limit · WebSocket │
│ /metrics (Prometheus) │
└───┬───────────────────────┬──┘
│ publish (RabbitMQ) │ read/write
│ │
┌────────────────────┼───────────────────┐ │
▼ ▼ ▼ │
queue.index queue.gpu queue.gost
┌───────────┐ ┌────────────┐ ┌───────────┐
│ worker- │ │ worker-gpu │ │ worker- │
│ indexer │ │ FAISS/Qdr. │ │ gost │
│ L1 Winnow │ │ L3 семант. │ │ ГОСТ 7.1/ │
│ L2 MinHash│ │ L4 LLM │ │ 7.0.5 │
│ PDF/DOCX │ │ эмбеддинги │ │ │
└─────┬─────┘ └──────┬─────┘ └─────┬─────┘
│ │ │
└─────────┬─────────┴───────┬──────────┘
▼ ▼
queue.notify общие данные
┌────────────┐ ┌──────────────────────────────┐
│ worker- │ │ PostgreSQL 1.38 · Redis 1.35 │
│ notifier │ │ RabbitMQ .82 · MinIO 1.21 │
│ SMTP email │ │ Elasticsearch (local 1.32) │
└────────────┘ │ Ollama .109 (bge-m3, эмбед.) │
│ OpenRouter/DeepSeek — LLM (опц.)│
│ [opt] Qdrant · Prometheus/Graf.│
└──────────────────────────────┘
3. Сервисы
| Сервис | Технологии | Ответственность |
|---|---|---|
| api | FastAPI, SQLAlchemy async (asyncpg), Redis, Celery-producer | HTTP/WS API, auth (JWT), rate-limits, диспетч задач в очереди, админ-панель |
| worker-indexer | Celery, PyMuPDF, python-docx, xxhash, datasketch | Извлечение текста (PDF/DOCX/TXT), фрагментация, L1 Winnowing, L2 MinHash LSH, парсинг источников, обогащение full-text |
| worker-gpu | Celery, FAISS/Qdrant, httpx→Ollama/OpenRouter | Эмбеддинги (EMBED_BACKEND: ollama/sentence_transformers/cloud), L3 семантический поиск, L4 LLM-анализ парафраза (LLM_BACKEND: ollama/openrouter), семантический поиск источников |
| worker-gost | Celery | Список литературы по ГОСТ 7.1-2003 / Р 7.0.5-2008 |
| worker-notifier | Celery, smtplib | Email: письма-результаты и верификация (jze9mail.ru) |
| frontend | React 18, Vite, TS, Tailwind, Zustand, React Query | SPA: 11 публичных страниц + админ-панель. Собирается в статику, отдаётся nginx |
4. Модель данных (PostgreSQL)
- users —
email,hashed_password(bcrypt),name,is_verified,is_admin,plan(free/student/premium),verification_token. - tasks —
id(UUID),public_id(внешний),user_id,type(TaskType: search/plagiarism/summarize/gost),status(TaskStatus: queued→processing→done/failed),celery_task_id,input_data(JSON),result(JSON),error,queue_position. - documents — корпус источников:
source(openalex/arxiv/cyberleninka/ user_submission),ext_id,doi,title,authors(JSON),year,lang,journal/volume/issue/pages,abstract,url,minio_key(full-text в MinIO),faiss_id. - fingerprints —
doc_id,hash_value(BIGINT, Winnowing),position— для L1. - usage_logs —
user_id,action— учёт лимитов по тарифу. - parse_sources — задания парсеров (админка): тип, query, годы, лимит, статус,
last_run_id— ссылка на последний прогон. - parse_runs — прогоны заливки: стадия, счётчики (
target/fetched/processed/ added/duplicates/skipped/failed),cancel_requested,heartbeat_at, журнал событий (JSON). Из них админка рисует шкалу загрузки, см. §12. - staged_works — пользовательские загрузки на модерацию перед добавлением в корпус.
- admin_sessions — одноразовые коды входа в админку.
5. Асинхронный конвейер (Celery + RabbitMQ)
Брокер — RabbitMQ, backend результатов — Redis. Маршрутизация по префиксу задачи:
| Очередь | Задачи | Воркер |
|---|---|---|
queue.index |
index.extract_and_check, index.add_document, index.run_parser, index.enrich_full_text, index.ingest_upload |
worker-indexer |
queue.gpu |
gpu.check_plagiarism, gpu.embed_documents, gpu.search_semantic |
worker-gpu |
queue.gost |
gost.format_bibliography |
worker-gost |
queue.notify |
notify.send_task_done, notify.send_verification |
worker-notifier |
Важно: API коммитит задачу в БД до send_task (иначе гонка dispatch-before-commit).
6. Ключевые потоки
Проверка плагиата. upload (api, файл→MinIO, Task) → index.extract_and_check
(извлечь текст → фрагментация → L1 Winnowing по fingerprints → L2 MinHash LSH
в Redis) → передаёт частичные совпадения в gpu.check_plagiarism (L3 FAISS/Qdrant
семантика по фрагментам → для подозрительных L4 LLM-парафраз) →
app.scoring.aggregate_results (итоговый %) → результат в Task → notify.send_task_done.
Фронтенд (HighlightedDocument.tsx) показывает исходный текст работы с подсветкой
совпадающих/процитированных фрагментов поверх обычного списка нарушений
(PlagiarismReport.tsx).
Поиск источников. api → gpu.search_semantic: эмбеддинг запроса → векторный поиск
(FAISS/Qdrant) + Elasticsearch BM25 → объединение → результат.
Список литературы. api → gost.format_bibliography: документы из БД →
app.bibliography.build_bibliography (сортировка кириллица→латиница, нумерация,
формат 7.1/7.0.5) → результат.
Наполнение корпуса. админка/CLI → index.run_parser (OpenAlex/arXiv/PMC/
КиберЛенинка, фильтр is_oa) → index.add_document (дедуп по ext_id,
fingerprints, MinHash, эмбеддинг) → index.enrich_full_text (скачать OA-PDF →
MinIO → переиндексация). Ход заливки пишется в parse_runs (§12).
Второй путь наполнения — ручная загрузка файлов админом: api сохраняет их в
MinIO (corpus-upload/) → index.ingest_upload (извлечь текст → add_document,
source=manual_upload). Это не проверка на плагиат: файл сразу становится
источником для сравнения, минуя отстойник.
7. Детекция плагиата — 4 уровня
- L1 Winnowing (
worker-indexer/app/algorithms/winnowing.py) — точные/частичные совпадения: k-граммы → xxHash → минимум в скользящем окне → fingerprint; Jaccard. - L2 MinHash LSH (
.../minhash.py) — нечёткие совпадения: шинглы → MinHash (128 перм.) → LSH-индекс в общем Redis (префиксantiplag_lsh, upsert, graceful-фолбэк в память). - L3 семантика (
worker-gpu) — эмбеддингиbge-m3(1024d, нормированы), бэкенд переключаетсяEMBED_BACKEND(ollama— дефолт, GGUF через Vulkan на AMD GPU;sentence_transformers— локальная загрузка CUDA/CPU;cloud— облачный инференс той же модели, без локального GPU) → cosine в FAISSIndexIDMap2(IndexFlatIP)или Qdrant (VECTOR_BACKEND); порог 0.75. - L4 LLM-парафраз — оценивает пары «источник↔фрагмент» для подозрительных из
L3; порог confidence 0.7. Бэкенд переключается
LLM_BACKEND:ollama(локальная модель, нужен GPU-хост) илиopenrouter(облачный DeepSeek — геоблокировку РФ обходит SOCKS5-проксиsingbox-proxy, докер-сервис на базе sing-box/Trojan). Ни один документ сsource=user_submission(прошлые проверки пользователей) не участвует в сравнении ни на одном уровне — иначе работа матчилась бы сама с собой.
Итог: scoring.aggregate_results — доля уникальных помеченных позиций (не выше 100%).
Кандидаты, похожие семантически, но не подтверждённые LLM как парафраз, не теряются —
попадают в отдельный список «похожие по теме источники» (рекомендации, не нарушения).
8. Векторный бэкенд
Абстрагирован за worker-gpu/app/vector_store.get_backend(); VECTOR_BACKEND=faiss
(файловый синглтон, дефолт) или qdrant (сервис, снимает SPOF/конкурентную запись).
Переключение и миграция — см. README, раздел «Векторный бэкенд».
9. Инфраструктура и топология
| Компонент | Узел | Примечание |
|---|---|---|
| api + воркеры + Elasticsearch | 1.32 (app-хост) | docker-compose.prod.yml |
| Frontend (nginx, статика + TLS) | CT 102 | деплоится отдельно |
| PostgreSQL 16 | 1.38 (выделенный LXC) | UTF8; бэкап→MinIO |
| Redis 7 | 1.35 (выделенный LXC) | кэш, rate-limits, LSH |
| RabbitMQ | 192.168.20.82 | брокер Celery |
| MinIO (S3) | 1.21 | документы, full-text, бэкапы |
| Ollama (bge-m3, эмбеддинги) | 192.168.20.109 «embedding-gpu» | RX580; известный failure mode — amdgpu ring timeout вешает Vulkan-контекст, лечится systemctl restart ollama |
| OpenRouter (DeepSeek, L4 LLM) | облако | опционально вместо локальной Ollama (LLM_BACKEND=openrouter); из РФ доступен только через singbox-proxy |
| Infisical (секреты) | 192.168.20.111 «VM111» | self-hosted, infisical.jze9.ru; единственный источник правды для .env на проде |
| Qdrant / Prometheus / Grafana | 1.32 | опционально, под compose-профилями |
CT 108 (192.168.20.163, GTX1070, qwen2.5:7b) — старая LLM-нода, роль полностью заменена OpenRouter, контейнер остановлен, но не удалён.
Деплой — Gitea Actions по push в main: гейт test (ruff+mypy → 113 юнит-тестов),
затем deploy (needs: test) через scripts/deploy.sh (умная пересборка изменённых
сервисов). PG/Redis/RabbitMQ/MinIO не в compose — общая инфра берётся из .env,
который на каждом деплое генерируется заново из Infisical (см. §10).
10. Конфигурация и секреты
Приложение читает .env (пример структуры — .env.example), но на проде этот файл
не редактируется руками и не персистентен как источник правды — на каждом
деплое scripts/deploy.sh логинится в self-hosted Infisical
(https://infisical.jze9.ru, проект academ, окружение prod) через Machine
Identity (Universal Auth) и генерирует .env заново (infisical export) до синка
кода. Правки секретов/конфига — через Infisical, а не через .env на сервере
(следующий деплой затрёт локальные правки). Если Infisical недоступен или вернул
подозрительно мало ключей, деплой прерывается раньше, не трогая рабочий .env.
Обязательно менять (значения уже в Infisical, не дефолты из .env.example):
SECRET_KEY, POSTGRES_PASSWORD, MINIO_SECRET_KEY. Опции: VECTOR_BACKEND,
QDRANT_URL, EMBED_BACKEND, LLM_BACKEND, GRAFANA_ADMIN_PASSWORD.
Локальная разработка (docker-compose.test.yml) продолжает использовать обычный
.env, отдельный от прод-потока через Infisical.
11. Качество и тесты
- 113 юнит-тестов (pytest, per-service) на чистую логику L1-L4/скоринг/фрагменты/
ГОСТ/библиография; инфра замокана или не нужна. Запуск:
make test. - Гейты CI: ruff (весь Python) + mypy (доменная логика) + тесты — блокируют деплой.
make lint. Хермет-раннеры вpython:3.11-slim. - Чистая доменная логика вынесена из Celery-задач в тестируемые модули
(
scoring.py,fragments.py,bibliography.py). Подробнее — README «Тестирование».
12. Наблюдаемость и эксплуатация
- Мониторинг:
antiplag_monitor.py(cron 5 мин, 7 сервисов, email-алерт при смене статуса). Опционально — Prometheus+Grafana (профильobservability, метрики API + Flower). - Бэкапы:
pg_dump→gzip→MinIO, cron 03:00, ротация 14. Проверка восстановления —scripts/ops/pg_restore_verify.sh. HA/DR — DR-HA.md. - Шкала загрузки источников (админка → «Источники»): каждый запуск создаёт строку
parse_runs, воркер пишет туда прогресс раз в ~2с. Шкала считается по формулеapi/app/core/progress.py: 0→50% — выборка из источника, 50→100% — индексация в базу. Раскрытая строка показывает журнал прогона (по шагам, с таймингами). Кнопки «Запустить всё» / «Остановить всё» — массовый старт и кооперативная отмена (флагcancel_requested, воркер останавливается сам на ближайшем тике; уже начатый прогон не рвём посреди записи в базу). - Панель отладки (админка → «Отладка»,
GET /api/admin/debug): один срез — живые воркеры Celery и что именно они крутят, глубина очередей RabbitMQ (в т.ч.unackedи число потребителей), покрытие корпуса эмбеддингами, активные и проблемные прогоны, зависшие прогоны (нет heartbeat >10 мин), упавшие проверки за сутки и текущие бэкенды (EMBED_BACKEND/LLM_BACKEND/VECTOR_BACKEND). - Бюджет времени прогона (
PARSER_TIME_BUDGET_S, по умолчанию 1500с) — защита от краш-лупа поconsumer_timeoutRabbitMQ, см. DR-HA.md §6.
13. Безопасность
JWT-аутентификация, bcrypt-хэши паролей, email-верификация, отдельный вход в админку
(одноразовые коды, admin_sessions). Rate-limits по тарифу — в Redis. CORS — явные
origins. Пользователь видит только свои задачи (ownership проверяется, в т.ч. на WebSocket).
14. Раскладка репозитория
services/ api, worker-{gpu,indexer,notifier,gost}, frontend
scripts/ parsers/ (OpenAlex/arXiv/PMC/КиберЛенинка), seed_*.py, ops/,
deploy.sh, run_tests.sh, run_lint.sh
infra/ nginx/, prometheus/, grafana/, singbox/ (VPN-прокси для OpenRouter)
docs/ ARCHITECTURE.md (этот файл), DR-HA.md
.gitea/workflows/ deploy.yml (гейт test → deploy)
ruff.toml · mypy.ini · Makefile · docker-compose.prod.yml