Таймаут запроса к бакету был 60с при бюджете таска 1500с — один залипший GET отъедал почти весь прогон. Плюс map() отдаёт результаты строго по порядку отправки, поэтому один медленный запрос блокировал все 11 уже готовых потоков. Таймаут снижен до 12с (обычный GET укладывается в доли секунды, 12с — запас на джиттер), map() заменён на as_completed: готовые результаты отдаются сразу, не дожидаясь залипшего соседа. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Академический помощник
Микросервисная система антиплагиата и поиска академических источников.
Студент вводит тему → система ищет источники → проверяет плагиат → форматирует ГОСТ-библиографию. Всё асинхронно: студент закрыл браузер, получил 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 │
│ (FAISS, CPU) │ │ (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 с bge-m3 на отдельном сервере эмбеддингов (192.168.1.40)
LLM-парафраз (L4) — Ollama или OpenRouter, переключается LLM_BACKEND
Быстрый старт
# 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 | 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 уровня)
- Winnowing — точные/частичные совпадения по fingerprint'ам (xxHash + скользящее окно)
- MinHash LSH — нечёткие совпадения (шинглы + Jaccard, индекс в общем Redis)
- FAISS cosine — семантическое сходство (порог 0.75; эмбеддинги bge-m3/1024d, IndexFlatIP на нормированных векторах)
- 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 |
Парсеры источников
# Запарсить 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.
Переменные окружения
Смотри .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):
# 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в Infisical (prod). Если не задан — падает на дефолтadmin, поэтому перед включением профиляobservabilityв проде убедиться, что значение в Infisical реально установлено (не пустое).
Тестирование и качество кода
Перед деплоем CI (.gitea/workflows/deploy.yml, job test) прогоняет два гейта,
и deploy стартует, только если оба зелёные — кривой код в прод не уезжает:
- Линт —
ruff(весь Python) +mypy(чистая доменная логика). Конфиги:ruff.toml,mypy.ini. - Юнит-тесты —
pytestпо сервисам: 150 тестов на ядро детекции, скоринга, парсеров, форматирования, 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/ |
19 |
| OAuth-ссылки (Google/Яндекс) | api/app/core/oauth.py |
6 |
| Прогресс заливки (счётчики, бюджет) | worker-indexer/app/progress.py |
7 |
| Шкала загрузки и тайминги прогонов | api/app/core/progress.py, schemas/admin.py |
11 |
Лицензия
MIT