Flux d'ingestion (PDF → Qdrant)
Flux d'ingestion (PDF → Qdrant)
Dernière mise à jour : 2026-05-10
Source de vérité : ARCHITECTURE.md § "Flux d'ingestion".
11 étapes
POST /api/games/ingest [multipart : PDF + metadata]
│
1. Upload PDF → /app/pdfs/<slug>-<timestamp>.pdf
│ └─ Choix type : base / extension / advanced_rules / faq
│
2. Extraction texte (pdfjs-dist → RawPage[])
│
3. OCR conditionnel (services/ocr/)
│ ├─ decideOCR(rawPages) : avg chars/page < OCR_AUTO_THRESHOLD ?
│ │ ├─ disabled (OCR_ENABLED=false)
│ │ ├─ no_text (0 page texte → PDF entièrement scanné)
│ │ ├─ low_text (couche texte cassée)
│ │ └─ sufficient (skip OCR)
│ ├─ Si shouldOCR=true :
│ │ ├─ pdftoppm rend chaque page en PNG 300 DPI
│ │ ├─ Pool tesseract (OCR_CONCURRENCY workers, défaut 2)
│ │ ├─ Langues OCR_LANGUAGES (défaut fra+eng)
│ │ ├─ Cache JSON ocr-v1-<slug>.json (sauvé tous les 3 pages)
│ │ └─ Confiance log warn si < OCR_MIN_CONFIDENCE (40)
│ └─ Réinjection texte OCR dans pipeline normal
│
4. Découpage sémantique (chunking/chunker.ts)
│ └─ Paragraphes + overlap, fusion intelligente
│
5. Hierarchy LLM (hierarchy.ts)
│ └─ Claude analyse le doc complet → arbre chapter > section > sub
│ └─ Chaque chunk reçoit hierarchy_path + hierarchy_level
│
6. Contextual Retrieval B (contextual-llm.ts)
│ ├─ Pour chaque chunk : Claude reçoit DOC + hierarchy_path
│ ├─ Génère 1-2 phrases de contexte
│ ├─ 10 SSH parallèles (pool worker)
│ ├─ Cache JSON contexts-v2-<slug>.json (reprise après crash/quota)
│ └─ Le contexte préfixe le chunk avant embedding
│
7. Embeddings TEI bge-m3 (batch 32)
│ └─ Vecteur dense 1024 par chunk (préfixé par contexte LLM)
│
8. Upsert Qdrant (collection rules_<slug>)
│ └─ Payload : hierarchy_path, page_start/end, is_extension, contextual_text, etc.
│
9. Progression SSE (jusqu'à 7 actes UI)
│ ├─ Lecture
│ ├─ [Reconnaissance optique] ← uniquement si OCR a tourné
│ ├─ Découpage
│ ├─ Structuration
│ ├─ Analyse contextuelle
│ ├─ Compréhension (embeddings)
│ └─ Archivage
│
10. [Optionnel, extensions seulement] Détection conflits (conflict-detect.ts)
│ ├─ Pour chaque chunk extension : recherche hybride dans le jeu de base
│ ├─ Si similarité > CONFLICT_SIMILARITY_THRESHOLD (0.65) :
│ │ └─ Claude SSH (CONFLICT_MODEL) classifie : replaces / modifies / extends
│ ├─ 10 SSH parallèles
│ ├─ Cache JSON conflicts-v1-<slug>.json
│ └─ Annotations stockées dans payload Qdrant
│ (conflict_type, conflict_base_chunk_id, conflict_summary, conflict_base_page)
│
11. Rendu PNG des pages (non-fatal)
│ ├─ pdftoppm 300 DPI → /app/pdfs/images/<slug>/page-XX.png
│ ├─ Idempotent : skip si déjà rendu par étape 3 (OCR)
│ └─ Échec → logger.warn, ingestion reste valide
Patterns importants
- Coordinator + stages :
services/ingest/coordinator.tsorchestre, chaque stage est un fichier dansstages/avec signature(game, ..., push: EventPusher) => Promise<...>. - Erreurs non-fatales : try/catch local dans le stage ou autour de l'appel dans le coordinator (cas de
renderPdfPages). - Reprise après quota : tous les stages SSH parallèles flushent le cache JSON avant de propager
ClaudeQuotaError. Le coordinator transforme l'erreur eningestStatus='scheduled'+scheduleIngestStart(gameId, retryAt)(pas'error'). - Auto-OCR transparent : pas de toggle UI. Si
OCR_ENABLED=true(défaut) et seuil dépassé, l'OCR tourne. L'acte "Reconnaissance optique" n'apparaît dans l'UI que si l'OCR a effectivement tourné.
Ingestion planifiée
scheduled_start_at (ISO, max +7 jours) dans le POST :
- Ligne
gamescréée avecingestStatus='scheduled'+ingestScheduledAt - PDF écrit sur disque
setTimeoutarmé danscron/ingest-scheduler.ts- Au boot :
restoreScheduledOnBoot()re-programme tous les timers en attente - Si date passée pendant downtime → start immédiat avec grace 1s
DELETE /api/games/:id/scheduled annule (clear timer + supprime PDF + supprime ligne). 409 si pas en scheduled.
Forums BGG (cron quotidien)
Si BGG_FORUM_SYNC_ENABLED=true :
- Cron à
BGG_FORUM_SYNC_HOUR(défaut 3h) - Pour chaque jeu avec
bgg_iden BDD :getBggForumList()trouve le forum "Rules"getBggForumThreads()récupère threads (max 3 pages = ~150)- Pour chaque thread : articles concaténés (~2000 chars max)
- Embed (dense + sparse) + upsert dans
rules_<slug>avecis_forum_chunk: true
- Rate-limit : 5s entre appels BGG
Chunks forum identifiés par is_forum_chunk: true + tag [FORUM] dans le contexte RAG. Pas de page (page_start: 0, page_end: 0).
Filtrage par intent (depuis 2026-04-26) : chunks is_forum_chunk=true exclus quand intent ∈ {synergy, deckbuilding, meta}. Les forums sont conçus pour clarifier des règles, pas pour discuter deckbuilding.
No comments to display
No comments to display