Files
anti-plagiarism/docs/ARCHITECTURE.md
jze9 e4ca4a7150
All checks were successful
Deploy / test (push) Successful in 2m59s
Deploy / deploy (push) Successful in 3s
docs: сервер эмбеддингов переехал на .1.40 + честные итоги проверок
Прод переключён на новый сервер эмбеддингов (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

266 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура — Академический помощник
Каноничное описание системы. Обновляется вместе с кодом; при расхождении верить
коду, а не этому файлу. Визуальная схема — [DIAGRAM.md](DIAGRAM.md). Смежные
документы: [DR-HA.md](DR-HA.md) (отказоустойчивость), [INGESTION.md](INGESTION.md)
(наполнение корпуса), [../README.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 1.40 (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). Тайминги раздельные: `started_at` — постановка в очередь,
`run_started_at` — реальный старт работы воркером (при массовом запуске между
ними часы ожидания), `finished_at` — конец. Из них админка рисует шкалу
загрузки, см. §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`, `gpu.index_stats` | 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 уровня
1. **L1 Winnowing** (`worker-indexer/app/algorithms/winnowing.py`) — точные/частичные
совпадения: k-граммы → xxHash → минимум в скользящем окне → fingerprint; Jaccard.
2. **L2 MinHash LSH** (`.../minhash.py`) — нечёткие совпадения: шинглы → MinHash (128
перм.) → LSH-индекс в **общем Redis** (префикс `antiplag_lsh`, upsert, graceful-фолбэк
в память).
3. **L3 семантика** (`worker-gpu`) — эмбеддинги `bge-m3` (1024d, нормированы),
бэкенд переключается `EMBED_BACKEND` (`ollama` — дефолт, GGUF через Vulkan на
AMD GPU; `sentence_transformers` — локальная загрузка CUDA/CPU; `cloud` —
облачный инференс той же модели, без локального GPU) → cosine в FAISS
`IndexIDMap2(IndexFlatIP)` **или** Qdrant (`VECTOR_BACKEND`); порог 0.75.
4. **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.1.40 «embedding-cpu» (VM 210 на хосте pve2 .1.37) | Xeon 4314, AVX-512+VNNI, 8 vCPU; модель всегда в RAM (`OLLAMA_KEEP_ALIVE=-1`), сторожевой таймер раз в 2 мин перезапускает зависшую Ollama. Замер на данных корпуса: 3.6 док/с против 2.2 у прежнего сервера |
| ~~Ollama на GPU~~ (выведен 31.08.2026) | 192.168.20.109 «embedding-gpu» | RX580; отказал по amdgpu ring timeout (Vulkan-контекст умирал при живом systemd-юните). Оставлен как есть — откат сводится к возврату `OLLAMA_URL` в Infisical |
| 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](DR-HA.md).
- **Шкала загрузки источников** (админка → «Источники»): каждый запуск создаёт строку
`parse_runs`, воркер пишет туда прогресс раз в ~2с. Шкала считается по формуле
`api/app/core/progress.py`: 0→50% — выборка из источника, 50→100% — индексация в
базу. Раскрытая строка показывает журнал прогона (по шагам, с таймингами).
Кнопки «Запустить всё» / «Остановить всё» — массовый старт и кооперативная отмена
(флаг `cancel_requested`, воркер останавливается сам на ближайшем тике; уже
начатый прогон не рвём посреди записи в базу).
- **Панель отладки** (админка → «Отладка», `GET /api/admin/debug` + `/health`):
один срез — доступность инфраструктуры (PostgreSQL/Redis/MinIO/ES/Ollama/
брокер: этот блок показывается даже когда сам срез не собирается, потому что
при аварии первый вопрос — что именно отвалилось),
живые воркеры Celery и что именно они крутят, глубина очередей RabbitMQ
(в т.ч. `unacked` и число потребителей), покрытие корпуса эмбеддингами,
активные и проблемные прогоны, зависшие прогоны (нет heartbeat >10 мин),
упавшие проверки за сутки и текущие бэкенды (`EMBED_BACKEND`/`LLM_BACKEND`/
`VECTOR_BACKEND`).
- **Бюджет времени прогона** (`PARSER_TIME_BUDGET_S`, по умолчанию 1500с) —
защита от краш-лупа по `consumer_timeout` RabbitMQ, см. [DR-HA.md](DR-HA.md) §6.
- **Покрытие L3 меряется по индексу, а не по БД.** Колонка `documents.faiss_id`
для этого непригодна: отметка остаётся после пересоздания индекса (смена
модели/размерности) и после сбоев worker-gpu. Панель отладки спрашивает
реальное число векторов задачей `gpu.index_stats` и отдельно предупреждает о
ложных отметках; чинит их
[`scripts/ops/faiss_reconcile.py`](../scripts/ops/faiss_reconcile.py).
- **Добор эмбеддингов** — [`scripts/ops/reembed_missing.py`](../scripts/ops/reembed_missing.py):
документ попадает в корпус сразу, а вектор для L3 считает отдельная задача
`gpu.embed_documents`; если worker-gpu или Ollama были недоступны, эти задачи
теряются и документ остаётся невидимым для семантического поиска. Скрипт
находит `faiss_id IS NULL` и переотправляет задачи пачками (dry-run по
умолчанию). Покрытие видно в панели отладки.
## 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
```