💡 Esempio completo funzionante disponibile su GitHub: document-to-markdown-rag-chunks-java
Il problema del desk dei sinistri: perché l’ingestione RAG si rompe con file reali
Una pipeline di ingestione RAG on‑premise è uno strato di parsing dei documenti che trasforma i file già in possesso del team in testo ricercabile e citabile senza inviarli a terzi. Le operazioni assicurative mostrano perché è importante. Un gestore di sinistri chiede all’assistente interno “cosa ha detto il perito riguardo al danno da acqua su questa polizza?”, e la risposta si trova in un’email di marzo scorso, all’interno di un report PDF allegato, sotto un’intestazione chiamata “Findings”. L’assistente può trovarla solo se l’ingestione legge l’allegato, conserva l’intestazione e ricorda a quale email appartiene il report.
La maggior parte dei lavori di ingestione sbaglia uno di questi tre aspetti. Un estrattore di testo semplice restituisce il corpo dell’email e ignora l’allegato. Un separatore a token fisso di 512 token taglia il report a metà dell’intestazione “Findings”. E un parser ospitato risolve entrambi i problemi, ma solo dopo che il file del sinistro, con i suoi dati personali e medici, è stato caricato sui server di qualcun altro e fatturato per pagina.
Ciò di cui il team ha bisogno è ristretto e concreto: leggere file Word, PDF, EML e ZIP all’interno del servizio Java che già possiede i dati, mantenere la loro struttura come Markdown, suddividerli alle intestazioni dell’autore e allegare la fonte e il genitore a ogni chunk.
Snapshot di mercato: le guide del 2026 su “document parsing for RAG” che abbiamo recensito confrontano parser Python open‑source con API cloud fatturate per pagina. Una libreria Java che gira sul proprio hardware è quasi assente da quella discussione.
Perché le pipeline RAG hanno bisogno di Markdown anziché di testo semplice?
Il Markdown conserva le parti di un documento che indicano a un retriever dove termina un’idea e inizia la successiva. Le intestazioni diventano linee #, le tabelle rimangono come righe e colonne, e le liste mantengono i loro elementi. Un chunker può suddividere su quelle intestazioni, e un LLM che legge il chunk vede la stessa struttura che vedrebbe una persona. Il testo semplice elimina quella struttura, lasciando la dimensione come unico segnale per la suddivisione.
Perché le soluzioni generiche non funzionano qui
I team di solito provano una delle tre cose prima di costruire uno strato di ingestione adeguato:
- Estrazione di testo semplice più un separatore di token: veloce da configurare, ma i chunk mescolano sezioni e perdono l’intestazione che si vorrebbe citare.
- Un’API di parsing cloud: buona struttura, ma i file lasciano la rete e la bolletta cresce con ogni pagina re‑indicizzata.
- Un parser fatto in casa per formato: una libreria per DOCX, un’altra per PDF, un parser MIME per le email e
java.util.zipper gli archivi, ognuno con i propri casi limite.
GroupDocs.Parser per Java copre tutti e quattro i formati dietro una singola classe Parser e gira in‑processo.
La soluzione: GroupDocs.Parser in un flusso di lavoro di ingestione dei sinistri
GroupDocs.Parser per Java legge un documento e può restituire il suo testo come Markdown tramite getFormattedText. Email e archivi sono esposti come contenitori i cui elementi si aprono come nuove istanze Parser, così un PDF allegato a un’email viene processato esattamente come un PDF su disco. Il progetto di esempio avvolge tutto in sei metodi statici e scrive il risultato come JSON Lines, che qualsiasi lavoro di embedding o vector store può caricare. Nessun framework RAG è coinvolto; il formato di output è il punto di integrazione.
Per seguire l’esempio ti serve:
- JDK 11 o successivo per il
pom.xmlfornito (il codice stesso si limita a funzionalità Java 8) - GroupDocs.Parser per Java 26.10 (get a temporary licence)
- Una manciata di file DOCX, PDF, EML o ZIP
Il pom.xml del progetto dichiara il repository https://releases.groupdocs.com/java/repo/ e la singola dipendenza, quindi la compilazione è un solo comando:
mvn compile exec:java
Implementare lo strato di ingestione passo dopo passo
I passaggi seguenti seguono un file di sinistro dalla casella di posta all’indice. Ogni passo corrisponde a uno o due metodi in RagChunksDemo.java. Li abbiamo mantenuti come semplici metodi statici di proposito, così puoi sollevare uno qualsiasi di essi in un worker esistente senza adottare il resto.
Passo 1 - Aprire ogni file e instradarlo
Ogni file caricato ottiene il proprio Parser. Il nome del file diventa l’ID della fonte, e un file di livello superiore non ha genitore.
// One call per uploaded claim document
String sourceId = new File(path).getName();
try (Parser parser = new Parser(path)) {
return chunkParser(parser, sourceId, null, 0); // null parent: top-level file
} catch (UnsupportedDocumentFormatException ex) {
// An unknown format is logged and skipped, the batch keeps going
System.out.println("skipped (unsupported format): " + sourceId);
return new ArrayList<>();
}
chunkParser estrae quindi il corpo, lo suddivide e discende negli allegati finché la profondità è inferiore a 2.
Passo 2 - Richiedere Markdown, accettare testo semplice
Il passo di estrazione verifica cosa supporta il formato invece di codificare una lista di estensioni.
if (parser.getFeatures().isFormattedText()) {
// Headings, lists and tables survive as Markdown
FormattedTextOptions options =
new FormattedTextOptions(FormattedTextMode.Markdown);
try (TextReader reader = parser.getFormattedText(options)) {
if (reader != null) {
return new ExtractedText("markdown", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
try (TextReader reader = parser.getText()) {
if (reader != null) { // fallback for formats without formatted text
return new ExtractedText("plain", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return new ExtractedText("none", ""); // e.g. a photo with no OCR
Perché queste impostazioni sono importanti per i dati dei sinistri:
FormattedTextMode.Markdown: l’intestazione “Findings” del perito arriva al chunker come## Findings, così può diventare un punto di divisione e una citazione.isFormattedText(): nell’esecuzione di esempio il DOCX e l’email tornano come Markdown mentre il PDF ricade su testo semplice, e il campoformatregistra quale caso si è verificato.- Il risultato
none: le foto dei sinistri dentro un ZIP non producono chunk senza OCR, il che è onesto e facile da monitorare.
Passo 3 - Dividere alle intestazioni dell’perito
Il chunker mantiene uno stack dell’intestazione corrente per ciascuno dei sei livelli Markdown. Una nuova intestazione chiude la sezione in corso e resetta tutto ciò che è al di sotto del suo livello.
for (String line : markdown.split("\\r?\\n", -1)) {
Matcher heading = HEADING.matcher(line); // "#" to "######" + title
if (heading.matches()) {
// Close the previous section under the old path
addSection(chunks, section.toString(), sourceId, parentId, path, format);
section.setLength(0);
int level = heading.group(1).length();
stack[level - 1] = heading.group(2).trim();
for (int i = level; i < stack.length; i++) {
stack[i] = null; // forget deeper headings
}
path = joinPath(stack); // "Heading 1 > Heading 1.2"
}
section.append(line).append('\n');
}
Le sezioni con più di 1500 caratteri vengono tagliate a interruzioni di paragrafo, così una lunga sezione di findings diventa diversi chunk che condividono lo stesso percorso di intestazione. Nel file Word fornito questo produce percorsi come Heading 1 > Heading 1.2 > Heading 1.2.1.
Passo 4 - Seguire gli allegati e scrivere JSON Lines
Questo è il passo a cui il desk dei sinistri tiene più a cuore. getContainer() elenca gli allegati email e le voci ZIP; ogni elemento si apre come documento figlio e eredita l’ID del genitore.
Iterable<ContainerItem> items = parser.getContainer();
if (items == null) {
return chunks; // not an email or archive
}
for (ContainerItem item : items) {
// "claim-0412.eml/report.pdf" style ids keep the lineage readable
String childId = sourceId + "/" + item.getFilePath();
try (Parser child = item.openParser()) {
chunks.addAll(chunkParser(child, childId, sourceId, depth + 1));
} catch (UnsupportedDocumentFormatException ex) {
System.out.println("skipped (unsupported format): " + childId);
}
}
writeJsonl scrive quindi una riga UTF‑8 per chunk con id, source, parent, heading_path, format e text. Con i quattro input di esempio l’esecuzione produce 22 righe. Il chunk dall’allegato dell’email di esempio è email-with-attachment.eml/attachment.txt#0, e il suo parent è email-with-attachment.eml. Questa coppia permette all’assistente di dire “dal report allegato all’email del 14 marzo” invece di “da qualche parte nell’archivio”.
Flusso di lavoro reale: una reindicizzazione settimanale dei sinistri
Immagina un team di operazioni assicurative che esporta ogni venerdì le caselle di posta dei sinistri chiusi della settimana come file EML e bundle ZIP di documenti di supporto. Un worker Java sposta già quegli export nello store dei documenti. Aggiungere l’ingestione significa chiamare chunkDocument per ogni file e aggiungere l’output JSON Lines alla coda di embedding.
Il Passo 4 è ciò che mantiene un report di riserva allegato all’email che lo ha consegnato. Il Passo 3 garantisce che, quando l’assistente cita “Findings”, la citazione nomi il file e l’intestazione invece di un semplice numero di chunk. La mia regola empirica per questo tipo di indice: se un chunk non può essere citato con riferimento a un file e a un’intestazione, non dovrebbe essere nell’indice.
Impatto sul business: cosa cambia
| Aspetto | Configurazione tipica prima | Con la pipeline GroupDocs.Parser |
|---|---|---|
| Dove i file vengono analizzati | Endpoint di terze parti | All’interno della tua JVM |
| Modello di costo del parsing | Per pagina, a ogni re‑indicizzazione | Nessuna tariffa cloud per pagina |
| Confini dei chunk | Finestre di token fisse | Intestazioni dell’autore, limite di 1500 caratteri |
| Allegati email | Spesso ignorati o appiattiti | Analizzati come documenti figli |
| Dati di citazione | Solo nome file al meglio | source, parent e heading_path |
| Dipendenze runtime | SDK più accesso di rete | Un unico artefatto Maven |
L’effetto combinato è un indice che il team di compliance può approvare e un assistente le cui risposte puntano a un documento reale e a una sezione specifica.
Altri scenari assicurativi in cui GroupDocs.Parser è adatto
Una volta che lo strato di ingestione esiste, lo stesso codice serve altri lavori intensivi di documenti:
- Ricerca nella formulazione delle polizze: documenti Word delle polizze suddivisi per intestazioni di clausola, così le risposte citano la sezione esatta.
- Sottomissioni di broker: upload ZIP con PDF e fogli di calcolo misti indicizzati membro per membro, ciascuno collegato alla sottomissione.
- Gestione dei reclami: email dei clienti e i loro allegati indicizzati insieme, con l’email come genitore di ogni chunk di allegato.
Iniziare nel tuo ambiente di sinistri
Clona il campione, eseguilo su alcune delle tue email esportate e apri output/chunks.jsonl per verificare i percorsi di intestazione e gli ID genitore prima di integrarlo nel tuo lavoro di embedding. Le note architetturali passo‑per‑passo si trovano nel caso d’uso della documentazione per questa pipeline, e il riferimento di output Markdown elenca quali formattazioni il modo conserva.