Раскрыт по умолчанию — свёрнутое состояние читалось как «ничего нет». Плюс явное сообщение, когда подсвечивать действительно нечего (у задачи нет ни совпадений, ни похожих по теме источников), вместо молчаливой голой стены текста.
Академический помощник
Микросервисная система антиплагиата и поиска академических источников.
Студент вводит тему → система ищет источники → проверяет плагиат → форматирует ГОСТ-библиографию. Всё асинхронно: студент закрыл браузер, получил email когда готово.
📐 Полное описание системы — docs/ARCHITECTURE.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-сервер
Быстрый старт
# 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
Команды
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 | sentence-transformers, FAISS-CPU (IndexIDMap2 · IndexFlatIP), Ollama (qwen2.5:7b) |
| 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 уровня)
- Winnowing — точные/частичные совпадения по fingerprint'ам (xxHash + скользящее окно)
- MinHash LSH — нечёткие совпадения (шинглы + Jaccard, индекс в общем Redis)
- FAISS cosine — семантическое сходство (порог 0.75; IndexFlatIP на нормированных эмбеддингах)
- Ollama qwen2.5:7b — LLM-анализ парафраза (порог confidence 0.7)
Тарифные планы
| Тариф | Цена | Поиск/день | Изложений/мес | Плагиат/мес | Одновременно |
|---|---|---|---|---|---|
| Бесплатный | 0₽ | 10 | 3 | 1 | 1 |
| Студенческий | 199₽ | безлимит | 30 | 10 | 2 |
| Премиум | 499₽ | безлимит | безлимит | 50 | 5 |
| Научный | 999₽ | безлимит | безлимит | безлимит | 10 |
Парсеры источников
# Запарсить 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
Переменные окружения
Смотри .env.example для полного списка переменных.
Обязательно смените SECRET_KEY, POSTGRES_PASSWORD, MINIO_SECRET_KEY.
Три docker-compose файла
| Файл | Назначение |
|---|---|
docker-compose.prod.yml |
Реальный прод. app-сервисы + nginx (собранный фронтенд + TLS) + локальный Elasticsearch. Postgres/Redis/RabbitMQ/MinIO/Ollama — уже существующие общие серверы, адреса в .env. |
docker-compose.test.yml |
Повседневная разработка — hot reload против той же общей инфры, что и прод (make test-up). |
docker-compose.selfhosted.yml.example |
Не используется. Полностью автономный вариант (свои Postgres/Redis/RabbitMQ/ES/MinIO/Ollama + GPU passthrough) — на случай отдельного выделенного сервера в будущем. |
Продакшн деплой
# На сервере
cp .env.example .env
nano .env # Настроить все пароли и ключи, указать реальные адреса общих Postgres/Redis/RabbitMQ/MinIO/Ollama
# Сертификат ДО первого запуска nginx-контейнера (standalone, порт 80 должен быть свободен)
certbot certonly --standalone -d academic.jze9.ru
make build
make up
make migrate
Nginx работает внутри контейнера (docker-compose.prod.yml, сервис nginx, Dockerfile в infra/nginx/Dockerfile) — собирает services/frontend в статику и отдаёт её вместе с проксированием /api/, /ws/ на api:8000 по конфигу infra/nginx/nginx.conf. Контейнер монтирует /etc/letsencrypt с хоста как read-only — сертификат обновляется на хосте (certbot renew), контейнер просто читает его.
Векторный бэкенд (FAISS / Qdrant)
Семантический индекс (уровень 3) спрятан за app.vector_store.get_backend() и
переключается настройкой VECTOR_BACKEND — без изменения кода:
faiss(по умолчанию) — файловыйIndexIDMap2(IndexFlatIP)в RAM воркера. Просто, но это единая точка отказа и без конкурентной записи.qdrant— сетевой сервис: снимает SPOF, допускает конкурентный upsert из нескольких воркеров, переживает рестарт, масштабируется горизонтально.
Переключение на Qdrant (аддитивно, ничего не ломает до шага 2):
# 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, по умолчанию не поднимается):
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в.env(по умолчаниюadmin).
Тестирование и качество кода
Перед деплоем CI (.gitea/workflows/deploy.yml, job test) прогоняет два гейта,
и deploy стартует, только если оба зелёные — кривой код в прод не уезжает:
- Линт —
ruff(весь Python) +mypy(чистая доменная логика). Конфиги:ruff.toml,mypy.ini. - Юнит-тесты —
pytestпо сервисам: 122 теста на ядро детекции, скоринга, парсеров, форматирования и OAuth, без внешней инфры (БД/Redis/GPU/Ollama замоканы либо не нужны).
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_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