Документация сильно разошлась с кодом за последние недели работы — привёл в
соответствие ARCHITECTURE.md, README, DIAGRAM.md, INGESTION.md:
- LLM (L4) и эмбеддинги (L3) теперь переключаемые бэкенды (LLM_BACKEND,
EMBED_BACKEND), а не жёстко Ollama/qwen2.5:7b — старая модель эмбеддингов
(768d mpnet) заменена на bge-m3 (1024d); L4 может идти через OpenRouter
(DeepSeek) с прокси singbox для обхода геоблокировки.
- .env на проде больше не редактируется руками — на каждом деплое
генерируется из Infisical; старая инструкция "cp .env.example .env; nano
.env" вводила в заблуждение (правки терялись бы на следующем деплое).
- README "Три docker-compose файла"/deploy разделы противоречили реальности:
фронтенд+TLS реально раздаёт хостовой nginx на CT 102, не докеризованный
nginx-сервис из compose (тот не запускается вообще).
добавлен PMC-парсер, self-match исключение объяснено, число тестов (113)
синхронизировано между файлами (было 82/122 в разных местах).
- INGESTION.md: снимок "42K доков, 0 русских" от 12.08 заменён актуальным
(165K доков, ru теперь большинство) — иначе документ откровенно врёт.
- Diagram: узлы под текущую топологию (embedding-gpu, OpenRouter, Infisical).
Заодно, раз перепроверял код на соответствие докам:
- Удалён мёртвый и битый эндпоинт GET /reports/{task_id} — фильтровал по
внутреннему Task.id вместо public_id (никогда не мог сработать через
обычный клиентский поток), фронтенд его всё равно не вызывал —
функциональность полностью дублирует рабочий GET /tasks/{public_id}.
- Убран мёртвый конфиг FAISS_NLIST/FAISS_NPROBE (worker-gpu/config.py) —
индекс всегда IndexFlatIP, IVF нигде не строится, эти поля ничего не делали.
224 lines
17 KiB
Markdown
224 lines
17 KiB
Markdown
# Архитектура — Академический помощник
|
||
|
||
Каноничное описание системы. Обновляется вместе с кодом; при расхождении верить
|
||
коду, а не этому файлу. Визуальная схема — [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 .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, годы, лимит, статус.
|
||
- **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` | 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 → переиндексация).
|
||
|
||
## 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.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](DR-HA.md).
|
||
|
||
## 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
|
||
```
|