Кандидаты уровня 3 (FAISS), которые прошли порог семантической схожести,
но LLM не подтвердила заимствование, раньше молча отбрасывались. Теперь
это отдельный блок "recommendations" в отчёте — не плагиат, но источники,
полезные для раскрытия темы.
Ответ на "неужели больше нет" / "мы ведь можем это исправить": закрывает
две дыры относительно Антиплагиат.ру/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>
FAISS-индекс — файловый синглтон в RAM одного воркера (save на каждую запись,
без блокировок, без HA, не горизонтален). Добавлен альтернативный бэкенд Qdrant
с тем же classmethod-интерфейсом, выбор через VECTOR_BACKEND — аддитивно и
безопасно: дефолт остаётся faiss, ничего не ломается, пока не переключат.
- app/qdrant_manager.py — search/add_vectors/save(no-op)/total_vectors поверх
qdrant-client (коллекция Cosine, id точки = doc_id, upsert идемпотентен);
- app/vector_store.py — get_backend() по настройке; задачи search/plagiarism
переведены на него (больше не импортируют FAISSManager напрямую);
- app/migrate_faiss_to_qdrant.py — перелив существующих векторов (reconstruct
из IndexIDMap2 → upsert), идемпотентно;
- docker-compose.prod.yml — сервис qdrant под профилем `qdrant` (по умолчанию не
поднимается, ресурсов не ест) + volume; README — раздел про переключение.
Тесты (9) гоняют QdrantManager против ВСТРОЕННОГО Qdrant (qdrant-client :memory:,
не моки) + диспетчеризацию бэкенда. Всего тестов: 82 (indexer 24, gost 24, gpu 34).
Плюсы Qdrant активируются только после явного переключения + миграции.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Итоговый процент схожести (тот, что видит студент) и дедупликация совпадений
жили внутри Celery-задачи check_plagiarism и не тестировались. Вынес в чистую
функцию app.scoring.aggregate_results (без Celery/БД/сети) — правильная «высота»
(доменная логика отдельно от оркестрации) и теперь покрыто тестами:
- процент = доля УНИКАЛЬНЫХ помеченных позиций от всех фрагментов;
- одна позиция с несколькими источниками считается один раз (не раздувает %);
- точный дубль (source_title:pos) дедуплицируется;
- деление на ноль фрагментов; кап на 100%; округление до 2 знаков; by_method.
Поведение сохранено 1:1 (лог задачи переведён на result[...]). scoring.py добавлен
в mypy-гейт. Тестов всего: 61 (indexer 19, gost 17, gpu 25).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Первый настоящий автоматический тест-суит проекта — раньше регрессии ловились
руками. Покрыта чистая логика детекции и форматирования (без БД/Redis/GPU/Ollama):
- worker-indexer: L1 Winnowing (точные совпадения, идемпотентность отпечатка,
диапазон signed int64) и L2 MinHash LSH (шинглы, Jaccard, near-duplicate +
upsert через in-memory-фолбэк).
- worker-gpu: L3 FAISS — возврат doc_id из PostgreSQL (IndexIDMap2),
идемпотентность add_vectors (remove-before-add, без дублей), self-match ≈ 1,
ранжирование. Прямо стережёт баги, из-за которых индекс переписывался.
- worker-gost: ГОСТ 7.1-2003 и 7.0.5-2008 — авторы (≤3 / 4+ «и др.»/et al.),
статья/книга/web, DOI, порядок сортировки кириллица→латиница, стр. в ссылке.
Обвязка: per-service pytest.ini/conftest/requirements-test. scripts/run_tests.sh
гоняет тесты в изолированных python:3.11-slim контейнерах (не засоряя хост),
через Tsinghua-зеркало. CI: job `test` теперь гейтит `deploy` (needs: test) —
падение тестов блокирует прод-деплой. make test / make test-one SVC=...
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>