docs: актуализировать документацию (Infisical, OpenRouter, bge-m3, PMC) + чистка
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / test (push) Has been cancelled

Документация сильно разошлась с кодом за последние недели работы — привёл в
соответствие ARCHITECTURE.md, README, DIAGRAM.md, INGESTION.md:
- LLM (L4) и эмбеддинги (L3) теперь переключаемые бэкенды (LLM_BACKEND,
  EMBED_BACKEND), а не жёстко Ollama/qwen2.5:7b — старая модель эмбеддингов
  (768d mpnet) заменена на bge-m3 (1024d); L4 может идти через OpenRouter
  (DeepSeek) с прокси singbox для обхода геоблокировки.
- .env на проде больше не редактируется руками — на каждом деплое
  генерируется из Infisical; старая инструкция "cp .env.example .env; nano
  .env" вводила в заблуждение (правки терялись бы на следующем деплое).
- README "Три docker-compose файла"/deploy разделы противоречили реальности:
  фронтенд+TLS реально раздаёт хостовой nginx на CT 102, не докеризованный
  nginx-сервис из compose (тот не запускается вообще).
  добавлен PMC-парсер, self-match исключение объяснено, число тестов (113)
  синхронизировано между файлами (было 82/122 в разных местах).
- INGESTION.md: снимок "42K доков, 0 русских" от 12.08 заменён актуальным
  (165K доков, ru теперь большинство) — иначе документ откровенно врёт.
- Diagram: узлы под текущую топологию (embedding-gpu, OpenRouter, Infisical).

Заодно, раз перепроверял код на соответствие докам:
- Удалён мёртвый и битый эндпоинт GET /reports/{task_id} — фильтровал по
  внутреннему Task.id вместо public_id (никогда не мог сработать через
  обычный клиентский поток), фронтенд его всё равно не вызывал —
  функциональность полностью дублирует рабочий GET /tasks/{public_id}.
- Убран мёртвый конфиг FAISS_NLIST/FAISS_NPROBE (worker-gpu/config.py) —
  индекс всегда IndexFlatIP, IVF нигде не строится, эти поля ничего не делали.
This commit is contained in:
jze9
2026-08-27 16:51:31 +05:00
parent 6c53213103
commit fc40793f4d
8 changed files with 128 additions and 135 deletions

View File

@@ -46,7 +46,8 @@
│ worker- │ │ PostgreSQL 1.38 · Redis 1.35 │
│ notifier │ │ RabbitMQ .82 · MinIO 1.21 │
│ SMTP email │ │ Elasticsearch (local 1.32) │
└────────────┘ │ Ollama .163 (qwen2.5:7b) │
└────────────┘ │ Ollama .109 (bge-m3, эмбед.) │
│ OpenRouter/DeepSeek — LLM (опц.)│
│ [opt] Qdrant · Prometheus/Graf.│
└──────────────────────────────┘
```
@@ -57,7 +58,7 @@
|--------|-----------|-----------------|
| **api** | FastAPI, SQLAlchemy async (asyncpg), Redis, Celery-producer | HTTP/WS API, auth (JWT), rate-limits, диспетч задач в очереди, админ-панель |
| **worker-indexer** | Celery, PyMuPDF, python-docx, xxhash, datasketch | Извлечение текста (PDF/DOCX/TXT), фрагментация, **L1 Winnowing**, **L2 MinHash LSH**, парсинг источников, обогащение full-text |
| **worker-gpu** | Celery, sentence-transformers, FAISS/Qdrant, httpx→Ollama | Эмбеддинги, **L3** семантический поиск, **L4** LLM-анализ парафраза, семантический поиск источников |
| **worker-gpu** | Celery, FAISS/Qdrant, httpx→Ollama/OpenRouter | Эмбеддинги (`EMBED_BACKEND`: ollama/sentence_transformers/cloud), **L3** семантический поиск, **L4** LLM-анализ парафраза (`LLM_BACKEND`: ollama/openrouter), семантический поиск источников |
| **worker-gost** | Celery | Список литературы по **ГОСТ 7.1-2003 / Р 7.0.5-2008** |
| **worker-notifier** | Celery, smtplib | Email: письма-результаты и верификация (jze9mail.ru) |
| **frontend** | React 18, Vite, TS, Tailwind, Zustand, React Query | SPA: 11 публичных страниц + админ-панель. Собирается в статику, отдаётся nginx |
@@ -98,8 +99,11 @@
**Проверка плагиата.** upload (api, файл→MinIO, Task) → `index.extract_and_check`
(извлечь текст → фрагментация → **L1 Winnowing** по fingerprints → **L2 MinHash LSH**
в Redis) → передаёт частичные совпадения в `gpu.check_plagiarism` (**L3** FAISS/Qdrant
семантика по фрагментам → для подозрительных **L4** Ollama-парафраз) →
семантика по фрагментам → для подозрительных **L4** LLM-парафраз) →
`app.scoring.aggregate_results` (итоговый %) → результат в Task → `notify.send_task_done`.
Фронтенд (`HighlightedDocument.tsx`) показывает исходный текст работы с подсветкой
совпадающих/процитированных фрагментов поверх обычного списка нарушений
(`PlagiarismReport.tsx`).
**Поиск источников.** api → `gpu.search_semantic`: эмбеддинг запроса → векторный поиск
(FAISS/Qdrant) + Elasticsearch BM25 → объединение → результат.
@@ -108,9 +112,10 @@
`app.bibliography.build_bibliography` (сортировка кириллица→латиница, нумерация,
формат 7.1/7.0.5) → результат.
**Наполнение корпуса.** админка/CLI → `index.run_parser` (OpenAlex/arXiv/КиберЛенинка,
фильтр `is_oa`) → `index.add_document` (дедуп по `ext_id`, fingerprints, MinHash,
эмбеддинг) → `index.enrich_full_text` (скачать OA-PDF → MinIO → переиндексация).
**Наполнение корпуса.** админка/CLI → `index.run_parser` (OpenAlex/arXiv/PMC/
КиберЛенинка, фильтр `is_oa`) → `index.add_document` (дедуп по `ext_id`,
fingerprints, MinHash, эмбеддинг) → `index.enrich_full_text` (скачать OA-PDF →
MinIO → переиндексация).
## 7. Детекция плагиата — 4 уровня
@@ -119,13 +124,22 @@
2. **L2 MinHash LSH** (`.../minhash.py`) — нечёткие совпадения: шинглы → MinHash (128
перм.) → LSH-индекс в **общем Redis** (префикс `antiplag_lsh`, upsert, graceful-фолбэк
в память).
3. **L3 семантика** (`worker-gpu`) — эмбеддинги `paraphrase-multilingual-mpnet-base-v2`
(768d, нормированы) → cosine в FAISS `IndexIDMap2(IndexFlatIP)` **или** Qdrant
(`VECTOR_BACKEND`); порог 0.75.
4. **L4 LLM-парафраз** — Ollama `qwen2.5:7b` оценивает пары «источник↔фрагмент» для
подозрительных из L3; порог confidence 0.7.
3. **L3 семантика** (`worker-gpu`) — эмбеддинги `bge-m3` (1024d, нормированы),
бэкенд переключается `EMBED_BACKEND` (`ollama` — дефолт, GGUF через Vulkan на
AMD GPU; `sentence_transformers` — локальная загрузка CUDA/CPU; `cloud` —
облачный инференс той же модели, без локального GPU) → cosine в FAISS
`IndexIDMap2(IndexFlatIP)` **или** Qdrant (`VECTOR_BACKEND`); порог 0.75.
4. **L4 LLM-парафраз** — оценивает пары «источник↔фрагмент» для подозрительных из
L3; порог confidence 0.7. Бэкенд переключается `LLM_BACKEND`: `ollama`
(локальная модель, нужен GPU-хост) или `openrouter` (облачный DeepSeek —
геоблокировку РФ обходит SOCKS5-прокси `singbox-proxy`, докер-сервис на базе
sing-box/Trojan). Ни один документ с `source=user_submission` (прошлые
проверки пользователей) не участвует в сравнении ни на одном уровне —
иначе работа матчилась бы сама с собой.
Итог: `scoring.aggregate_results` — доля уникальных помеченных позиций (не выше 100%).
Кандидаты, похожие семантически, но не подтверждённые LLM как парафраз, не теряются —
попадают в отдельный список «похожие по теме источники» (рекомендации, не нарушения).
## 8. Векторный бэкенд
@@ -143,22 +157,40 @@
| Redis 7 | 1.35 (выделенный LXC) | кэш, rate-limits, LSH |
| RabbitMQ | 192.168.20.82 | брокер Celery |
| MinIO (S3) | 1.21 | документы, full-text, бэкапы |
| Ollama (qwen2.5:7b) | 192.168.20.163 | GPU-сервер |
| Ollama (bge-m3, эмбеддинги) | 192.168.20.109 «embedding-gpu» | RX580; известный failure mode — amdgpu ring timeout вешает Vulkan-контекст, лечится `systemctl restart ollama` |
| OpenRouter (DeepSeek, L4 LLM) | облако | опционально вместо локальной Ollama (`LLM_BACKEND=openrouter`); из РФ доступен только через `singbox-proxy` |
| Infisical (секреты) | 192.168.20.111 «VM111» | self-hosted, `infisical.jze9.ru`; единственный источник правды для `.env` на проде |
| Qdrant / Prometheus / Grafana | 1.32 | опционально, под compose-профилями |
**Деплой** — Gitea Actions по push в `main`: гейт `test` (ruff+mypy → 82 юнит-теста),
CT 108 (192.168.20.163, GTX1070, qwen2.5:7b) — старая LLM-нода, роль полностью
заменена OpenRouter, контейнер остановлен, но не удалён.
**Деплой** — Gitea Actions по push в `main`: гейт `test` (ruff+mypy → 113 юнит-тестов),
затем `deploy` (`needs: test`) через `scripts/deploy.sh` (умная пересборка изменённых
сервисов). PG/Redis не в compose — общая инфра берётся из `.env`.
сервисов). PG/Redis/RabbitMQ/MinIO не в compose — общая инфра берётся из `.env`,
который на каждом деплое генерируется заново из Infisical (см. §10).
## 10. Конфигурация
## 10. Конфигурация и секреты
Всё через `.env` (пример — `.env.example`). Обязательно менять: `SECRET_KEY`,
`POSTGRES_PASSWORD`, `MINIO_SECRET_KEY`. Опции: `VECTOR_BACKEND`, `QDRANT_URL`,
`GRAFANA_ADMIN_PASSWORD`. `.env` не в git и исключён из деплой-rsync (не откатывается).
Приложение читает `.env` (пример структуры — `.env.example`), но на проде этот файл
**не редактируется руками** и не персистентен как источник правды — на каждом
деплое `scripts/deploy.sh` логинится в self-hosted **Infisical**
(`https://infisical.jze9.ru`, проект `academ`, окружение `prod`) через Machine
Identity (Universal Auth) и генерирует `.env` заново (`infisical export`) до синка
кода. Правки секретов/конфига — через Infisical, а не через `.env` на сервере
(следующий деплой затрёт локальные правки). Если Infisical недоступен или вернул
подозрительно мало ключей, деплой прерывается раньше, не трогая рабочий `.env`.
Обязательно менять (значения уже в Infisical, не дефолты из `.env.example`):
`SECRET_KEY`, `POSTGRES_PASSWORD`, `MINIO_SECRET_KEY`. Опции: `VECTOR_BACKEND`,
`QDRANT_URL`, `EMBED_BACKEND`, `LLM_BACKEND`, `GRAFANA_ADMIN_PASSWORD`.
Локальная разработка (`docker-compose.test.yml`) продолжает использовать обычный
`.env`, отдельный от прод-потока через Infisical.
## 11. Качество и тесты
- **82 юнит-теста** (pytest, per-service) на чистую логику L1-L4/скоринг/фрагменты/
- **113 юнит-тестов** (pytest, per-service) на чистую логику L1-L4/скоринг/фрагменты/
ГОСТ/библиография; инфра замокана или не нужна. Запуск: `make test`.
- **Гейты CI**: ruff (весь Python) + mypy (доменная логика) + тесты — блокируют деплой.
`make lint`. Хермет-раннеры в `python:3.11-slim`.
@@ -182,9 +214,9 @@ origins. Пользователь видит только свои задачи
```
services/ api, worker-{gpu,indexer,notifier,gost}, frontend
scripts/ parsers/ (OpenAlex/arXiv/КиберЛенинка), ops/, deploy.sh,
run_tests.sh, run_lint.sh
infra/ nginx/, prometheus/, grafana/
scripts/ parsers/ (OpenAlex/arXiv/PMC/КиберЛенинка), seed_*.py, ops/,
deploy.sh, run_tests.sh, run_lint.sh
infra/ nginx/, prometheus/, grafana/, singbox/ (VPN-прокси для OpenRouter)
docs/ ARCHITECTURE.md (этот файл), DR-HA.md
.gitea/workflows/ deploy.yml (гейт test → deploy)
ruff.toml · mypy.ini · Makefile · docker-compose.prod.yml

View File

@@ -31,7 +31,9 @@ flowchart TB
PG[("PostgreSQL 1.38<br/>users/tasks/documents/<br/>fingerprints/...")]
REDIS[("Redis 1.35<br/>кэш · rate-limit · LSH-индекс")]
MINIO[("MinIO 1.21<br/>документы · full-text · бэкапы")]
OLLAMA["Ollama .163<br/>qwen2.5:7b"]
OLLAMA["Ollama .109 embedding-gpu<br/>bge-m3, эмбеддинги (L3)"]
OPENROUTER["OpenRouter/DeepSeek<br/>LLM L4 (опц., через singbox-proxy)"]
INFISICAL["Infisical .111<br/>секреты → .env на каждом деплое"]
end
subgraph obs["Опционально (профили compose)"]
@@ -56,10 +58,12 @@ flowchart TB
IDX -->|"gpu.embed_documents"| MQ
GPU <-->|"documents"| PG
GPU -->|"L3 эмбеддинги"| OLLAMA
GPU --> VEC
VEC --> FAISS
VEC -.->|"переключение флагом"| QDR
GPU -->|"L4 парафраз"| OLLAMA
GPU -.->|"L4 парафраз (LLM_BACKEND)"| OLLAMA
GPU -.->|"L4 парафраз, опц."| OPENROUTER
GPU --> ES
GOST <-->|"documents"| PG
@@ -81,7 +85,7 @@ sequenceDiagram
participant API as API
participant IDX as worker-indexer
participant GPU as worker-gpu
participant LLM as Ollama
participant LLM as Ollama/OpenRouter<br/>(LLM_BACKEND)
U->>API: upload файла
API->>API: сохранить в MinIO, создать Task, commit
@@ -103,6 +107,7 @@ sequenceDiagram
## Легенда
- Сплошные стрелки — прямые вызовы/запросы; пунктир у `VEC` — переключение бэкенда
по настройке `VECTOR_BACKEND`, не одновременная работа обоих.
по настройке `VECTOR_BACKEND`, не одновременная работа обоих. Пунктир у L4
(`OLLAMA`/`OPENROUTER`) — то же самое для `LLM_BACKEND`.
- `obs` (Prometheus/Grafana) и `Qdrant` — опциональны, поднимаются под
`docker compose --profile qdrant|observability`, по умолчанию выключены.

View File

@@ -1,11 +1,24 @@
# Наполнение корпуса — runbook
## Текущее состояние (на 2026-08-12)
## Текущее состояние (на 2026-08-27)
- ~**42K документов**, из них **99.4% английские, 0 русских** (см. `documents`).
- Заливка **встала 2026-08-07**. Корпус — seed из ~30 английских тем OpenAlex/arXiv.
- Для сервиса под русских студентов это главный дефект: русские работы проверять
не с чем.
- **165 480 документов**, русский теперь большинство: `ru` 97 507, `en` 67 646,
остальные языки — единицы/десятки. Проблема «не с чем сравнивать русские
работы» из более ранней версии этого документа закрыта.
- По источникам: CyberLeninka 97 506, OpenAlex 49 218, PMC 10 451, arXiv 8 304,
`user_submission` (проверенные пользователями работы, не источник для сравнения
сами с собой — см. ARCHITECTURE.md §7) 1.
- Добавлен 4-й парсер — **PMC** (PubMed Central, `scripts/parsers/pmc.py`),
англоязычные научные статьи открытого доступа.
- Массовое расширение по дисциплинам теперь двумя сидерами: `seed_ru_sources.py`
(русскоязычные, CyberLeninka/OpenAlex `lang=ru`) и **`scripts/seed_broad_corpus.py`**
(англоязычные — OpenAlex/arXiv/PMC по широкому списку дисциплин, добавлен позже
первой волны).
- Известный операционный риск при массовой докачке: retry-бэкофф на 429 от OpenAlex
не должен по сумме ожиданий превышать `consumer_timeout` RabbitMQ (по умолчанию
1800с) — иначе брокер рвёт канал до того, как таск успевает сдаться, и
`worker-indexer` уходит в бесконечный краш-луп на редоставленном сообщении
(см. `scripts/parsers/openalex.py`, `MAX_RATE_LIMIT_RETRIES`).
## Что подготовлено
@@ -20,7 +33,9 @@
## Запуск русской заливки
```bash
# 1. (на app-хосте / в контейнере worker-indexer, где есть psycopg2 и прод-.env)
# 1. (на app-хосте / в контейнере worker-indexer, где есть psycopg2 и прод-.env
# — .env на проде теперь генерируется из Infisical на каждом деплое, см.
# ARCHITECTURE.md §10, руками его не редактировать)
# Посмотреть план:
python scripts/seed_ru_sources.py
# Создать источники в parse_sources (лимит на дисциплину):
@@ -48,5 +63,6 @@ SELECT source, count(*) FROM documents WHERE source='cyberleninka'; -- > 0
заголовки с меткой ru).
- Для миллионов — **bulk** (снапшот OpenAlex на S3), а не постраничный API.
- На масштабе обязателен `VECTOR_BACKEND=qdrant` (FAISS flat не тянет), а таблица
`fingerprints` (уже ~29M строк на 42K доков) потребует партиционирования. См.
`fingerprints` (уже ~88M строк на 165K доков — партиционирование стоит планировать
заранее, не постфактум) потребует партиционирования. См.
[ARCHITECTURE.md](ARCHITECTURE.md) и [DR-HA.md](DR-HA.md).