💡 Volledig werkend voorbeeld beschikbaar op GitHub:
document-to-markdown-rag-chunks-java

Het Probleem bij de Claims Desk: Waarom RAG‑Inname faalt bij echte bestanden

Een on‑premise RAG‑ingestiepijplijn is een document‑parselag die de bestanden die een team al heeft omzet in doorzoekbare, citeerbare tekst zonder ze naar een derde partij te sturen. Verzekeringsoperaties laten zien waarom dat belangrijk is. Een claim‑verwerker vraagt de interne assistent “wat zei de expert over waterschade op deze polis?”, en het antwoord staat in een e‑mail van afgelopen maart, in een PDF‑rapport dat eraan is gekoppeld, onder een kopje genaamd “Findings”. De assistent kan het alleen vinden als de ingestie de bijlage leest, het kopje behoudt en onthoudt bij welke e‑mail het rapport hoort.

De meeste ingestie‑taken doen één van die drie dingen verkeerd. Een plain‑text‑extractor geeft alleen de e‑mail‑body terug en negeert de bijlage. Een vaste 512‑token‑splitter knipt het rapport midden in “Findings”. En een gehoste parser lost beide op, maar alleen nadat het claim‑bestand, met persoonlijke en medische details, is geüpload naar de servers van iemand anders en per pagina wordt gefactureerd.

Wat het team nodig heeft is smal en concreet: lees Word‑, PDF‑, EML‑ en ZIP‑bestanden binnen de Java‑service die de data al bezit, behoud hun structuur als Markdown, split op de koppen van de auteur, en koppel de bron en ouder aan elk fragment.

Marktsnapshot: de gidsen uit 2026 over “document parsing for RAG” die we hebben beoordeeld, vergelijken open‑source Python‑parsers met cloud‑API’s die per pagina worden gefactureerd. Een Java‑bibliotheek die op je eigen hardware draait, ontbreekt grotendeels in dat gesprek.

Waarom RAG‑pijplijnen Markdown nodig hebben in plaats van plain‑text?

Markdown behoudt de delen van een document die een retriever vertellen waar het ene idee eindigt en het volgende begint. Koppen worden #‑regels, tabellen blijven rijen en kolommen, en lijsten behouden hun items. Een chunker kan op die koppen splitsen, en een LLM die het fragment leest ziet dezelfde outline als een persoon. Plain‑text verwijdert die outline, waardoor grootte het enige signaal voor splitsen wordt.

Waarom generieke oplossingen hier niet werken

Teams proberen meestal één van drie dingen voordat ze een juiste ingestielaag bouwen:

  • Plain‑text‑extractie plus een token‑splitter: snel op te zetten, maar fragmenten mengen secties en verliezen het kopje dat je zou citeren.
  • Een cloud‑parsing‑API: goede structuur, maar bestanden verlaten het netwerk en de rekening groeit met elke opnieuw geïndexeerde pagina.
  • Een zelfgebouwde parser per formaat: één bibliotheek voor DOCX, een andere voor PDF, een MIME‑parser voor e‑mail en java.util.zip voor archieven, elk met eigen randgevallen.

GroupDocs.Parser for Java dekt alle vier formaten achter één Parser‑klasse en draait in‑process.

De Oplossing: GroupDocs.Parser in een Claims‑Ingestieworkflow

GroupDocs.Parser for Java leest een document en kan de tekst als Markdown retourneren via getFormattedText. E‑mails en archieven worden blootgesteld als containers waarvan de items openen als nieuwe Parser‑instanties, zodat een PDF‑bijlage bij een e‑mail precies wordt verwerkt als een PDF op schijf. Het voorbeeldproject verpakt dit in zes statische methoden en schrijft het resultaat als JSON‑Lines, die elke embed‑job of vector‑store kan laden. Er is geen RAG‑framework bij betrokken; het uitvoerformaat is het integratiepunt.

Om mee te doen heb je nodig:

  • JDK 11 of hoger voor de meegeleverde pom.xml (de code zelf blijft bij Java 8‑features)
  • GroupDocs.Parser for Java 26.10 (get a temporary licence)
  • Een handvol DOCX‑, PDF‑, EML‑ of ZIP‑bestanden

Het pom.xml‑bestand van het project declareert de https://releases.groupdocs.com/java/repo/‑repository en de enkele afhankelijkheid, zodat een build één commando is:

mvn compile exec:java

Implementatie van de Ingestielaag Stap voor Stap

De onderstaande stappen volgen één claim‑bestand van de inbox tot de index. Elke stap correspondeert met één of twee methoden in RagChunksDemo.java. We hebben ze bewust als eenvoudige statische methoden gehouden, zodat je er één uit kunt tillen naar een bestaande worker zonder de rest over te nemen.

Stap 1 - Open elk bestand en routeer het

Elk geüpload bestand krijgt zijn eigen Parser. De bestandsnaam wordt de bron‑id, en een top‑level bestand heeft geen ouder.

// 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 extraheert vervolgens de body, splitst deze, en duikt in bijlagen zolang de diepte onder 2 blijft.

Stap 2 - Vraag om Markdown, accepteer plain‑text

De extractiestap controleert wat het formaat ondersteunt in plaats van een vaste lijst extensies hard‑coded te gebruiken.

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

Waarom deze instellingen belangrijk zijn voor claim‑data:

  • FormattedTextMode.Markdown: de “Findings”‑kop van de expert komt bij de chunker binnen als ## Findings, zodat het een splitsingspunt en een citaat kan worden.
  • isFormattedText(): in de voorbeeldrun komen de DOCX‑ en e‑mail‑bestanden terug als Markdown, terwijl de PDF terugvalt op plain‑text, en het format‑veld legt vast wat er gebeurde.
  • Het none‑resultaat: claim‑foto’s in een ZIP leveren geen fragmenten zonder OCR, wat eerlijk is en makkelijk te monitoren.

Stap 3 - Split op de koppen van de expert

De chunker houdt een stack bij van de huidige kop op elk van de zes Markdown‑niveaus. Een nieuwe kop sluit de lopende sectie en reset alles onder zijn niveau.

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');
}

Secties van meer dan 1500 tekens worden bij alinea‑breuken afgekapt, zodat een lange findings‑sectie meerdere fragmenten wordt die één kop‑pad delen. In het meegeleverde Word‑bestand levert dit paden op zoals Heading 1 > Heading 1.2 > Heading 1.2.1.

Stap 4 - Volg bijlagen en schrijf JSON‑Lines

Dit is de stap die een claims‑desk het meest interesseert. getContainer() geeft e‑mail‑bijlagen en ZIP‑items weer; elk item opent als een kind‑document en erft de id van de ouder.

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 schrijft vervolgens één UTF‑8‑regel per fragment met id, source, parent, heading_path, format en text. Met de vier voorbeeld‑inputs produceert de run 22 regels. Het fragment van de bijlage van de voorbeeld‑e‑mail is email-with-attachment.eml/attachment.txt#0, en de parent is email-with-attachment.eml. Dat paar maakt het mogelijk dat de assistent zegt “van het rapport bijgevoegd bij de e‑mail van 14 maart” in plaats van “van ergens in het archief”.

Werkelijke Workflow: Een Wekelijkse Claims‑Herindex

Stel je een claims‑operatieteam voor dat elke vrijdag de gesloten‑claim‑mailboxen van de week exporteert als EML‑bestanden en ZIP‑bundels met ondersteunende documenten. Een Java‑worker verplaatst die exports al naar de document‑store. Ingestie toevoegen betekent dat je chunkDocument aanroept voor elk bestand en de JSON‑Lines‑output toevoegt aan de embed‑queue.

Stap 4 is wat een reserve‑rapport bij de e‑mail houdt die het leverde. Stap 3 betekent dat wanneer de assistent “Findings” citeert, de bron de bestandsnaam en de kop vermeldt in plaats van een kale fragment‑nummer. Mijn vuistregel voor dit soort index: als een fragment niet kan worden terugverwezen naar een bestand en een kop, hoort het helemaal niet in de index.

Zakelijke Impact: Wat Dit Verandert

Aspect Typische configuratie vóór Met de GroupDocs.Parser‑pijplijn
Waar bestanden worden geparseerd Endpoint van derde partij Binnen je eigen JVM
Kostenmodel voor parsing Per pagina, bij elke her‑index Geen per‑pagina cloud‑kosten
Chunk‑grenzen Vaste token‑vensters Koppen van auteur, 1500‑karakter limiet
E‑mail‑bijlagen Vaak overgeslagen of afgevlakt Geparseerd als kind‑documenten
Citaatgegevens Bestandsnaam op zijn best source, parent en heading_path
Runtime‑afhankelijkheden SDK plus netwerktoegang Eén Maven‑artifact

Het samengestelde effect is een index waar het compliance‑team goedkeuring voor kan geven en een assistent waarvan de antwoorden wijzen naar een echt document en een sectie.

Andere Verzekeringsscenario’s Waar GroupDocs.Parser Past

Zodra de ingestielaag bestaat, dient dezelfde code andere document‑intensieve taken:

  • Zoeken in polisvoorwaarden: Word‑polisdocumenten gesplitst op clausule‑koppen, zodat antwoorden de exacte sectie citeren.
  • Broker‑inzendingen: ZIP‑uploads met gemengde PDF’s en spreadsheets, lid voor lid geïndexeerd, elk gekoppeld aan de inzending.
  • Klachtenafhandeling: klant‑e‑mails en hun bijlagen samen geïndexeerd, waarbij de e‑mail de ouder is van elk bijlage‑fragment.

Aan de Slag in Jouw Claims‑Omgeving

Clone het voorbeeld, voer het uit tegen een paar van je eigen geëxporteerde e‑mails, en open output/chunks.jsonl om de kop‑paden en ouder‑ids te controleren voordat je het in je embed‑job integreert. De stap‑voor‑stap‑architectuurnotities staan in de documentatie‑use‑case voor deze pijplijn, en de Markdown‑output‑referentie geeft weer welke opmaak de modus behoudt.

Resources