💡 Voll funktionsfähiges Beispiel auf GitHub verfügbar: document-to-markdown-rag-chunks-java
Das Problem der Schadensabteilung: Warum RAG‑Ingestion bei echten Dateien scheitert
Eine lokale RAG‑Ingestion‑Pipeline ist eine Dokument‑Parsing‑Schicht, die die Dateien, die ein Team bereits besitzt, in durchsuchbaren, zitierbaren Text umwandelt, ohne sie an einen Drittanbieter zu senden. Versicherungsabläufe zeigen, warum das wichtig ist. Ein Schadensbearbeiter fragt den internen Assistenten: „Was hat der Gutachter zum Wasserschaden in dieser Police gesagt?“, und die Antwort befindet sich in einer E‑Mail vom letzten März, in einem angehängten PDF‑Bericht, unter einer Überschrift namens „Findings“. Der Assistent kann sie nur finden, wenn die Ingestion den Anhang gelesen, die Überschrift beibehalten und sich gemerkt hat, zu welcher E‑Mail der Bericht gehört.
Die meisten Ingestion‑Jobs machen einen dieser drei Fehler. Ein Plain‑Text‑Extraktor gibt nur den E‑Mail‑Body zurück und ignoriert den Anhang. Ein fester 512‑Token‑Splitter schneidet den Bericht mitten in „Findings“ ab. Und ein gehosteter Parser löst beides, jedoch erst, nachdem die Schadensdatei mit ihren persönlichen und medizinischen Details auf die Server eines Dritten hochgeladen und pro Seite abgerechnet wurde.
Was das Team wirklich braucht, ist eng und konkret: Word‑, PDF‑, EML‑ und ZIP‑Dateien im bereits vorhandenen Java‑Service lesen, ihre Struktur als Markdown erhalten, an den Überschriften des Autors splitten und Quelle sowie Eltern‑ID jedem Chunk zuordnen.
Marktüberblick: Die 2026‑Leitfäden zum „Document Parsing for RAG“, die wir geprüft haben, vergleichen Open‑Source‑Python‑Parser mit Cloud‑APIs, die pro Seite abgerechnet werden. Eine Java‑Bibliothek, die auf Ihrer eigenen Hardware läuft, fehlt in diesem Gespräch fast vollständig.
Warum RAG‑Pipelines Markdown statt Plain‑Text benötigen
Markdown bewahrt die Teile eines Dokuments, die einem Retriever zeigen, wo eine Idee endet und die nächste beginnt. Überschriften werden zu #‑Zeilen, Tabellen bleiben als Zeilen und Spalten erhalten, und Listen behalten ihre Elemente. Ein Chunker kann an diesen Überschriften splitten, und ein LLM, das den Chunk liest, sieht dieselbe Gliederung, die eine Person sehen würde. Plain‑Text wirft diese Gliederung weg, sodass nur die Größe als Split‑Signal bleibt.
Warum generische Lösungen hier nicht funktionieren
Teams probieren meist eines von drei Dingen, bevor sie eine richtige Ingestion‑Schicht bauen:
- Plain‑Text‑Extraktion plus Token‑Splitter: schnell eingerichtet, aber die Chunks vermischen Abschnitte und verlieren die Überschrift, die Sie zitieren würden.
- Eine Cloud‑Parsing‑API: gute Struktur, aber Dateien verlassen das Netzwerk und die Rechnung wächst mit jeder neu indizierten Seite.
- Ein eigens entwickelter Parser pro Format: eine Bibliothek für DOCX, eine andere für PDF, ein MIME‑Parser für E‑Mails und
java.util.zipfür Archive, jeweils mit eigenen Randfällen.
GroupDocs.Parser für Java deckt alle vier Formate hinter einer einzigen Parser‑Klasse ab und läuft im Prozess.
Die Lösung: GroupDocs.Parser in einem Claims‑Ingestion‑Workflow
GroupDocs.Parser für Java liest ein Dokument und kann dessen Text über getFormattedText als Markdown zurückgeben. E‑Mails und Archive werden als Container bereitgestellt, deren Elemente als neue Parser‑Instanzen geöffnet werden, sodass ein an eine E‑Mail angehängtes PDF exakt wie ein PDF auf der Festplatte verarbeitet wird. Das Beispielprojekt verpackt das in sechs statische Methoden und schreibt das Ergebnis als JSON‑Lines, die jeder Embedding‑Job oder Vektor‑Store laden kann. Kein RAG‑Framework ist beteiligt; das Ausgabeformat ist der Integrationspunkt.
Um dem Beispiel zu folgen, benötigen Sie:
- JDK 11 oder höher für das mitgelieferte
pom.xml(der Code selbst bleibt bei Java 8‑Features) - GroupDocs.Parser für Java 26.10 (temporäre Lizenz erhalten)
- Eine Handvoll DOCX‑, PDF‑, EML‑ oder ZIP‑Dateien
Das pom.xml des Projekts deklariert das Repository https://releases.groupdocs.com/java/repo/ und die einzige Abhängigkeit, sodass ein Build ein einziger Befehl ist:
mvn compile exec:java
Implementierung der Ingestion‑Schicht Schritt für Schritt
Die nachfolgenden Schritte verarbeiten eine Schadensdatei vom Posteingang bis zum Index. Jeder Schritt entspricht einer oder zwei Methoden in RagChunksDemo.java. Wir haben sie bewusst als reine statische Methoden belassen, damit Sie jede beliebige davon in einen bestehenden Worker übernehmen können, ohne den Rest zu übernehmen.
Schritt 1 – Jede Datei öffnen und weiterleiten
Jede hochgeladene Datei erhält ihr eigenes Parser. Der Dateiname wird zur Source‑ID, und eine Datei auf oberster Ebene hat keinen Eltern‑Eintrag.
// 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 extrahiert dann den Body, splittet ihn und steigt in Anhänge ein, solange die Tiefe unter 2 liegt.
Schritt 2 – Nach Markdown fragen, Plain‑Text akzeptieren
Der Extraktionsschritt prüft, was das Format unterstützt, anstatt eine feste Liste von Erweiterungen zu codieren.
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
Warum diese Einstellungen für Schadensdaten wichtig sind:
FormattedTextMode.Markdown: Die Überschrift des Gutachters „Findings“ gelangt als## Findingszum Chunker, kann also als Split‑Punkt und Zitat verwendet werden.isFormattedText(): Im Beispiel‑Durchlauf kommen das DOCX und die E‑Mail als Markdown zurück, während das PDF auf Plain‑Text zurückfällt; das Feldformatvermerkt, was passiert ist.- Das Ergebnis
none: Schadensfotos in einem ZIP erzeugen ohne OCR keine Chunks – das ist ehrlich und leicht zu überwachen.
Schritt 3 – An den Überschriften des Gutachters splitten
Der Chunker führt einen Stack der aktuellen Überschrift für jede der sechs Markdown‑Ebenen. Eine neue Überschrift schließt den laufenden Abschnitt und setzt alles unterhalb ihrer Ebene zurück.
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');
}
Abschnitte mit mehr als 1500 Zeichen werden an Absatzgrenzen abgeschnitten, sodass ein langer Findings‑Abschnitt zu mehreren Chunks wird, die denselben Überschriftspfad teilen. Im mitgelieferten Word‑Beispiel entstehen Pfade wie Heading 1 > Heading 1.2 > Heading 1.2.1.
Schritt 4 – Anhänge folgen und JSON‑Lines schreiben
Dies ist der Schritt, der der Schadensabteilung am wichtigsten ist. getContainer() listet E‑Mail‑Anhänge und ZIP‑Einträge auf; jedes Element wird als Kind‑Dokument geöffnet und erbt die Eltern‑ID.
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 schreibt dann pro Chunk eine UTF‑8‑Zeile mit id, source, parent, heading_path, format und text. Mit den vier Beispiel‑Eingaben erzeugt der Lauf 22 Zeilen. Der Chunk aus dem Anhang der Beispiel‑E‑Mail lautet email-with-attachment.eml/attachment.txt#0, und sein parent ist email-with-attachment.eml. Dieses Paar ermöglicht es dem Assistenten zu sagen „aus dem Bericht, der an die E‑Mail vom 14. März angehängt war“ statt „irgendwo im Archiv“.
Realer Workflow: Wöchentliche Neu‑Indizierung von Schadensfällen
Stellen Sie sich ein Schadens‑Operationsteam vor, das jeden Freitag die geschlossenen Schadens‑Postfächer als EML‑Dateien und ZIP‑Pakete mit Begleitdokumenten exportiert. Ein Java‑Worker verschiebt diese Exporte bereits in den Dokumentenspeicher. Durch das Hinzufügen der Ingestion wird für jede Datei chunkDocument aufgerufen und die JSON‑Lines‑Ausgabe an die Embedding‑Warteschlange angehängt.
Schritt 4 sorgt dafür, dass ein Reserve‑Bericht an die E‑Mail angehängt bleibt, die ihn geliefert hat. Schritt 3 bedeutet, dass wenn der Assistent „Findings“ zitiert, die Zitation den Dateinamen und die Überschrift nennt statt einer bloßen Chunk‑Nummer. Meine Faustregel für diesen Index lautet: Wenn ein Chunk nicht auf eine Datei und eine Überschrift zurückverfolgt werden kann, sollte er überhaupt nicht im Index stehen.
Geschäftliche Auswirkungen: Was sich dadurch ändert
| Aspekt | Typische Konfiguration vorher | Mit der GroupDocs.Parser‑Pipeline |
|---|---|---|
| Wo Dateien geparst werden | Drittanbieter‑Endpoint | Innerhalb Ihrer eigenen JVM |
| Kostenmodell für das Parsen | Pro Seite, bei jedem Neu‑Index | Keine Cloud‑Gebühr pro Seite |
| Chunk‑Grenzen | Feste Token‑Fenster | Überschriften des Autors, 1500‑Zeichen‑Obergrenze |
| E‑Mail‑Anhänge | Oft übersprungen oder abgeflacht | Als Kind‑Dokumente geparst |
| Zitationsdaten | Dateiname höchstens | source, parent und heading_path |
| Laufzeit‑Abhängigkeiten | SDK + Netzwerkzugriff | Ein Maven‑Artefakt |
Der Gesamteffekt ist ein Index, den das Compliance‑Team freigeben kann, und ein Assistent, dessen Antworten auf ein echtes Dokument und einen echten Abschnitt verweisen.
Weitere Versicherungsszenarien, in denen GroupDocs.Parser passt
Existiert die Ingestion‑Schicht, kann derselbe Code weitere dokumentintensive Aufgaben unterstützen:
- Suche in Policentexten: Word‑Policendokumente werden nach Klausel‑Überschriften gesplittet, sodass Antworten exakt den Abschnitt zitieren.
- Broker‑Einreichungen: ZIP‑Uploads mit gemischten PDFs und Tabellenkalkulationen werden Mitglied für Mitglied indiziert, jedes an die Einreichung gebunden.
- Beschwerde‑Bearbeitung: Kunden‑E‑Mails und deren Anhänge werden gemeinsam indiziert, wobei die E‑Mail der Eltern‑Chunk jedes Anhangs ist.
Einstieg in Ihrer Schadensumgebung
Klonen Sie das Beispiel, führen Sie es gegen einige Ihrer eigenen exportierten E‑Mails aus und öffnen Sie output/chunks.jsonl, um die Überschriftspfade und Eltern‑IDs zu prüfen, bevor Sie es in Ihren Embedding‑Job einbinden. Die stufenweise Architektur wird im Dokumentations‑Use‑Case für diese Pipeline beschrieben, und die Markdown‑Ausgabe‑Referenz listet auf, welche Formatierungen der Modus beibehält.