jze9 780a0a1061
All checks were successful
Deploy / test (push) Successful in 3m49s
Deploy / deploy (push) Successful in 4s
fix(ops): читать дамп Википедии с диска — сетевой поток рвётся на 5.6 ГБ
Wikimedia закрывает долгие потоковые соединения: обрыв пришёлся на 32 МБ из
5.9 ГБ. Скачать файл с докачкой (curl -C -) и читать локально надёжнее, поэтому
у скрипта появился --dump-file; чтение из сети осталось запасным путём.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:54:59 +05:00

Академический помощник

Микросервисная система антиплагиата и поиска академических источников.

Студент вводит тему → система ищет источники → проверяет плагиат → форматирует ГОСТ-библиографию. Всё асинхронно: студент закрыл браузер, получил 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 уровня)

  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

Парсеры источников

# Запарсить 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 стартует, только если оба зелёные — кривой код в прод не уезжает:

  1. Линт — ruff (весь Python) + mypy (чистая доменная логика). Конфиги: ruff.toml, mypy.ini.
  2. Юнит-тесты — 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

Description
No description provided
Readme 382 KiB
Languages
Python 72.6%
TypeScript 23.7%
Shell 2.2%
Makefile 0.8%
Dockerfile 0.5%