Files
anti-plagiarism/README.md
jze9 b471ec767a
Some checks failed
Deploy / test (push) Failing after 4m15s
Deploy / deploy (push) Has been skipped
feat: две доработки в духе коммерческих систем — цитаты и авто-корпус
Ответ на "неужели больше нет" / "мы ведь можем это исправить": закрывает
две дыры относительно Антиплагиат.ру/Turnitin, о которых договорились.

1. Различение цитаты и голого плагиата (app.scoring.is_cited, worker-gpu):
   эвристика — фрагмент считается процитированным, если обрамлён кавычками
   («…», "…") либо сразу за ним (в пределах ~150 симв.) идёт скобочная ссылка
   с годом: [Иванов, 2023], (Smith, 2020) — совпадает и с нашим же форматом
   ГОСТ 7.0.5. aggregate_results теперь принимает full_text, размечает
   match["cited"] и считает uncited_similarity (доля БЕЗ похожих на цитаты —
   ближе к тому, что коммерческие системы называют "% некорректных
   заимствований") отдельно от overall_similarity (как было, для совместимости).
   Фронтенд: бейдж "Цитата" на совпадении + строка с разбивкой в отчёте
   (аддитивные опциональные поля в типах — старые задачи не ломаются).

2. Автопополнение корпуса проверенными работами (как у коммерческих систем —
   так ловится списывание у предыдущих потоков). Раньше загруженная на проверку
   работа складывалась в StagedWork и ждала РУЧНОГО одобрения админом — де-факто
   не пополняла базу для сравнения. Теперь index.auto_approve_submission
   (диспатчится из gpu.check_plagiarism ПОСЛЕ сохранения результата — чтобы
   работа не сматчилась сама с собой) добавляет её в documents автоматически,
   под настройкой AUTO_APPROVE_SUBMISSIONS (default True). Ручное
   approve/reject в админке остаётся рабочим (идемпотентно — auto-approve
   пропускает уже не-pending записи), пригодится при AUTO_APPROVE=False.
   Конвертация StagedWork→doc_data вынесена в чистый app/staging.py (без
   Celery/SQLAlchemy/MinIO) — тестируется изолированно, идентична ручному
   пути в admin.py (POST /admin/staging/{id}/approve).

Тестов добавлено 16 (scoring 8→18, новый staging.py — 6). Оба mypy-гейта
расширены (scoring.py, staging.py). Тестов всего: 112 (было 82 в последнем
подсчёте README — таблица давно отставала, заодно поправил на актуальные цифры
по всем сервисам, включая забытый в прошлый раз Qdrant).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 15:09:43 +05:00

245 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Академический помощник
Микросервисная система антиплагиата и поиска академических источников.
Студент вводит тему → система ищет источники → проверяет плагиат → форматирует ГОСТ-библиографию.
Всё асинхронно: студент закрыл браузер, получил 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 | 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 уровня)
1. **Winnowing** — точные/частичные совпадения по fingerprint'ам (xxHash + скользящее окно)
2. **MinHash LSH** — нечёткие совпадения (шинглы + Jaccard, индекс в общем Redis)
3. **FAISS cosine** — семантическое сходство (порог 0.75; IndexFlatIP на нормированных эмбеддингах)
4. **Ollama qwen2.5:7b** — LLM-анализ парафраза (порог confidence 0.7)
## Тарифные планы
| Тариф | Цена | Поиск/день | Изложений/мес | Плагиат/мес | Одновременно |
|-------|------|-----------|--------------|-------------|-------------|
| Бесплатный | 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
```
## Переменные окружения
Смотри `.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) — на случай отдельного выделенного сервера в будущем. |
## Продакшн деплой
```bash
# На сервере
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):
```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` в `.env` (по умолчанию `admin`).
## Тестирование и качество кода
Перед деплоем CI (`.gitea/workflows/deploy.yml`, job `test`) прогоняет два гейта,
и `deploy` стартует, только если оба зелёные — кривой код в прод не уезжает:
1. **Линт** — `ruff` (весь Python) + `mypy` (чистая доменная логика).
Конфиги: [`ruff.toml`](ruff.toml), [`mypy.ini`](mypy.ini).
2. **Юнит-тесты** — `pytest` по сервисам: 112 тестов на ядро детекции, скоринга,
парсеров и форматирования, без внешней инфры (БД/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 |
| 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 |
## Лицензия
MIT