From fc40793f4d43d592d659ac5182132942fdeed4f8 Mon Sep 17 00:00:00 2001 From: jze9 Date: Thu, 27 Aug 2026 16:51:31 +0500 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20(Infisical,=20OpenRouter,=20bge-m3,=20PMC)=20+=20?= =?UTF-8?q?=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Документация сильно разошлась с кодом за последние недели работы — привёл в соответствие 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 нигде не строится, эти поля ничего не делали. --- README.md | 58 +++++++++++++++------- docs/ARCHITECTURE.md | 76 ++++++++++++++++++++--------- docs/DIAGRAM.md | 13 +++-- docs/INGESTION.md | 30 +++++++++--- services/api/app/api/reports.py | 76 ----------------------------- services/api/app/main.py | 3 +- services/frontend/src/api/client.ts | 5 -- services/worker-gpu/app/config.py | 2 - 8 files changed, 128 insertions(+), 135 deletions(-) delete mode 100644 services/api/app/api/reports.py diff --git a/README.md b/README.md index 1083653..9bc4953 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ 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) | +| 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 | @@ -99,8 +99,13 @@ make clean # Удалить контейнеры и volumes 1. **Winnowing** — точные/частичные совпадения по fingerprint'ам (xxHash + скользящее окно) 2. **MinHash LSH** — нечёткие совпадения (шинглы + Jaccard, индекс в общем Redis) -3. **FAISS cosine** — семантическое сходство (порог 0.75; IndexFlatIP на нормированных эмбеддингах) -4. **Ollama qwen2.5:7b** — LLM-анализ парафраза (порог confidence 0.7) +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%. ## Тарифные планы @@ -130,8 +135,17 @@ 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](docs/INGESTION.md). + ## Переменные окружения Смотри `.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.selfhosted.yml.example` | Не используется. Полностью автономный вариант (свои Postgres/Redis/RabbitMQ/ES/MinIO/Ollama + GPU passthrough) — на случай отдельного выделенного сервера в будущем. | ## Продакшн деплой -```bash -# На сервере -cp .env.example .env -nano .env # Настроить все пароли и ключи, указать реальные адреса общих Postgres/Redis/RabbitMQ/MinIO/Ollama +Автоматический: push в `main` → Gitea Actions (`.gitea/workflows/deploy.yml`) → гейт +`test` (lint + юнит-тесты) → `deploy` → `scripts/deploy.sh` на app-хосте. Умная +пересборка — образ пересобирается только у сервисов, чей код изменился с прошлого +деплоя (маркер SHA в `.last_deploy_sha`); правка `docker-compose.prod.yml` триггерит +полную пересборку всех пяти бэкенд-сервисов. -# Сертификат ДО первого запуска nginx-контейнера (standalone, порт 80 должен быть свободен) -certbot certonly --standalone -d academic.jze9.ru +**`.env` на проде НЕ редактируется руками.** Первым шагом `deploy.sh` логинится в +self-hosted Infisical (Machine Identity, Universal Auth) и генерирует `.env` заново +из окружения `prod` на каждом запуске — ручные правки файла на сервере переживут +максимум до следующего деплоя. Менять секреты/конфиг — через Infisical +(`https://infisical.jze9.ru`, проект `academ`), не через `.env` напрямую. Если +Infisical недоступен или вернул подозрительно мало ключей, деплой падает раньше +синка кода и не трогает рабочий `.env`. -make build -make up -make migrate -``` +Фронтенд собирается отдельно (`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/`. -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) @@ -199,7 +221,9 @@ docker compose -f docker-compose.prod.yml --profile observability up -d promethe - Prometheus (`infra/prometheus/prometheus.yml`) скрейпит API и **Flower** — из Flower приходят метрики Celery (задачи, время выполнения, воркеры) без доп. кода. - 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` (чистая доменная логика). Конфиги: [`ruff.toml`](ruff.toml), [`mypy.ini`](mypy.ini). -2. **Юнит-тесты** — `pytest` по сервисам: 122 теста на ядро детекции, скоринга, +2. **Юнит-тесты** — `pytest` по сервисам: 113 тестов на ядро детекции, скоринга, парсеров, форматирования и OAuth, без внешней инфры (БД/Redis/GPU/Ollama замоканы либо не нужны). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b92f3ee..f75a959 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/DIAGRAM.md b/docs/DIAGRAM.md index 17af2e5..046fda0 100644 --- a/docs/DIAGRAM.md +++ b/docs/DIAGRAM.md @@ -31,7 +31,9 @@ flowchart TB PG[("PostgreSQL 1.38
users/tasks/documents/
fingerprints/...")] REDIS[("Redis 1.35
кэш · rate-limit · LSH-индекс")] MINIO[("MinIO 1.21
документы · full-text · бэкапы")] - OLLAMA["Ollama .163
qwen2.5:7b"] + OLLAMA["Ollama .109 embedding-gpu
bge-m3, эмбеддинги (L3)"] + OPENROUTER["OpenRouter/DeepSeek
LLM L4 (опц., через singbox-proxy)"] + INFISICAL["Infisical .111
секреты → .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
(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`, по умолчанию выключены. diff --git a/docs/INGESTION.md b/docs/INGESTION.md index 2b2f607..44c85dc 100644 --- a/docs/INGESTION.md +++ b/docs/INGESTION.md @@ -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). diff --git a/services/api/app/api/reports.py b/services/api/app/api/reports.py deleted file mode 100644 index d1846ff..0000000 --- a/services/api/app/api/reports.py +++ /dev/null @@ -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, - } diff --git a/services/api/app/main.py b/services/api/app/main.py index c3b2381..5a0a971 100644 --- a/services/api/app/main.py +++ b/services/api/app/main.py @@ -9,7 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse 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.core.minio_client import ensure_bucket 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(search.router, prefix="/api") app.include_router(documents.router, prefix="/api") -app.include_router(reports.router, prefix="/api") app.include_router(admin.router, prefix="/api") # ─── Метрики Prometheus ─────────────────────────────────────────────────────── diff --git a/services/frontend/src/api/client.ts b/services/frontend/src/api/client.ts index 058bd9a..a09f9d3 100644 --- a/services/frontend/src/api/client.ts +++ b/services/frontend/src/api/client.ts @@ -86,11 +86,6 @@ export const documentsApi = { }, }; -export const reportsApi = { - get: (taskId: string) => - api.get(`/reports/${taskId}`), -}; - // ─── Админка ──────────────────────────────────────────────────────────────── export const adminApi = { openSession: () => api.post('/admin/session'), diff --git a/services/worker-gpu/app/config.py b/services/worker-gpu/app/config.py index 91737f8..3bfd7ba 100644 --- a/services/worker-gpu/app/config.py +++ b/services/worker-gpu/app/config.py @@ -68,8 +68,6 @@ class Settings(BaseSettings): CLOUD_EMBED_URL: str = "https://api.deepinfra.com/v1/openai/embeddings" CLOUD_EMBED_MODEL: str = "BAAI/bge-m3" CLOUD_EMBED_API_KEY: str = "" - FAISS_NLIST: int = 1024 # Количество кластеров для IVFFlat - FAISS_NPROBE: int = 64 # Количество кластеров для поиска # Векторный бэкенд: "faiss" (файловый синглтон, дефолт) или "qdrant" (сервис, # снимает SPOF и конкурентную запись). Переключается без изменения кода.