# 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
```
---
## 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
Usa MinerU per convertire il PDF e posiziona la cartella di output in `sources/`:
```
sources/
└── /
└── auto/
├── _content_list_v2.json
├── _model.json
└── ...
```
### 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)