# Академический помощник Микросервисная система антиплагиата и поиска академических источников. Студент вводит тему → система ищет источники → проверяет плагиат → форматирует ГОСТ-библиографию. Всё асинхронно: студент закрыл браузер, получил email когда готово. > 📐 Полное описание системы — [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). > Отказоустойчивость и восстановление — [docs/DR-HA.md](docs/DR-HA.md). ## Архитектура ``` ┌─────────────────┐ │ Frontend │ React + Vite + TypeScript │ (React SPA) │ └────────┬────────┘ │ HTTPS ┌────────▼────────┐ │ API Gateway │ FastAPI, порт 8000 │ (FastAPI) │ JWT, Rate Limit, WebSocket └──┬─────────┬───┘ │ RabbitMQ│ Celery задачи ┌────────────▼──┐ ┌──▼──────────────┐ │ worker-gpu │ │ worker-indexer │ │ (CUDA/FAISS) │ │ (PDF/DOCX parse) │ │ Sem. search │ │ Winnowing/MinHash │ │ LLM paraphrase│ └──────────────────┘ └────────────────┘ │ ┌─────────▼──────────┐ ┌────────────────────┐ │ worker-notifier │ │ worker-gost │ │ (SMTP email) │ │ (ГОСТ 7.1/7.0.5) │ └────────────────────┘ └────────────────────┘ Инфраструктура: PostgreSQL 16 · Redis 7 · RabbitMQ 3 · Elasticsearch 8 MinIO (S3) · Ollama (qwen2.5:7b) · отдельный GPU-сервер ``` ## Быстрый старт ```bash # 1. Клонировать репозиторий git clone https://github.com/jze9/anti-plagiarism.git cd anti-plagiarism # 2. Создать файл конфигурации cp .env.example .env # Отредактировать .env — сменить пароли и ключи! # 3. Запустить в режиме разработки make dev # 4. Применить миграции базы данных make migrate-dev # 5. Создать ES индекс make es-init # 6. Открыть браузер # Frontend: http://localhost:5173 # API docs: http://localhost:8000/api/docs # RabbitMQ: http://localhost:15672 (guest/guest) # Flower: http://localhost:5555 # MinIO: http://localhost:9001 ``` ## Команды ```bash make dev # Запуск в dev режиме (hot reload) make build # Сборка Docker образов make up # Запуск в продакшн режиме make down # Остановить все сервисы make migrate # Применить Alembic миграции make logs # Логи всех сервисов make shell-api # Shell в контейнере API make shell-gpu # Shell в контейнере GPU воркера make lint # ruff + mypy в контейнере (тот же гейт, что в CI) make test # юнит-тесты всех сервисов в контейнерах make clean # Удалить контейнеры и volumes ``` ## Стек технологий | Компонент | Технологии | |-----------|-----------| | API Gateway | FastAPI 0.111, Python 3.11, SQLAlchemy 2.0, Alembic | | GPU Worker | FAISS-CPU (IndexIDMap2 · IndexFlatIP), эмбеддинги bge-m3/1024d (Ollama/sentence-transformers/облако — `EMBED_BACKEND`), LLM-парафраз через Ollama или OpenRouter (`LLM_BACKEND`) | | Indexer | PyMuPDF, python-docx, Winnowing, MinHash LSH | | Очереди | RabbitMQ (брокер) + Celery 5 (воркеры) + Redis (результаты) | | База данных | PostgreSQL 16 | | Поиск | Elasticsearch 8 (BM25) + FAISS (cosine, IndexFlatIP) | | Хранилище | MinIO (S3-совместимый) | | Frontend | React 18, Vite, TypeScript, TailwindCSS, Zustand, React Query v5 | ## Проверка плагиата (4 уровня) 1. **Winnowing** — точные/частичные совпадения по fingerprint'ам (xxHash + скользящее окно) 2. **MinHash LSH** — нечёткие совпадения (шинглы + Jaccard, индекс в общем Redis) 3. **FAISS cosine** — семантическое сходство (порог 0.75; эмбеддинги bge-m3/1024d, IndexFlatIP на нормированных векторах) 4. **LLM-анализ парафраза** (порог confidence 0.7) — бэкенд переключается `LLM_BACKEND`: локальная Ollama или облачный OpenRouter/DeepSeek (геоблокировка РФ обходится через SOCKS5-прокси `singbox-proxy`, см. `infra/singbox/`) Проверенные работы сами пополняют корпус (`source=user_submission`) — это даёт эффект как у коммерческих систем (база растёт от каждой проверки), но такие документы **исключены** из сравнения на всех уровнях L1-L3 (`AUTO_APPROVE_SUBMISSIONS`, по умолчанию выключено), иначе работа матчилась бы сама с собой на 100%. ## Тарифные планы | Тариф | Цена | Поиск/день | Изложений/мес | Плагиат/мес | Одновременно | |-------|------|-----------|--------------|-------------|-------------| | Бесплатный | 0₽ | 10 | 3 | 1 | 1 | | Студенческий | 199₽ | безлимит | 30 | 10 | 2 | | Премиум | 499₽ | безлимит | безлимит | 50 | 5 | | Научный | 999₽ | безлимит | безлимит | безлимит | 10 | ## Парсеры источников ```bash # Запарсить OpenAlex python scripts/run_parser.py openalex \ --query "машинное обучение" \ --limit 10000 \ --output /data/processed # КиберЛенинка python scripts/run_parser.py cyberleninka \ --query "нейронные сети" \ --limit 1000 # arXiv python scripts/run_parser.py arxiv \ --query "deep learning" \ --categories cs.AI cs.LG \ --limit 5000 # PubMed Central (PMC) — англоязычные научные статьи открытого доступа python scripts/run_parser.py pmc \ --query "public health" \ --limit 1000 ``` Массовое расширение корпуса по дисциплинам (не единичный запрос) — `scripts/seed_broad_corpus.py` и `scripts/seed_ru_sources.py` заводят десятки `parse_sources` записей сразу, дальше их разбирает `index.run_parser` через очередь. Подробности и текущий охват — [docs/INGESTION.md](docs/INGESTION.md). ## Переменные окружения Смотри `.env.example` для полного списка переменных. Обязательно смените `SECRET_KEY`, `POSTGRES_PASSWORD`, `MINIO_SECRET_KEY`. ## Три docker-compose файла | Файл | Назначение | |------|-----------| | `docker-compose.prod.yml` | **Реальный прод.** app-сервисы + локальный Elasticsearch. Postgres/Redis/RabbitMQ/MinIO/Ollama — уже существующие общие серверы, адреса в `.env`. Файл описывает и сервис `nginx` (собранный фронтенд + TLS), но в текущей топологии он **не запускается** — фронтенд и TLS реально раздаёт хостовой nginx на отдельном CT 102 (см. «Продакшн деплой» ниже), а `api` публикует `8000:8000` наружу именно под него. | | `docker-compose.test.yml` | Повседневная разработка — hot reload против той же общей инфры, что и прод (`make test-up`). | | `docker-compose.selfhosted.yml.example` | Не используется. Полностью автономный вариант (свои Postgres/Redis/RabbitMQ/ES/MinIO/Ollama + GPU passthrough) — на случай отдельного выделенного сервера в будущем. | ## Продакшн деплой Автоматический: push в `main` → Gitea Actions (`.gitea/workflows/deploy.yml`) → гейт `test` (lint + юнит-тесты) → `deploy` → `scripts/deploy.sh` на app-хосте. Умная пересборка — образ пересобирается только у сервисов, чей код изменился с прошлого деплоя (маркер SHA в `.last_deploy_sha`); правка `docker-compose.prod.yml` триггерит полную пересборку всех пяти бэкенд-сервисов. **`.env` на проде НЕ редактируется руками.** Первым шагом `deploy.sh` логинится в self-hosted Infisical (Machine Identity, Universal Auth) и генерирует `.env` заново из окружения `prod` на каждом запуске — ручные правки файла на сервере переживут максимум до следующего деплоя. Менять секреты/конфиг — через Infisical (`https://infisical.jze9.ru`, проект `academ`), не через `.env` напрямую. Если Infisical недоступен или вернул подозрительно мало ключей, деплой падает раньше синка кода и не трогает рабочий `.env`. Фронтенд собирается отдельно (`docker run node:20-slim` → `npm run build`) и заливается по `scp`/`pct push` на CT 102, где раздаётся **хостовым** (не докеризованным) nginx с TLS через certbot — см. `/etc/nginx/sites-available/academic` на CT 102. Это отдельная машина от app-хоста, деплоится только когда меняется `services/frontend/`. Ручной запуск деплоя (например, после смены секрета в Infisical без изменений кода) — `workflow_dispatch` в Gitea Actions, или локально: `PVE_PASSWORD=... INFISICAL_CLIENT_ID=... INFISICAL_CLIENT_SECRET=... bash scripts/deploy.sh` из полного чекаута репозитория. ## Векторный бэкенд (FAISS / Qdrant) Семантический индекс (уровень 3) спрятан за `app.vector_store.get_backend()` и переключается настройкой `VECTOR_BACKEND` — без изменения кода: - **`faiss`** (по умолчанию) — файловый `IndexIDMap2(IndexFlatIP)` в RAM воркера. Просто, но это единая точка отказа и без конкурентной записи. - **`qdrant`** — сетевой сервис: снимает SPOF, допускает конкурентный upsert из нескольких воркеров, переживает рестарт, масштабируется горизонтально. Переключение на Qdrant (аддитивно, ничего не ломает до шага 2): ```bash # 1. Поднять Qdrant (профиль qdrant в docker-compose.prod.yml) docker compose -f docker-compose.prod.yml --profile qdrant up -d qdrant # 2. В .env выставить VECTOR_BACKEND=qdrant (QDRANT_URL по умолчанию http://qdrant:6333) # 3. Перелить существующие векторы FAISS → Qdrant (идемпотентно) docker compose -f docker-compose.prod.yml exec worker-gpu python -m app.migrate_faiss_to_qdrant # 4. Перезапустить GPU-воркер docker compose -f docker-compose.prod.yml up -d worker-gpu ``` ## Наблюдаемость (Prometheus + Grafana) Опционально (профиль `observability`, по умолчанию не поднимается): ```bash docker compose -f docker-compose.prod.yml --profile observability up -d prometheus grafana ``` - API отдаёт HTTP-метрики на `/metrics` (кол-во и латентность запросов по хендлерам). - Prometheus (`infra/prometheus/prometheus.yml`) скрейпит API и **Flower** — из Flower приходят метрики Celery (задачи, время выполнения, воркеры) без доп. кода. - Grafana с автоподключённым источником Prometheus (`infra/grafana/provisioning/`); пароль admin — `GRAFANA_ADMIN_PASSWORD` в Infisical (prod). Если не задан — падает на дефолт `admin`, поэтому перед включением профиля `observability` в проде убедиться, что значение в Infisical реально установлено (не пустое). ## Тестирование и качество кода Перед деплоем CI (`.gitea/workflows/deploy.yml`, job `test`) прогоняет два гейта, и `deploy` стартует, только если оба зелёные — кривой код в прод не уезжает: 1. **Линт** — `ruff` (весь Python) + `mypy` (чистая доменная логика). Конфиги: [`ruff.toml`](ruff.toml), [`mypy.ini`](mypy.ini). 2. **Юнит-тесты** — `pytest` по сервисам: 113 тестов на ядро детекции, скоринга, парсеров, форматирования и OAuth, без внешней инфры (БД/Redis/GPU/Ollama замоканы либо не нужны). ```bash make lint # ruff + mypy в изолированном контейнере make lint-fix # авто-исправления ruff make test # все юнит-тесты в контейнерах make test-one SVC=worker-gost # тесты одного сервиса ``` Всё гоняется в `python:3.11-slim` (не засоряя хост) через [`scripts/run_lint.sh`](scripts/run_lint.sh) и [`scripts/run_tests.sh`](scripts/run_tests.sh). **Что покрыто.** Чистая логика вынесена из Celery-задач в отдельные тестируемые модули (доменная логика отдельно от оркестрации): | Слой | Модуль | Тестов | |------|--------|:------:| | L1 — точные совпадения | `worker-indexer/app/algorithms/winnowing.py` | 13 | | L2 — нечёткие (MinHash LSH) | `worker-indexer/app/algorithms/minhash.py` | 6 | | Разбиение на фрагменты | `worker-indexer/app/fragments.py` | 5 | | Автопополнение корпуса | `worker-indexer/app/staging.py` | 6 | | Извлечение текста из PDF | `worker-indexer/app/extractors/pdf.py` | 4 | | L3 — семантический индекс (FAISS) | `worker-gpu/app/faiss_manager.py` | 5 | | L3 — векторный бэкенд (Qdrant + выбор) | `worker-gpu/app/qdrant_manager.py`, `vector_store.py` | 9 | | L4 — LLM-парафраз | `worker-gpu/app/ollama_client.py` | 12 | | Итоговый % плагиата + цитаты | `worker-gpu/app/scoring.py` | 18 | | ГОСТ 7.1 / 7.0.5 | `worker-gost/app/formatters/` | 17 | | Список литературы | `worker-gost/app/bibliography.py` | 7 | | Парсеры источников (CyberLeninka, PMC) | `scripts/parsers/` | 14 | | OAuth-ссылки (Google/Яндекс) | `api/app/core/oauth.py` | 6 | ## Лицензия MIT