docs: актуализировать документацию (Infisical, OpenRouter, bge-m3, PMC) + чистка
Документация сильно разошлась с кодом за последние недели работы — привёл в
соответствие 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:
58
README.md
58
README.md
@@ -87,7 +87,7 @@ make clean # Удалить контейнеры и volumes
|
|||||||
| Компонент | Технологии |
|
| Компонент | Технологии |
|
||||||
|-----------|-----------|
|
|-----------|-----------|
|
||||||
| API Gateway | FastAPI 0.111, Python 3.11, SQLAlchemy 2.0, Alembic |
|
| API Gateway | FastAPI 0.111, Python 3.11, SQLAlchemy 2.0, Alembic |
|
||||||
| GPU Worker | sentence-transformers, FAISS-CPU (IndexIDMap2 · IndexFlatIP), Ollama (qwen2.5:7b) |
|
| 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 |
|
| Indexer | PyMuPDF, python-docx, Winnowing, MinHash LSH |
|
||||||
| Очереди | RabbitMQ (брокер) + Celery 5 (воркеры) + Redis (результаты) |
|
| Очереди | RabbitMQ (брокер) + Celery 5 (воркеры) + Redis (результаты) |
|
||||||
| База данных | PostgreSQL 16 |
|
| База данных | PostgreSQL 16 |
|
||||||
@@ -99,8 +99,13 @@ make clean # Удалить контейнеры и volumes
|
|||||||
|
|
||||||
1. **Winnowing** — точные/частичные совпадения по fingerprint'ам (xxHash + скользящее окно)
|
1. **Winnowing** — точные/частичные совпадения по fingerprint'ам (xxHash + скользящее окно)
|
||||||
2. **MinHash LSH** — нечёткие совпадения (шинглы + Jaccard, индекс в общем Redis)
|
2. **MinHash LSH** — нечёткие совпадения (шинглы + Jaccard, индекс в общем Redis)
|
||||||
3. **FAISS cosine** — семантическое сходство (порог 0.75; IndexFlatIP на нормированных эмбеддингах)
|
3. **FAISS cosine** — семантическое сходство (порог 0.75; эмбеддинги bge-m3/1024d, IndexFlatIP на нормированных векторах)
|
||||||
4. **Ollama qwen2.5:7b** — LLM-анализ парафраза (порог confidence 0.7)
|
4. **LLM-анализ парафраза** (порог confidence 0.7) — бэкенд переключается `LLM_BACKEND`: локальная Ollama или облачный OpenRouter/DeepSeek (геоблокировка РФ обходится через SOCKS5-прокси `singbox-proxy`, см. `infra/singbox/`)
|
||||||
|
|
||||||
|
Проверенные работы сами пополняют корпус (`source=user_submission`) — это даёт эффект
|
||||||
|
как у коммерческих систем (база растёт от каждой проверки), но такие документы
|
||||||
|
**исключены** из сравнения на всех уровнях L1-L3 (`AUTO_APPROVE_SUBMISSIONS`,
|
||||||
|
по умолчанию выключено), иначе работа матчилась бы сама с собой на 100%.
|
||||||
|
|
||||||
## Тарифные планы
|
## Тарифные планы
|
||||||
|
|
||||||
@@ -130,8 +135,17 @@ python scripts/run_parser.py arxiv \
|
|||||||
--query "deep learning" \
|
--query "deep learning" \
|
||||||
--categories cs.AI cs.LG \
|
--categories cs.AI cs.LG \
|
||||||
--limit 5000
|
--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](docs/INGESTION.md).
|
||||||
|
|
||||||
## Переменные окружения
|
## Переменные окружения
|
||||||
|
|
||||||
Смотри `.env.example` для полного списка переменных.
|
Смотри `.env.example` для полного списка переменных.
|
||||||
@@ -141,26 +155,34 @@ python scripts/run_parser.py arxiv \
|
|||||||
|
|
||||||
| Файл | Назначение |
|
| Файл | Назначение |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| `docker-compose.prod.yml` | **Реальный прод.** app-сервисы + nginx (собранный фронтенд + TLS) + локальный Elasticsearch. Postgres/Redis/RabbitMQ/MinIO/Ollama — уже существующие общие серверы, адреса в `.env`. |
|
| `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.test.yml` | Повседневная разработка — hot reload против той же общей инфры, что и прод (`make test-up`). |
|
||||||
| `docker-compose.selfhosted.yml.example` | Не используется. Полностью автономный вариант (свои Postgres/Redis/RabbitMQ/ES/MinIO/Ollama + GPU passthrough) — на случай отдельного выделенного сервера в будущем. |
|
| `docker-compose.selfhosted.yml.example` | Не используется. Полностью автономный вариант (свои Postgres/Redis/RabbitMQ/ES/MinIO/Ollama + GPU passthrough) — на случай отдельного выделенного сервера в будущем. |
|
||||||
|
|
||||||
## Продакшн деплой
|
## Продакшн деплой
|
||||||
|
|
||||||
```bash
|
Автоматический: push в `main` → Gitea Actions (`.gitea/workflows/deploy.yml`) → гейт
|
||||||
# На сервере
|
`test` (lint + юнит-тесты) → `deploy` → `scripts/deploy.sh` на app-хосте. Умная
|
||||||
cp .env.example .env
|
пересборка — образ пересобирается только у сервисов, чей код изменился с прошлого
|
||||||
nano .env # Настроить все пароли и ключи, указать реальные адреса общих Postgres/Redis/RabbitMQ/MinIO/Ollama
|
деплоя (маркер SHA в `.last_deploy_sha`); правка `docker-compose.prod.yml` триггерит
|
||||||
|
полную пересборку всех пяти бэкенд-сервисов.
|
||||||
|
|
||||||
# Сертификат ДО первого запуска nginx-контейнера (standalone, порт 80 должен быть свободен)
|
**`.env` на проде НЕ редактируется руками.** Первым шагом `deploy.sh` логинится в
|
||||||
certbot certonly --standalone -d academic.jze9.ru
|
self-hosted Infisical (Machine Identity, Universal Auth) и генерирует `.env` заново
|
||||||
|
из окружения `prod` на каждом запуске — ручные правки файла на сервере переживут
|
||||||
|
максимум до следующего деплоя. Менять секреты/конфиг — через Infisical
|
||||||
|
(`https://infisical.jze9.ru`, проект `academ`), не через `.env` напрямую. Если
|
||||||
|
Infisical недоступен или вернул подозрительно мало ключей, деплой падает раньше
|
||||||
|
синка кода и не трогает рабочий `.env`.
|
||||||
|
|
||||||
make build
|
Фронтенд собирается отдельно (`docker run node:20-slim` → `npm run build`) и
|
||||||
make up
|
заливается по `scp`/`pct push` на CT 102, где раздаётся **хостовым** (не
|
||||||
make migrate
|
докеризованным) nginx с TLS через certbot — см. `/etc/nginx/sites-available/academic`
|
||||||
```
|
на CT 102. Это отдельная машина от app-хоста, деплоится только когда меняется
|
||||||
|
`services/frontend/`.
|
||||||
|
|
||||||
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`), контейнер просто читает его.
|
Ручной запуск деплоя (например, после смены секрета в Infisical без изменений кода) —
|
||||||
|
`workflow_dispatch` в Gitea Actions, или локально: `PVE_PASSWORD=... INFISICAL_CLIENT_ID=... INFISICAL_CLIENT_SECRET=... bash scripts/deploy.sh` из полного чекаута репозитория.
|
||||||
|
|
||||||
## Векторный бэкенд (FAISS / Qdrant)
|
## Векторный бэкенд (FAISS / Qdrant)
|
||||||
|
|
||||||
@@ -199,7 +221,9 @@ docker compose -f docker-compose.prod.yml --profile observability up -d promethe
|
|||||||
- Prometheus (`infra/prometheus/prometheus.yml`) скрейпит API и **Flower** — из
|
- Prometheus (`infra/prometheus/prometheus.yml`) скрейпит API и **Flower** — из
|
||||||
Flower приходят метрики Celery (задачи, время выполнения, воркеры) без доп. кода.
|
Flower приходят метрики Celery (задачи, время выполнения, воркеры) без доп. кода.
|
||||||
- Grafana с автоподключённым источником Prometheus (`infra/grafana/provisioning/`);
|
- Grafana с автоподключённым источником Prometheus (`infra/grafana/provisioning/`);
|
||||||
пароль admin — через `GRAFANA_ADMIN_PASSWORD` в `.env` (по умолчанию `admin`).
|
пароль admin — `GRAFANA_ADMIN_PASSWORD` в Infisical (prod). Если не задан — падает
|
||||||
|
на дефолт `admin`, поэтому перед включением профиля `observability` в проде
|
||||||
|
убедиться, что значение в Infisical реально установлено (не пустое).
|
||||||
|
|
||||||
## Тестирование и качество кода
|
## Тестирование и качество кода
|
||||||
|
|
||||||
@@ -208,7 +232,7 @@ docker compose -f docker-compose.prod.yml --profile observability up -d promethe
|
|||||||
|
|
||||||
1. **Линт** — `ruff` (весь Python) + `mypy` (чистая доменная логика).
|
1. **Линт** — `ruff` (весь Python) + `mypy` (чистая доменная логика).
|
||||||
Конфиги: [`ruff.toml`](ruff.toml), [`mypy.ini`](mypy.ini).
|
Конфиги: [`ruff.toml`](ruff.toml), [`mypy.ini`](mypy.ini).
|
||||||
2. **Юнит-тесты** — `pytest` по сервисам: 122 теста на ядро детекции, скоринга,
|
2. **Юнит-тесты** — `pytest` по сервисам: 113 тестов на ядро детекции, скоринга,
|
||||||
парсеров, форматирования и OAuth, без внешней инфры (БД/Redis/GPU/Ollama
|
парсеров, форматирования и OAuth, без внешней инфры (БД/Redis/GPU/Ollama
|
||||||
замоканы либо не нужны).
|
замоканы либо не нужны).
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@
|
|||||||
│ worker- │ │ PostgreSQL 1.38 · Redis 1.35 │
|
│ worker- │ │ PostgreSQL 1.38 · Redis 1.35 │
|
||||||
│ notifier │ │ RabbitMQ .82 · MinIO 1.21 │
|
│ notifier │ │ RabbitMQ .82 · MinIO 1.21 │
|
||||||
│ SMTP email │ │ Elasticsearch (local 1.32) │
|
│ SMTP email │ │ Elasticsearch (local 1.32) │
|
||||||
└────────────┘ │ Ollama .163 (qwen2.5:7b) │
|
└────────────┘ │ Ollama .109 (bge-m3, эмбед.) │
|
||||||
|
│ OpenRouter/DeepSeek — LLM (опц.)│
|
||||||
│ [opt] Qdrant · Prometheus/Graf.│
|
│ [opt] Qdrant · Prometheus/Graf.│
|
||||||
└──────────────────────────────┘
|
└──────────────────────────────┘
|
||||||
```
|
```
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
|--------|-----------|-----------------|
|
|--------|-----------|-----------------|
|
||||||
| **api** | FastAPI, SQLAlchemy async (asyncpg), Redis, Celery-producer | HTTP/WS API, auth (JWT), rate-limits, диспетч задач в очереди, админ-панель |
|
| **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-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-gost** | Celery | Список литературы по **ГОСТ 7.1-2003 / Р 7.0.5-2008** |
|
||||||
| **worker-notifier** | Celery, smtplib | Email: письма-результаты и верификация (jze9mail.ru) |
|
| **worker-notifier** | Celery, smtplib | Email: письма-результаты и верификация (jze9mail.ru) |
|
||||||
| **frontend** | React 18, Vite, TS, Tailwind, Zustand, React Query | SPA: 11 публичных страниц + админ-панель. Собирается в статику, отдаётся nginx |
|
| **frontend** | React 18, Vite, TS, Tailwind, Zustand, React Query | SPA: 11 публичных страниц + админ-панель. Собирается в статику, отдаётся nginx |
|
||||||
@@ -98,8 +99,11 @@
|
|||||||
**Проверка плагиата.** upload (api, файл→MinIO, Task) → `index.extract_and_check`
|
**Проверка плагиата.** upload (api, файл→MinIO, Task) → `index.extract_and_check`
|
||||||
(извлечь текст → фрагментация → **L1 Winnowing** по fingerprints → **L2 MinHash LSH**
|
(извлечь текст → фрагментация → **L1 Winnowing** по fingerprints → **L2 MinHash LSH**
|
||||||
в Redis) → передаёт частичные совпадения в `gpu.check_plagiarism` (**L3** FAISS/Qdrant
|
в Redis) → передаёт частичные совпадения в `gpu.check_plagiarism` (**L3** FAISS/Qdrant
|
||||||
семантика по фрагментам → для подозрительных **L4** Ollama-парафраз) →
|
семантика по фрагментам → для подозрительных **L4** LLM-парафраз) →
|
||||||
`app.scoring.aggregate_results` (итоговый %) → результат в Task → `notify.send_task_done`.
|
`app.scoring.aggregate_results` (итоговый %) → результат в Task → `notify.send_task_done`.
|
||||||
|
Фронтенд (`HighlightedDocument.tsx`) показывает исходный текст работы с подсветкой
|
||||||
|
совпадающих/процитированных фрагментов поверх обычного списка нарушений
|
||||||
|
(`PlagiarismReport.tsx`).
|
||||||
|
|
||||||
**Поиск источников.** api → `gpu.search_semantic`: эмбеддинг запроса → векторный поиск
|
**Поиск источников.** api → `gpu.search_semantic`: эмбеддинг запроса → векторный поиск
|
||||||
(FAISS/Qdrant) + Elasticsearch BM25 → объединение → результат.
|
(FAISS/Qdrant) + Elasticsearch BM25 → объединение → результат.
|
||||||
@@ -108,9 +112,10 @@
|
|||||||
`app.bibliography.build_bibliography` (сортировка кириллица→латиница, нумерация,
|
`app.bibliography.build_bibliography` (сортировка кириллица→латиница, нумерация,
|
||||||
формат 7.1/7.0.5) → результат.
|
формат 7.1/7.0.5) → результат.
|
||||||
|
|
||||||
**Наполнение корпуса.** админка/CLI → `index.run_parser` (OpenAlex/arXiv/КиберЛенинка,
|
**Наполнение корпуса.** админка/CLI → `index.run_parser` (OpenAlex/arXiv/PMC/
|
||||||
фильтр `is_oa`) → `index.add_document` (дедуп по `ext_id`, fingerprints, MinHash,
|
КиберЛенинка, фильтр `is_oa`) → `index.add_document` (дедуп по `ext_id`,
|
||||||
эмбеддинг) → `index.enrich_full_text` (скачать OA-PDF → MinIO → переиндексация).
|
fingerprints, MinHash, эмбеддинг) → `index.enrich_full_text` (скачать OA-PDF →
|
||||||
|
MinIO → переиндексация).
|
||||||
|
|
||||||
## 7. Детекция плагиата — 4 уровня
|
## 7. Детекция плагиата — 4 уровня
|
||||||
|
|
||||||
@@ -119,13 +124,22 @@
|
|||||||
2. **L2 MinHash LSH** (`.../minhash.py`) — нечёткие совпадения: шинглы → MinHash (128
|
2. **L2 MinHash LSH** (`.../minhash.py`) — нечёткие совпадения: шинглы → MinHash (128
|
||||||
перм.) → LSH-индекс в **общем Redis** (префикс `antiplag_lsh`, upsert, graceful-фолбэк
|
перм.) → LSH-индекс в **общем Redis** (префикс `antiplag_lsh`, upsert, graceful-фолбэк
|
||||||
в память).
|
в память).
|
||||||
3. **L3 семантика** (`worker-gpu`) — эмбеддинги `paraphrase-multilingual-mpnet-base-v2`
|
3. **L3 семантика** (`worker-gpu`) — эмбеддинги `bge-m3` (1024d, нормированы),
|
||||||
(768d, нормированы) → cosine в FAISS `IndexIDMap2(IndexFlatIP)` **или** Qdrant
|
бэкенд переключается `EMBED_BACKEND` (`ollama` — дефолт, GGUF через Vulkan на
|
||||||
(`VECTOR_BACKEND`); порог 0.75.
|
AMD GPU; `sentence_transformers` — локальная загрузка CUDA/CPU; `cloud` —
|
||||||
4. **L4 LLM-парафраз** — Ollama `qwen2.5:7b` оценивает пары «источник↔фрагмент» для
|
облачный инференс той же модели, без локального GPU) → cosine в FAISS
|
||||||
подозрительных из L3; порог confidence 0.7.
|
`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%).
|
Итог: `scoring.aggregate_results` — доля уникальных помеченных позиций (не выше 100%).
|
||||||
|
Кандидаты, похожие семантически, но не подтверждённые LLM как парафраз, не теряются —
|
||||||
|
попадают в отдельный список «похожие по теме источники» (рекомендации, не нарушения).
|
||||||
|
|
||||||
## 8. Векторный бэкенд
|
## 8. Векторный бэкенд
|
||||||
|
|
||||||
@@ -143,22 +157,40 @@
|
|||||||
| Redis 7 | 1.35 (выделенный LXC) | кэш, rate-limits, LSH |
|
| Redis 7 | 1.35 (выделенный LXC) | кэш, rate-limits, LSH |
|
||||||
| RabbitMQ | 192.168.20.82 | брокер Celery |
|
| RabbitMQ | 192.168.20.82 | брокер Celery |
|
||||||
| MinIO (S3) | 1.21 | документы, full-text, бэкапы |
|
| 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-профилями |
|
| 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` (умная пересборка изменённых
|
затем `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`,
|
Приложение читает `.env` (пример структуры — `.env.example`), но на проде этот файл
|
||||||
`POSTGRES_PASSWORD`, `MINIO_SECRET_KEY`. Опции: `VECTOR_BACKEND`, `QDRANT_URL`,
|
**не редактируется руками** и не персистентен как источник правды — на каждом
|
||||||
`GRAFANA_ADMIN_PASSWORD`. `.env` не в git и исключён из деплой-rsync (не откатывается).
|
деплое `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. Качество и тесты
|
## 11. Качество и тесты
|
||||||
|
|
||||||
- **82 юнит-теста** (pytest, per-service) на чистую логику L1-L4/скоринг/фрагменты/
|
- **113 юнит-тестов** (pytest, per-service) на чистую логику L1-L4/скоринг/фрагменты/
|
||||||
ГОСТ/библиография; инфра замокана или не нужна. Запуск: `make test`.
|
ГОСТ/библиография; инфра замокана или не нужна. Запуск: `make test`.
|
||||||
- **Гейты CI**: ruff (весь Python) + mypy (доменная логика) + тесты — блокируют деплой.
|
- **Гейты CI**: ruff (весь Python) + mypy (доменная логика) + тесты — блокируют деплой.
|
||||||
`make lint`. Хермет-раннеры в `python:3.11-slim`.
|
`make lint`. Хермет-раннеры в `python:3.11-slim`.
|
||||||
@@ -182,9 +214,9 @@ origins. Пользователь видит только свои задачи
|
|||||||
|
|
||||||
```
|
```
|
||||||
services/ api, worker-{gpu,indexer,notifier,gost}, frontend
|
services/ api, worker-{gpu,indexer,notifier,gost}, frontend
|
||||||
scripts/ parsers/ (OpenAlex/arXiv/КиберЛенинка), ops/, deploy.sh,
|
scripts/ parsers/ (OpenAlex/arXiv/PMC/КиберЛенинка), seed_*.py, ops/,
|
||||||
run_tests.sh, run_lint.sh
|
deploy.sh, run_tests.sh, run_lint.sh
|
||||||
infra/ nginx/, prometheus/, grafana/
|
infra/ nginx/, prometheus/, grafana/, singbox/ (VPN-прокси для OpenRouter)
|
||||||
docs/ ARCHITECTURE.md (этот файл), DR-HA.md
|
docs/ ARCHITECTURE.md (этот файл), DR-HA.md
|
||||||
.gitea/workflows/ deploy.yml (гейт test → deploy)
|
.gitea/workflows/ deploy.yml (гейт test → deploy)
|
||||||
ruff.toml · mypy.ini · Makefile · docker-compose.prod.yml
|
ruff.toml · mypy.ini · Makefile · docker-compose.prod.yml
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ flowchart TB
|
|||||||
PG[("PostgreSQL 1.38<br/>users/tasks/documents/<br/>fingerprints/...")]
|
PG[("PostgreSQL 1.38<br/>users/tasks/documents/<br/>fingerprints/...")]
|
||||||
REDIS[("Redis 1.35<br/>кэш · rate-limit · LSH-индекс")]
|
REDIS[("Redis 1.35<br/>кэш · rate-limit · LSH-индекс")]
|
||||||
MINIO[("MinIO 1.21<br/>документы · full-text · бэкапы")]
|
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
|
end
|
||||||
|
|
||||||
subgraph obs["Опционально (профили compose)"]
|
subgraph obs["Опционально (профили compose)"]
|
||||||
@@ -56,10 +58,12 @@ flowchart TB
|
|||||||
IDX -->|"gpu.embed_documents"| MQ
|
IDX -->|"gpu.embed_documents"| MQ
|
||||||
|
|
||||||
GPU <-->|"documents"| PG
|
GPU <-->|"documents"| PG
|
||||||
|
GPU -->|"L3 эмбеддинги"| OLLAMA
|
||||||
GPU --> VEC
|
GPU --> VEC
|
||||||
VEC --> FAISS
|
VEC --> FAISS
|
||||||
VEC -.->|"переключение флагом"| QDR
|
VEC -.->|"переключение флагом"| QDR
|
||||||
GPU -->|"L4 парафраз"| OLLAMA
|
GPU -.->|"L4 парафраз (LLM_BACKEND)"| OLLAMA
|
||||||
|
GPU -.->|"L4 парафраз, опц."| OPENROUTER
|
||||||
GPU --> ES
|
GPU --> ES
|
||||||
|
|
||||||
GOST <-->|"documents"| PG
|
GOST <-->|"documents"| PG
|
||||||
@@ -81,7 +85,7 @@ sequenceDiagram
|
|||||||
participant API as API
|
participant API as API
|
||||||
participant IDX as worker-indexer
|
participant IDX as worker-indexer
|
||||||
participant GPU as worker-gpu
|
participant GPU as worker-gpu
|
||||||
participant LLM as Ollama
|
participant LLM as Ollama/OpenRouter<br/>(LLM_BACKEND)
|
||||||
|
|
||||||
U->>API: upload файла
|
U->>API: upload файла
|
||||||
API->>API: сохранить в MinIO, создать Task, commit
|
API->>API: сохранить в MinIO, создать Task, commit
|
||||||
@@ -103,6 +107,7 @@ sequenceDiagram
|
|||||||
## Легенда
|
## Легенда
|
||||||
|
|
||||||
- Сплошные стрелки — прямые вызовы/запросы; пунктир у `VEC` — переключение бэкенда
|
- Сплошные стрелки — прямые вызовы/запросы; пунктир у `VEC` — переключение бэкенда
|
||||||
по настройке `VECTOR_BACKEND`, не одновременная работа обоих.
|
по настройке `VECTOR_BACKEND`, не одновременная работа обоих. Пунктир у L4
|
||||||
|
(`OLLAMA`/`OPENROUTER`) — то же самое для `LLM_BACKEND`.
|
||||||
- `obs` (Prometheus/Grafana) и `Qdrant` — опциональны, поднимаются под
|
- `obs` (Prometheus/Grafana) и `Qdrant` — опциональны, поднимаются под
|
||||||
`docker compose --profile qdrant|observability`, по умолчанию выключены.
|
`docker compose --profile qdrant|observability`, по умолчанию выключены.
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
# Наполнение корпуса — runbook
|
# Наполнение корпуса — runbook
|
||||||
|
|
||||||
## Текущее состояние (на 2026-08-12)
|
## Текущее состояние (на 2026-08-27)
|
||||||
|
|
||||||
- ~**42K документов**, из них **99.4% английские, 0 русских** (см. `documents`).
|
- **165 480 документов**, русский теперь большинство: `ru` 97 507, `en` 67 646,
|
||||||
- Заливка **встала 2026-08-07**. Корпус — seed из ~30 английских тем OpenAlex/arXiv.
|
остальные языки — единицы/десятки. Проблема «не с чем сравнивать русские
|
||||||
- Для сервиса под русских студентов это главный дефект: русские работы проверять
|
работы» из более ранней версии этого документа закрыта.
|
||||||
не с чем.
|
- По источникам: 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
|
```bash
|
||||||
# 1. (на app-хосте / в контейнере worker-indexer, где есть psycopg2 и прод-.env)
|
# 1. (на app-хосте / в контейнере worker-indexer, где есть psycopg2 и прод-.env
|
||||||
|
# — .env на проде теперь генерируется из Infisical на каждом деплое, см.
|
||||||
|
# ARCHITECTURE.md §10, руками его не редактировать)
|
||||||
# Посмотреть план:
|
# Посмотреть план:
|
||||||
python scripts/seed_ru_sources.py
|
python scripts/seed_ru_sources.py
|
||||||
# Создать источники в parse_sources (лимит на дисциплину):
|
# Создать источники в parse_sources (лимит на дисциплину):
|
||||||
@@ -48,5 +63,6 @@ SELECT source, count(*) FROM documents WHERE source='cyberleninka'; -- > 0
|
|||||||
заголовки с меткой ru).
|
заголовки с меткой ru).
|
||||||
- Для миллионов — **bulk** (снапшот OpenAlex на S3), а не постраничный API.
|
- Для миллионов — **bulk** (снапшот OpenAlex на S3), а не постраничный API.
|
||||||
- На масштабе обязателен `VECTOR_BACKEND=qdrant` (FAISS flat не тянет), а таблица
|
- На масштабе обязателен `VECTOR_BACKEND=qdrant` (FAISS flat не тянет), а таблица
|
||||||
`fingerprints` (уже ~29M строк на 42K доков) потребует партиционирования. См.
|
`fingerprints` (уже ~88M строк на 165K доков — партиционирование стоит планировать
|
||||||
|
заранее, не постфактум) потребует партиционирования. См.
|
||||||
[ARCHITECTURE.md](ARCHITECTURE.md) и [DR-HA.md](DR-HA.md).
|
[ARCHITECTURE.md](ARCHITECTURE.md) и [DR-HA.md](DR-HA.md).
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
"""Роутер для получения отчётов о выполненных задачах."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from app.core.security import get_current_user
|
|
||||||
from app.database import get_db
|
|
||||||
from app.models.task import Task
|
|
||||||
from app.models.user import User
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/reports", tags=["reports"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{task_id}")
|
|
||||||
async def get_report(
|
|
||||||
task_id: str,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
Получить готовый отчёт по задаче.
|
|
||||||
|
|
||||||
Возвращает task.result в зависимости от типа задачи:
|
|
||||||
- search: список источников с ГОСТ-цитатами
|
|
||||||
- plagiarism: детальный отчёт с совпадениями
|
|
||||||
- gost: отформатированная библиография
|
|
||||||
- summarize: краткое изложение
|
|
||||||
"""
|
|
||||||
result = await db.execute(
|
|
||||||
select(Task).where(Task.id == task_id, Task.user_id == current_user.id)
|
|
||||||
)
|
|
||||||
task = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if task is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Задача не найдена",
|
|
||||||
)
|
|
||||||
|
|
||||||
if task.status == "queued":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_202_ACCEPTED,
|
|
||||||
detail="Задача ещё в очереди",
|
|
||||||
)
|
|
||||||
|
|
||||||
if task.status == "processing":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_202_ACCEPTED,
|
|
||||||
detail="Задача ещё выполняется",
|
|
||||||
)
|
|
||||||
|
|
||||||
if task.status == "failed":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Задача завершилась с ошибкой: {task.error}",
|
|
||||||
)
|
|
||||||
|
|
||||||
if task.result is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Результат недоступен",
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"task_id": task.id,
|
|
||||||
"type": task.type,
|
|
||||||
"status": task.status,
|
|
||||||
"created_at": task.created_at.isoformat() if task.created_at else None,
|
|
||||||
"updated_at": task.updated_at.isoformat() if task.updated_at else None,
|
|
||||||
"result": task.result,
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from prometheus_fastapi_instrumentator import Instrumentator
|
from prometheus_fastapi_instrumentator import Instrumentator
|
||||||
|
|
||||||
from app.api import admin, auth, documents, reports, search, tasks
|
from app.api import admin, auth, documents, search, tasks
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.core.minio_client import ensure_bucket
|
from app.core.minio_client import ensure_bucket
|
||||||
from app.core.redis_client import close_pool
|
from app.core.redis_client import close_pool
|
||||||
@@ -78,7 +78,6 @@ app.include_router(auth.router, prefix="/api")
|
|||||||
app.include_router(tasks.router, prefix="/api")
|
app.include_router(tasks.router, prefix="/api")
|
||||||
app.include_router(search.router, prefix="/api")
|
app.include_router(search.router, prefix="/api")
|
||||||
app.include_router(documents.router, prefix="/api")
|
app.include_router(documents.router, prefix="/api")
|
||||||
app.include_router(reports.router, prefix="/api")
|
|
||||||
app.include_router(admin.router, prefix="/api")
|
app.include_router(admin.router, prefix="/api")
|
||||||
|
|
||||||
# ─── Метрики Prometheus ───────────────────────────────────────────────────────
|
# ─── Метрики Prometheus ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -86,11 +86,6 @@ export const documentsApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reportsApi = {
|
|
||||||
get: (taskId: string) =>
|
|
||||||
api.get(`/reports/${taskId}`),
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Админка ────────────────────────────────────────────────────────────────
|
// ─── Админка ────────────────────────────────────────────────────────────────
|
||||||
export const adminApi = {
|
export const adminApi = {
|
||||||
openSession: () => api.post('/admin/session'),
|
openSession: () => api.post('/admin/session'),
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ class Settings(BaseSettings):
|
|||||||
CLOUD_EMBED_URL: str = "https://api.deepinfra.com/v1/openai/embeddings"
|
CLOUD_EMBED_URL: str = "https://api.deepinfra.com/v1/openai/embeddings"
|
||||||
CLOUD_EMBED_MODEL: str = "BAAI/bge-m3"
|
CLOUD_EMBED_MODEL: str = "BAAI/bge-m3"
|
||||||
CLOUD_EMBED_API_KEY: str = ""
|
CLOUD_EMBED_API_KEY: str = ""
|
||||||
FAISS_NLIST: int = 1024 # Количество кластеров для IVFFlat
|
|
||||||
FAISS_NPROBE: int = 64 # Количество кластеров для поиска
|
|
||||||
|
|
||||||
# Векторный бэкенд: "faiss" (файловый синглтон, дефолт) или "qdrant" (сервис,
|
# Векторный бэкенд: "faiss" (файловый синглтон, дефолт) или "qdrant" (сервис,
|
||||||
# снимает SPOF и конкурентную запись). Переключается без изменения кода.
|
# снимает SPOF и конкурентную запись). Переключается без изменения кода.
|
||||||
|
|||||||
Reference in New Issue
Block a user