Files
anti-plagiarism/docs/INGESTION.md
jze9 e4ca4a7150
All checks were successful
Deploy / test (push) Successful in 2m59s
Deploy / deploy (push) Successful in 3s
docs: сервер эмбеддингов переехал на .1.40 + честные итоги проверок
Прод переключён на новый сервер эмбеддингов (VM 210 «embedding-cpu»,
192.168.1.40, хост pve2). Ключевое, чего не было в исходном плане переключения:
OLLAMA_URL живёт в Infisical, и deploy.sh генерирует .env из него на каждом
запуске — правка .env на сервере откатилась бы первым же деплоем.

Совместимость векторов проверена прямым сравнением, а не на слово: косинус
0.9999997, расхождение 1e-4 (округление AVX2 против AVX-512) — переиндексация
93 тыс. векторов не понадобилась.

Документация приведена в соответствие с фактами:
- ARCHITECTURE/DIAGRAM/README/CREDENTIALS: новый сервер, старый помечен как
  выведенный; убрано упоминание CUDA — GPU в проекте нет;
- DR-HA: эмбеддинги больше не висят на хосте .254, чьё падение 28.08 разом
  унесло брокер, прокси и секреты;
- INGESTION: исправлено собственное враньё про КиберЛенинку — «48 часов на
  100 тысяч» опровергнуто практикой, сайт блокирует выкачку после ~130 статей;
  снапшот OpenAlex вычеркнут как путь к миллионам (там только метаданные).

bulk_ingest_pmc.py: снята пометка «не закончен» — бага не было, первый прогон
упал уже после успешной вставки, а второй корректно пропустил дубли. Проверено:
100 статей, 183 090 отпечатков. Реальный темп 0.3 ст/с записан честно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:06:41 +05:00

156 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.
# Наполнение корпуса — runbook
## Текущее состояние (на 2026-08-31)
- **177 247 документов**, русский большинство: `ru` 99 884, `en` 77 036,
остальные языки — единицы/десятки. Проблема «не с чем сравнивать русские
работы» из более ранней версии этого документа закрыта.
- По источникам: CyberLeninka 99 883, OpenAlex 49 276, PMC 15 010, arXiv 13 077,
`user_submission` (проверенные пользователями работы, не источник для сравнения
сами с собой — см. ARCHITECTURE.md §7) 1.
- Повторный прогон по уже залитым источникам даёт почти одни дубли (типично
1490 из 1500 на источник): прирост дают только новые публикации. Реальный
рост корпуса — поднятый `limit` или новые темы, а не повторный запуск.
- OpenAlex при массовом запуске упирается в лимит вежливого пула (10 req/s
на mailto, общий для всех воркеров) — пауза между страницами поднята до 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 по широкому списку дисциплин, добавлен позже
первой волны).
- Операционный риск массовой докачки (`consumer_timeout` RabbitMQ vs долгие таски)
закрыт бюджетом времени прогона и `worker_prefetch_multiplier=1` — подробности
и почему префетч тут главный, см. [DR-HA.md](DR-HA.md) §6.
### Глубина индексации — чем реально располагает детекция (замер 2026-08-28)
Мерить глубину по `minio_key` (сохранён ли текст в MinIO) **нельзя**: PMC,
например, кладёт тело статьи в отпечатки прямо при заливке, не сохраняя файл.
Честный показатель — **число отпечатков на документ**: аннотация даёт десятки,
полный текст — тысячи.
| Источник | Документов | Глубоко (≥500 отпечатков) | Среднее отпечатков |
|----------|-----------:|--------------------------:|-------------------:|
| КиберЛенинка | 99 883 | 0 (0.0%) | **30** |
| OpenAlex | 49 276 | 6 240 (12.7%) | 907 |
| PMC | 14 910 | 14 519 (97.4%) | 2 208 |
| arXiv | 13 077 | 12 236 (93.6%) | 4 035 |
**Главная слабость — русская часть корпуса.** 56% базы (КиберЛенинка) в индексе
представлено заголовком и аннотацией: списывание из тела русской статьи L1 не
найдёт, хотя сервис рассчитан именно на русских студентов. Причина не в
алгоритме: search API отдаёт только аннотацию и OCR-фрагмент (~700 символов), а
`url` ведёт на HTML-страницу — докачка по нему бесполезна (замер: 0 из 8).
Частично лечится `scripts/ops/backfill_cyberleninka_pdf.py`: PDF доступен прямым
адресом `{url}/pdf` (проверено: 44 из 50, в среднем 23 тыс. символов, глубина
30 → ~1450 отпечатков).
**Но массовый прогон упирается в защиту сайта.** Замер 2026-08-28: первые ~130
статей скачались штатно, дальше КиберЛенинка перестала отдавать PDF и начала
возвращать HTML-заглушку ~5.7 КБ — счётчик успехов замер на 122, скрипт работал
вхолостую. Расчёт «48 часов на 100 тысяч при 1 req/s» этим опровергнут. Чтобы
углубить русскую часть корпуса, нужен другой подход: заметно большие паузы,
разные исходящие адреса или договорённость с источником.
Покрытие L3: 60 993 документа числились векторизованными ошибочно — отметки
сброшены `faiss_reconcile.py`, после чего 31.08 запущен пересчёт всех 84 094
документов без вектора. Считает новый сервер эмбеддингов (192.168.1.40,
3.6 док/с против 2.2 у прежнего), полный проход занимает около 6.5 часов.
### Массовая заливка: bulk вместо API
Постраничные API дают 1-2 статьи в секунду и упираются в rate limit — миллионы
так не залить. Для объёма есть `scripts/ops/bulk_ingest_pmc.py`: открытый бакет
`pmc-oa-opendata` (ключ не нужен) отдаёт у каждой статьи готовый извлечённый
текст. Проверено 31.08: 100 статей, 183 090 отпечатков — 1831 на статью, то есть
настоящая глубина, а не аннотация.
Ограничение по темпу: 0.3 статьи/с, миллион в один поток — около 40 суток.
Скачивание тут не узкое место (12 статей/с в 12 потоков), упирается в
последовательную обработку документа; для миллионов её надо распараллелить по
ядрам воркера. Планировать объём: 1 млн статей ≈ 2.5 млрд отпечатков ≈ 400 ГБ
в базе с индексами (сейчас 113 млн строк занимают 12 ГБ).
## Что подготовлено
- **Парсер CyberLeninka починен** (`scripts/parsers/cyberleninka.py`): раньше слал GET
на `/api/search` → HTTP 405; теперь POST с JSON-телом (`mode=articles`), authors из
списка, чистка `<b>`/HTML-сущностей. Проверено вживую (5/5 тем) + юнит-тесты
(`scripts/parsers/tests/`), в гейте CI.
- **Прод-путь готов**: `index.run_parser(source_id)` уже умеет `cyberleninka` и
`openalex` c `lang=ru`.
- **Сидер источников**: `scripts/seed_ru_sources.py` — 30 студенческих дисциплин.
## Запуск русской заливки
```bash
# 1. (на app-хосте / в контейнере worker-indexer, где есть psycopg2 и прод-.env
# — .env на проде теперь генерируется из Infisical на каждом деплое, см.
# ARCHITECTURE.md §10, руками его не редактировать)
# Посмотреть план:
python scripts/seed_ru_sources.py
# Создать источники в parse_sources (лимит на дисциплину):
python scripts/seed_ru_sources.py --apply --limit 500
# 2. Проверить пару источников на темпе/качестве, затем запустить заливку:
# • Админ-панель → «Источники» → «Запустить» (или «Запустить всё»), ЛИБО
# • Celery: index.run_parser.delay(source_id) по каждому id
```
## Управление заливкой из админки
Всё, что ниже, доступно на странице «Источники» — CLI для этого больше не нужен:
- **Шкала загрузки** у каждого источника: стадия (выборка → индексация), сколько
получено из скольки, сколько добавлено/дублей/ошибок. Раскрытая строка —
журнал прогона по шагам с таймингами (таблица `parse_runs`).
- **«Запустить всё»** — прогон по всем включённым источникам; уже идущие
пропускаются. **«Остановить всё»** и остановка по одному — кооперативная
отмена: воркер останавливается сам на ближайшем тике, не обрывая запись в базу.
- **Пакетное добавление** — один тип источника + список тем (по строке),
опционально с немедленным запуском. Заменяет `seed_*.py` для разовых расширений.
- **Загрузка работ в базу** — PDF/DOCX/TXT прямо в корпус сравнения
(`index.ingest_upload`, `source=manual_upload`), минуя проверку и отстойник.
- **Страница «Отладка»** — очереди, воркеры, покрытие эмбеддингами, зависшие и
упавшие прогоны (см. ARCHITECTURE.md §12).
После большой заливки — свериться с покрытием L3 (панель отладки, «Векторов в
индексе»). Порядок именно такой, из двух шагов:
1. `scripts/ops/faiss_reconcile.py` (в контейнере worker-gpu) — сверяет отметки
`faiss_id` с реальным содержимым индекса и обнуляет ложные. Без этого шага
документы, потерявшие вектор при пересоздании индекса, не попадут на
пересчёт: они всё ещё «отмечены».
2. `scripts/ops/reembed_missing.py` (в контейнере worker-indexer) — отправляет
`gpu.embed_documents` для всех `faiss_id IS NULL`.
Оба по умолчанию dry-run, отправляют/меняют только с `--apply`.
Прогон со статусом `partial` — это не ошибка: сработал бюджет времени
(`PARSER_TIME_BUDGET_S`, 1500с), заливка остановилась раньше `consumer_timeout`
RabbitMQ. Остаток добирается повторным запуском источника.
Заливка сама: fetch (rate-limit 1 req/s) → `add_document` (дедуп по `ext_id`,
fingerprints L1, MinHash L2) → батч-эмбеддинги `gpu.embed_documents` (L3). ~30 тем ×
500 ≈ 15K русских документов на первый заход.
## Проверка результата
```sql
SELECT lang, count(*) FROM documents GROUP BY lang ORDER BY 2 DESC; -- должен появиться ru
SELECT source, count(*) FROM documents WHERE source='cyberleninka'; -- > 0
```
## Масштаб (следующий уровень)
- Больше тем + выше `--limit`; добавить OpenAlex `lang=ru` (качество ниже — англ.
заголовки с меткой ru).
- Для миллионов — **bulk**, а не постраничный API. Снапшот OpenAlex для этого не
годится: там только метаданные, а миллионы аннотаций детекции не дают (см.
провал КиберЛенинки выше). Рабочий источник полных текстов — бакет
`pmc-oa-opendata`, см. «Массовая заливка» выше.
- На масштабе обязателен `VECTOR_BACKEND=qdrant` (FAISS flat не тянет), а таблица
`fingerprints` (уже ~113M строк на 177K доков — партиционирование стоит планировать
заранее, не постфактум) потребует партиционирования. См.
[ARCHITECTURE.md](ARCHITECTURE.md) и [DR-HA.md](DR-HA.md).