# Наполнение корпуса — 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 часов. ### Источники русского текста — что проверено (31.08.2026) | Источник | Объём | Годен для массовой заливки | |----------|-------|----------------------------| | **Википедия ru** | дамп 5.6 ГБ, ~2 млн статей | **да** — `bulk_ingest_wikipedia_ru.py`, потоком без блокировок, ~919 отпечатков на статью. Требует осмысленный User-Agent, иначе 403 | | КиберЛенинка | ~3 млн статей | нет — блокирует выкачку PDF после ~130 запросов | | OpenAlex `language:ru` + OA | заявлено 395 410 | практически нет — по прямым `pdf_url` скачалось 2 из 10 (остальное 403 издателей), а метка языка ненадёжна: в выдаче попадаются англоязычные журналы | | eLIBRARY.RU (РИНЦ) | ~40 млн | только по договору, публичной выгрузки нет | | Math-Net.Ru | российские матжурналы | архив отдаёт 403 | | НЭБ, BASE, вузовские DSpace | — | открыты, механизм выгрузки не проверялся | Википедия формально не научный источник, но студенты копируют из неё чаще всего, а по объёму связного русского текста ей нет альтернативы среди доступного. ### Массовая заливка: 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 из списка, чистка ``/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).