Заливку Википедии и PMC я сделал отдельными скриптами мимо существующей инфраструктуры: запуск руками через ssh, состояние в файле в /tmp, никакой видимости. Результат предсказуем — за неделю обе умерли молча (обрыв базы на 20 008 статьях из 2 млн и таймаут сети на 85 тыс. из 100 тыс.), прогресс потерялся, а узнали мы об этом через неделю. При том что рядом лежит готовый механизм: parse_sources, прогоны со шкалой, журнал, кнопки, ретраи Celery. Теперь это обычные типы источника — wikipedia_ru и pmc_bulk: - заводятся и запускаются из админки, как OpenAlex или КиберЛенинка; - показывают ту же шкалу, журнал и кнопку остановки; - падение воркера больше не теряет прогресс: позиция продолжения хранится в parse_sources.resume_token (номер статьи в дампе / токен страницы бакета), повторный запуск берёт следующую порцию; - укладываются в бюджет времени таска — заливка идёт порциями, а не одним многосуточным процессом. Чего не хватало конвейеру для миллионов и что добавлено: - парсеры отдают генератор, а не список: 2 млн статей в память не влезают; - app/bulk_writer.py — запись пачками через COPY (21 тыс. строк/с против 6.7 тыс. построчно) с переподключением к базе при обрыве; - эмбеддинги при массовой заливке не диспатчатся: они на порядок медленнее и стали бы узким местом, вектора досчитываются отдельно (reembed_missing.py). scripts/ops/bulk_ingest_*.py удалены — их работу делает конвейер. Проверено на проде: оба парсера отдают документы, прогон через run_parser завершается штатно, позиция продолжения сдвигается (300 → 600), повторный запуск продолжает с неё. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
16 KiB
Markdown
187 lines
16 KiB
Markdown
# Наполнение корпуса — 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 млн статей | **да** — тип источника `wikipedia_ru`, ~919 отпечатков на статью. Дамп качается заранее (Wikimedia отдаёт 403 без осмысленного User-Agent и рвёт долгие потоковые соединения) |
|
||
| КиберЛенинка | ~3 млн статей | нет — блокирует выкачку PDF после ~130 запросов |
|
||
| OpenAlex `language:ru` + OA | заявлено 395 410 | практически нет — по прямым `pdf_url` скачалось 2 из 10 (остальное 403 издателей), а метка языка ненадёжна: в выдаче попадаются англоязычные журналы |
|
||
| eLIBRARY.RU (РИНЦ) | ~40 млн | только по договору, публичной выгрузки нет |
|
||
| Math-Net.Ru | российские матжурналы | архив отдаёт 403 |
|
||
| НЭБ, BASE, вузовские DSpace | — | открыты, механизм выгрузки не проверялся |
|
||
|
||
Википедия формально не научный источник, но студенты копируют из неё чаще всего,
|
||
а по объёму связного русского текста ей нет альтернативы среди доступного.
|
||
|
||
### Массовые источники — тот же конвейер, что и обычные
|
||
|
||
Постраничные API дают 1-2 статьи в секунду и упираются в rate limit — миллионы
|
||
так не залить. Для объёма есть два типа источника, которые читают дамп или
|
||
бакет потоком:
|
||
|
||
| Тип | Откуда | Особенность |
|
||
|-----|--------|-------------|
|
||
| `wikipedia_ru` | локальный дамп `scripts/parsers/ruwiki.xml.bz2` | позиция продолжения — номер статьи в дампе |
|
||
| `pmc_bulk` | бакет `pmc-oa-opendata` (открыт, ключ не нужен) | позиция — токен страницы бакета; у каждой статьи готовый извлечённый текст |
|
||
|
||
Заводятся и запускаются они как любой другой источник — через админку, и точно
|
||
так же показывают шкалу, журнал и кнопку остановки. Отличия внутри:
|
||
|
||
- парсер отдаёт **генератор**, а не список: миллионы статей в память не влезут;
|
||
- запись идёт пачками через `COPY` (`worker-indexer/app/bulk_writer.py`) —
|
||
построчная вставка даёт 6.7 тыс. строк/с против 21 тыс. у COPY, а миллион
|
||
статей это ~2 млрд отпечатков;
|
||
- **эмбеддинги при заливке не считаются**: они медленнее заливки и сделали бы её
|
||
узким местом. L1 и L2 работают сразу, векторы досчитываются потом
|
||
(`scripts/ops/reembed_missing.py`);
|
||
- позиция продолжения хранится в `parse_sources.resume_token`, поэтому источник
|
||
запускается повторно до исчерпания — каждый прогон берёт следующую порцию и
|
||
укладывается в бюджет времени таска.
|
||
|
||
Планировать объём: 1 млн статей ≈ 2 млрд отпечатков ≈ 200 ГБ в базе с индексами.
|
||
При таком росте индексы перестают помещаться в память сервера БД — проверено на
|
||
практике: поиск L1 деградировал с 31 мс до 484 мс, пока не увеличили RAM и
|
||
`shared_buffers` (см. DR-HA.md).
|
||
|
||
## Что подготовлено
|
||
|
||
- **Парсер 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).
|