# RAG su documenti accademici Pipeline completa per trasformare PDF accademici in un sistema RAG interrogabile in linguaggio naturale. **Stack:** Python · MinerU · ChromaDB · Ollama **Chunking:** rule-based, deterministico, senza LLM **Embedding + Generazione:** Ollama (locale) --- ## Prerequisiti ### 1. MinerU — conversione PDF → Markdown strutturato MinerU è il tool esterno che converte i PDF in Markdown + metadati strutturati. Repository: [https://github.com/opendatalab/MinerU](https://github.com/opendatalab/MinerU) #### Requisiti di sistema | Risorsa | Minimo | Raccomandato | |---------|--------|--------------| | Python | 3.10–3.13 | 3.11 | | RAM | 16 GB | 32 GB+ | | Disco | 20 GB (modelli inclusi) | 40 GB+ | | GPU | opzionale | 4–8 GB VRAM (CUDA) | | OS | Linux, macOS, Windows | Linux / WSL2 per Docker | #### Installazione **pip (raccomandato):** ```bash pip install mineru[all] # oppure con uv (più veloce): uv pip install -U "mineru[all]" ``` **conda:** ```bash conda create -n mineru python=3.11 conda activate mineru pip install mineru[all] ``` **Docker (Linux / WSL2 only):** ```bash # CPU docker run --rm -v /percorso/locale:/data opendatalab/mineru mineru -p /data/doc.pdf -o /data/output # GPU (richiede nvidia-container-toolkit) docker run --rm --gpus all -v /percorso/locale:/data opendatalab/mineru:cuda mineru -p /data/doc.pdf -o /data/output ``` Al primo avvio MinerU scarica automaticamente i modelli (~15 GB). Per forzare il download manuale: ```bash mineru-models-download ``` #### Uso — CLI ```bash mineru -p -o [opzioni] ``` | Flag | Descrizione | Default | |------|-------------|---------| | `-p` | Percorso PDF, cartella di PDF, o URL | — | | `-o` | Cartella di output | — | | `-b` | Backend di conversione (vedi sotto) | `pipeline` | | `-m` | Metodo di estrazione: `auto`, `txt`, `ocr` | `auto` | | `-l` | Lingua per OCR (es. `it`, `en`, `zh`) | `en` | | `-s` | Pagina iniziale (0-based) | 0 | | `-e` | Pagina finale (0-based, inclusa) | ultima | | `--formula` | Attiva/disattiva riconoscimento formule | config | | `--table` | Attiva/disattiva riconoscimento tabelle | config | **Esempio tipico:** ```bash mineru -p articolo.pdf -o sources/articolo/ # Output in sources/articolo/auto/ ``` Per estrarre solo alcune pagine: ```bash mineru -p documento.pdf -o sources/documento/ -s 10 -e 50 ``` #### Backend di conversione | Backend | Descrizione | Accuratezza | GPU richiesta | |---------|-------------|-------------|---------------| | `pipeline` | CPU-only, modelli leggeri | ~86% | No | | `hybrid-auto-engine` | Combina pipeline + VLM selettivo | ~95%+ | Raccomandato | | `vlm-auto-engine` | VLM completo su tutte le pagine | massima | Sì | | `pipeline-http` / `vlm-http` | Come sopra ma via API remota | — | Remota | ```bash # Raccomandato se si ha una GPU con ≥ 4 GB VRAM mineru -p doc.pdf -o output/ -b hybrid-auto-engine # Solo CPU mineru -p doc.pdf -o output/ -b pipeline ``` #### Configurazione avanzata File di configurazione utente: `~/mineru.json` Variabili d'ambiente principali: | Variabile | Valori | Effetto | |-----------|--------|---------| | `MINERU_FORMULA_ENABLE` | `0` / `1` | Abilita/disabilita OCR formule matematiche | | `MINERU_TABLE_ENABLE` | `0` / `1` | Abilita/disabilita riconoscimento tabelle | | `MINERU_DEVICE` | `cpu`, `cuda`, `mps` | Forza il dispositivo di inferenza | #### Output di MinerU MinerU produce per ogni PDF una cartella con questa struttura: ``` / └── auto/ ├── .md ← Markdown grezzo (non usato dalla pipeline) ├── _content_list_v2.json ← struttura ricca: tipo, livello, bbox [RICHIESTO] ├── _model.json ← label di layout: doc_title, abstract… [RACCOMANDATO] ├── _middle.json ← dati intermedi (non usato) ├── _content_list.json ← formato v1 flat (non usato) ├── _layout.pdf ← PDF annotato con i bounding box ├── _span.pdf ← PDF con span di testo evidenziati └── images/ ← immagini estratte ``` `` è il nome del documento, usato come identificatore in tutta la pipeline. I file usati dalla pipeline di questo repository sono: - **`_content_list_v2.json`** ← struttura ad albero: ogni blocco ha `type` (title/paragraph/table/list/image), `level` (per i titoli: 1=capitolo, 2=sezione), `content` con il testo, e `bbox` per la posizione sulla pagina. - **`_model.json`** ← label semantiche per bounding box: `doc_title`, `paragraph_title`, `abstract`, `header`, `number`, `text`, `aside_text`… Usato per arricchire la classificazione del testo (es. riconoscere SOMMARIO interni, prefissi di capitolo). #### Limitazioni note di MinerU - **Testo verticale** (CJK ruotato, riviste asiatiche): qualità ridotta. - **Testo scritto a mano**: accuratezza molto bassa. - **Fumetti e album d'arte**: non supportati. - **Blocchi di codice**: non sempre preservati correttamente (indentazione può essere persa). - **PDF scansionati senza OCR**: richiede backend con OCR attivo (`-m ocr`). - **Formule matematiche complesse**: dipendono dalla qualità del rendering PDF originale. ### 2. Ollama — embedding e generazione Scarica e avvia [Ollama](https://ollama.com), poi installa i modelli: ```bash ollama pull nomic-embed-text # embedding (obbligatorio) ollama pull qwen3.5:4b # generazione LLM (o altro modello) ``` --- ## Setup ```bash git clone cd rag python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt ``` --- ## Configurazione rapida Prima di usare la pipeline verifica i parametri nei due file di configurazione: **`chunks/config.py`** — parametri di chunking: | Parametro | Default | Quando cambiarlo | |-----------|---------|-----------------| | `MAX_CHARS` | 1200 | Aumenta per chunk più lunghi (es. testi narrativi) | | `MIN_CHARS` | 80 | Abbassa se hai molti titoli brevi | | `FRONTMATTER_HEADINGS` | set italiano | Svuota (`set()`) per documenti non italiani | | `OVERLAP_SENTENCES` | 1 | Aumenta a 2 se il retrieval perde contesto tra chunk | **`config.py`** — parametri RAG: | Parametro | Default | Quando cambiarlo | |-----------|---------|-----------------| | `OLLAMA_MODEL` | `qwen3.5:4b` | Sostituisci con il modello Ollama che vuoi usare | | `EMBED_MODEL` | `nomic-embed-text` | Deve corrispondere al modello usato in ingestion | | `TOP_K` | 6 | Aumenta per recuperare più contesto per domanda | | `OLLAMA_URL` | `localhost:11434` | Cambia se Ollama gira su un altro host/porta | > Se cambi `EMBED_MODEL` devi rieseguire l'ingestion con `--force`. --- ## Flusso completo ``` PDF │ ▼ (MinerU — esterno) sources//auto/ │ ▼ python chunks/chunker.py --stem │ Stage 1: _content_list_v2.json + _model.json → _clean.md │ Stage 2: _clean.md → chunks//chunks.json │ ▼ python chunks/verify_chunks.py --stem │ Verifica qualità chunk │ ▼ python chunks/fix_chunks.py --stem [se necessario] │ Correzioni automatiche │ ▼ python ingestion/ingest.py --stem │ Embedding → ChromaDB │ ▼ python rag.py --stem Interrogazione in linguaggio naturale ``` --- ### Passo 1 — Converti il PDF con MinerU Per questo step è necessario **MinerU** ([github.com/opendatalab/MinerU](https://github.com/opendatalab/MinerU)). **Opzione A — Google Colab (consigliata, nessuna installazione locale)** Usa il notebook incluso in questo repository: ``` mineru_demo.ipynb ``` Aprilo su Google Colab, carica il tuo PDF nella sessione e segui le celle in ordine. Al termine scarica la cartella `output//` generata. **Opzione B — Installazione locale** ```bash pip install "mineru[all]" mineru -p documento.pdf -o sources// ``` Al termine, **copia la cartella completa** prodotta da MinerU dentro `sources/`. La struttura attesa è: ``` sources/ └── / └── auto/ ├── .md ├── _content_list_v2.json ← richiesto ├── _model.json ← raccomandato ├── _middle.json ├── _content_list.json ├── _layout.pdf ├── _span.pdf └── images/ ``` ### Passo 2 — Chunking (Stage 1 + Stage 2) Un singolo comando esegue entrambe le fasi: ```bash # Singolo documento .venv/bin/python chunks/chunker.py --stem # Tutti i documenti in sources/ .venv/bin/python chunks/chunker.py # Rigenera tutto da zero .venv/bin/python chunks/chunker.py --stem --force # Salta Stage 1 (usa il _clean.md già presente) .venv/bin/python chunks/chunker.py --stem --skip-optimize ``` **Stage 1 — Ottimizzazione Markdown** (`md_optimizer.py` interno): - Legge `_content_list_v2.json` e `_model.json` - Riconosce la gerarchia heading (H1 = capitolo, H2 = sezione, H3 = sottosezione) - Fonde titoli di capitolo multipli: `CAPITOLO 2` + `Il titolo` → `# CAPITOLO 2 — Il titolo` - Rimuove TOC, frontespizio, copyright, indici e sommari interni - Produce `sources//auto/_clean.md` **Stage 2 — Chunking semantico**: - Un chunk per paragrafo — mai due paragrafi nello stesso chunk - Split a confine di frase se il paragrafo supera `MAX_CHARS` (default 1200 chars) - Overlap di 1 frase tra chunk consecutivi per continuità contestuale - Tabelle e liste sono blocchi atomici (non vengono mai spezzati) - Ogni chunk include un prefisso `[Sezione > Titolo]` per il retrieval Output in `chunks//`: | File | Contenuto | |------|-----------| | `chunks.json` | Lista di chunk con testo, sezione, titolo, n_chars | | `meta.json` | Parametri usati (max_chars, overlap, strategia) | | `report.json` | Statistiche e anomalie (generato da verify) | ### Passo 3 — Verifica i chunk ```bash .venv/bin/python chunks/verify_chunks.py --stem ``` | Verdict | Significato | Cosa fare | |---------|-------------|-----------| | `ok` | Nessun problema | Procedi alla vettorizzazione | | `warnings_only` | Solo avvisi minori (frasi lunghe) | Puoi procedere | | `blocked` | Chunk incompleti o senza prefisso | Esegui il fix | ### Passo 4 — Correggi i problemi (se verdict = `blocked`) ```bash # Anteprima senza applicare .venv/bin/python chunks/fix_chunks.py --stem --dry-run # Applica le correzioni .venv/bin/python chunks/fix_chunks.py --stem ``` Il fix risolve automaticamente: chunk incompleti (frase spezzata), chunk troppo corti (accorpati al successivo), chunk troppo lunghi (spezzati su punteggiatura). Itera fino alla convergenza o per un massimo di `FIX_MAX_ITERATIONS` passate. ### Passo 5 — Vettorizzazione Verifica che Ollama sia attivo, poi: ```bash # Singolo documento → collection omonima .venv/bin/python ingestion/ingest.py --stem # Più documenti → un'unica collection condivisa .venv/bin/python ingestion/ingest.py --collection --stems doc1 doc2 doc3 # Rigenera dopo aver aggiornato i chunk o cambiato modello embedding .venv/bin/python ingestion/ingest.py --stem --force ``` > Se cambi `EMBED_MODEL` in `config.py`, devi rilanciare l'ingestion con `--force`. ### Passo 6 — Interrogazione ```bash # RAG interattivo .venv/bin/python rag.py --stem .venv/bin/python rag.py --collection # Retrieval puro (debug, senza generazione LLM) .venv/bin/python retrieve.py --stem .venv/bin/python retrieve.py --stem --top-k 10 ``` Comandi nel loop interattivo di `retrieve.py`: | Input | Effetto | |-------|---------| | `` | Chunk più simili con score (testo troncato) | | ` -f` | Testo completo dei chunk | | `exit` | Termina | --- ## Configurazione ### Parametri di chunking — `chunks/config.py` | Parametro | Default | Descrizione | |-----------|---------|-------------| | `MAX_CHARS` | 1200 | Lunghezza massima chunk in caratteri | | `MIN_CHARS` | 80 | Soglia minima (warning sotto questa soglia) | | `OVERLAP_SENTENCES` | 1 | Frasi di overlap tra chunk consecutivi | | `MIN_CONTENT_CHARS` | 2500 | Soglia per distinguere frontespizio da contenuto reale | | `FRONTMATTER_HEADINGS` | set italiano | Sezioni da rimuovere (Sommario, Indice, Autori…) — svuotare per documenti non italiani | | `SOMMARIO_PATTERNS` | 5 lingue | Pattern regex per sommari interni da saltare | | `CHAPTER_PREFIX_PATTERNS` | 4 lingue | Pattern per prefissi di capitolo come paragrafi separati | | `MODEL_SKIP_LABELS` | set | Label `_model.json` da ignorare (header, number…) | ### Parametri RAG — `config.py` | Parametro | Default | Descrizione | |-----------|---------|-------------| | `OLLAMA_MODEL` | `qwen3.5:4b` | Modello LLM per la generazione | | `EMBED_MODEL` | `nomic-embed-text` | Modello embedding | | `TOP_K` | 6 | Chunk recuperati per domanda | | `TEMPERATURE` | 0.2 | Creatività del modello (0 = deterministico) | | `OLLAMA_URL` | `localhost:11434` | URL server Ollama | --- ## Struttura del repository ``` rag/ ├── sources/ ← output di MinerU (una cartella per documento) │ └── /auto/ ├── chunks/ │ ├── chunker.py ← pipeline unificata (Stage 1 + Stage 2) │ ├── md_optimizer.py ← Stage 1: JSON MinerU → _clean.md │ ├── config.py ← tutti i parametri di chunking │ ├── verify_chunks.py ← verifica qualità │ └── fix_chunks.py ← correzioni automatiche ├── ingestion/ │ └── ingest.py ← embedding → ChromaDB ├── ollama/ │ └── check_env.py ← verifica ambiente Ollama ├── chroma_db/ ← database vettoriale (ignorato da git) ├── config.py ← parametri RAG (modelli, TOP_K, prompt) ├── rag.py ← loop RAG interattivo └── retrieve.py ← retrieval puro (debug) ``` --- ## Note su generalità e adattamento La pipeline è progettata per funzionare con **qualsiasi output MinerU**, non solo per documenti in italiano. MinerU produce sempre la stessa struttura di file (`_content_list_v2.json`, `_model.json`) indipendentemente dal documento sorgente. Per adattare a documenti in altre lingue o con struttura diversa, modificare in `chunks/config.py`: - **`FRONTMATTER_HEADINGS`**: impostare `set()` per disabilitare, o aggiungere i nomi delle sezioni da rimuovere nella lingua del documento - **`SOMMARIO_PATTERNS`**: aggiungere/rimuovere pattern regex per i sommari interni - **`CHAPTER_PREFIX_PATTERNS`**: aggiungere pattern per la lingua del documento - **`MIN_CONTENT_CHARS`**: alzare se il documento ha lunghe sezioni di copyright/prefazione da escludere - **`MIN_TOC_HEADINGS`**: abbassare se il documento ha pochi capitoli (es. 3-4)