💡 Plně funkční příklad dostupný na GitHubu:
spring-ai-document-reader-using-groupdocs-parser-java

Starý způsob: Přílohy, které nikdy nedosáhnou indexu

Spring AI DocumentReader je první fáze ETL pipeline Spring AI: převádí soubor na objekty Document, které spotřebovávají splitter a vektorové úložiště, takže vše, co odhodí, je navždy ztraceno. Představte si asistenta, o kterém váš podpora tým žádal. Exportujete sdílenou poštovní schránku, indexujete těla zpráv souborů .eml, rozdělíte, vložíte a odešlete. Přijde první skutečná otázka a odpověď cituje e‑mail, jehož celé tělo zní „Please see the attached refund policy.“ Politika samotná, PDF příloha, nikdy nebyla přečtena.

Nic, co je níže v řetězci, to nemůže opravit. TokenTextSplitter rozdělí jedinou větu, kterou dostane, a vektorové úložiště ji vrátí jako nejbližší shodu na „what is our refund window?“. Každá komponenta odvedla svou práci. Mezera je na samém začátku pipeline, v rozhodnutí, co se počítá jako dokument. Export poštovní schránky je strom: zprávy, přílohy a někdy archivy uvnitř příloh. Čtečka, která vidí jen nejvyšší úroveň, indexuje jen obalový papír.

Moje první verze dělala přesně tohle. Opravil jsem to zjevně: rozbalil jsem balíčky v jednom pomocníkovi, vytáhl přílohy ze zpráv v dalším, procházel PDF stránkami ve třetím a v každém vymyslel názvy metadat. Fungovalo, dokud nepřišel čtvrtý formát a pomocníci nesouhlasili, jak nazvat stránku.

Skutečná cena: ve vzorovém projektu 2 ze 4 vstupních souborů, e‑mail a ZIP, obsahují text, který čtení jen těla nikdy neindexuje. V korpusu podpory tento poměr rozhoduje, zda je asistent užitečný.

Lepší způsob: Jedna čtečka pro každý vstup

GroupDocs.Parser pro Java čte 65+ formátů souborů přímo v procesu. Detekuje formát podle obsahu proudu, vrací text po stránkách a vypisuje přílohy e‑mailu nebo položky ZIP archivu pomocí jedné API kontejneru. Zabalíte tyto volání do rozhraní DocumentReader Spring AI a zbytek pipeline nepozná rozdíl, kromě toho, že Dokumenty nyní nesou čísla stránek a původ.

Čtečky Spring AI zůstávají dobrými volbami pro své případy: referenční dokumentace ETL pipeline popisuje TikaDocumentReader na Apache Tika a PagePdfDocumentReader na Apache PDFBox. Zde je úhel užší. Když musí být přílohy a položky archivu první‑třídními zdroji, čtečka, která prochází kontejnery, vám ušetří zoo pomocníků.

Než začneme, budete potřebovat:

  • Java 17 nebo novější, základ pro Spring AI 2.x
  • GroupDocs.Parser pro Java 26.10 (Získat dočasnou licenci) a org.springframework.ai:spring-ai-commons 2.0.2
  • Exportovaný e‑mail s přílohou nebo čtyři vzorové soubory z repozitáře

Klonejte vzor a spusťte jej jedním příkazem:

mvn compile exec:java

Co musí Spring AI DocumentReader vracet pro RAG citace?

Potřebuje List<Document>, kde je každý záznam dostatečně malý na citaci a dostatečně označený pro sledování. V praxi to znamená jeden Dokument na stránku s page_number, source pojmenovávajícím soubor a pro vše, co přišlo uvnitř e‑mailu nebo archivu, parent_source pojmenovávajícím kontejner. TokenTextSplitter kopíruje tato metadata na každý úsek, takže citace přežije až do vektorového úložiště.

Nový způsob: Přílohy jako první‑třídní dokumenty

Zde je stejná úloha s jednou třídou, GroupDocsParserDocumentReader. Tři kroky ji pokrývají: otevřít zdroj, číst stránky, pak projít kontejnery a předat vše splitteru.

Krok 1 – Otevřít libovolný zdroj jako proud

Čtečka přijímá Spring Resource, takže soubor, položku classpath i nahrání používají stejnou cestu. Parser čte bajty, což znamená, že blob bez přípony se stále dokáže parsovat.

// Format detection works on content, so the file name is only a label
try (InputStream stream = resource.getInputStream();
     Parser parser = new Parser(stream)) {
    List<Document> documents = new ArrayList<>(readBody(parser, source, null));
    if (includeAttachments) {
        documents.addAll(readAttachments(parser, source)); // the fix for "see attached"
    }
    return documents;
} catch (UnsupportedDocumentFormatException ex) {
    // Log the unreadable file and return an empty list; the batch keeps going
    System.out.println("skipped (unsupported format): " + source);
    return new ArrayList<>();
}

Krok 2 – Číst stránku po stránce

Režim stránky je výchozí. Nechal bych ho zapnutý pro cokoliv delšího než jedna‑dvě stránky, protože recenzent kontrolující odpověď chce skočit rovnou na stránku 31.

if (pagePerDocument && pageCount > 0 && parser.getFeatures().isTextPage()) {
    for (int page = 0; page < pageCount; page++) {
        String text = readText(parser.getText(page));
        Map<String, Object> metadata =
                metadata(source, parent, fileType, pageCount);
        metadata.put(METADATA_PAGE, page + 1); // people count pages from 1
        addIfNotBlank(documents, text, metadata); // blank pages never get embedded
    }
}

Klíčové body:

  • pagePerDocument: true dává každému úseku page_number; tříargumentový konstruktor to vypíná pro krátké poznámky.
  • isTextPage(): chrání formáty bez stránek; u nich čtečka zavolá getText() jednou a stále zaznamená source, file_type a page_count.

Krok 3 – Projít kontejnery, pak rozdělit

Každá příloha nebo položka archivu získá vlastní Parser a projde stejnou logikou stránek, označenou její cestou uvnitř rodiče.

Iterable<ContainerItem> items = parser.getContainer(); // null for plain files
if (items == null) {
    return documents;
}
for (ContainerItem item : items) {
    String childSource = source + "/" + item.getFilePath(); // "mail.eml/policy.pdf"
    try (Parser child = item.openParser()) {
        documents.addAll(readBody(child, childSource, source)); // parent_source = source
    } catch (UnsupportedDocumentFormatException ex) {
        System.out.println("skipped (unsupported format): " + childSource);
    }
}

Fáze splitteru je standardní Spring AI a zachovává metadata čtečky na každém úseku:

TokenTextSplitter splitter = TokenTextSplitter.builder()
        .withChunkSize(300) // tokens per chunk in the sample run
        .build();
List<Document> chunks = splitter.apply(documents);

Na 4 vzorových souborech čtečka vrátí 12 Dokumentů a splitter vytvoří 20 úseků. Demo je zapíše do output/spring-ai-documents.jsonl místo vectorStore.add(chunks), takže můžete číst source, parent_source, page_number a chunk_index splitteru na každém řádku před vytvořením skutečného indexu.

Ten soubor stojí pět minut vašeho času. Vyhledejte v něm parent_source a uvidíte přesně, které úseky pocházejí zevnitř e‑mailu a ZIP; vyhledejte page_number a uvidíte, kde končila každá stránka. Když jsem to poprvé spustil na reálném exportu poštovní schránky, tyto dva vyhledávání odpověděla na více otázek o kvalitě vyhledávání než den ladění promptů.

Vedle‑vedle: Před vs Po

Zde je, co se změnilo mezi mojí verzí s pomocníky pro každý formát a jednou čtečkou:

Před (čtení pouze těla + pomocníci pro každý formát) Po (GroupDocs.Parser DocumentReader)
Email attachments ztraceny, nebo extrahovány samostatným pomocníkem čteny jako Dokumenty s parent_source
ZIP members rozbaleny na disk nejprve čteny na místě, source = sample.zip/sample.pdf
Citations pouze název souboru název souboru plus page_number
Code to maintain jeden pomocník pro každý formát jedna třída DocumentReader
Unreadable file závisí na pomocníkovi zaznamenáno, přeskočeno, dávka pokračuje

Pipeline po čtečce se vůbec nezměnila; změnil se jen vstup, který do ní proudí.

Příklad ze skutečného světa: Noční index podpory

Podpora tým exportuje svou sdílenou poštovní schránku každou noc a dostává ZIP s aktualizovanými manuály ke každému vydání produktu. Plánovaná úloha v jejich Spring Boot službě vypisuje nové soubory, čte je pomocí GroupDocsParserDocumentReader, rozdělí a zapíše do vektorového úložiště za asistentem. Když asistent požádá o refundace, odpověď nyní cituje manuals-2026-10.zip/refunds.pdf, stránka 4, a filtr na parent_source může omezit vyhledávání na nejnovější balíček.

Kód ze tří kroků mapuje přesně na tuto úlohu. Krok 1 přijímá cokoliv, co noční export vytvoří, včetně souborů, kterým se ztratila přípona. Krok 2 udržuje stránky manuálu oddělené, takže asistent může otevřít stránku 4 před odpovědí zákazníkovi. Krok 3 dělá z ZIP a poštovní schránky viditelné rodiče místo neprůhledných blobů a volání splitteru zůstává nezměněno oproti jakékoli jiné pipeline Spring AI, kterou tým už používá.

Krátké interní poznámky nepotřebují rozdělení na stránky, takže tým čte je v režimu celého souboru a ponechává přílohy:

// One Document per note; attachments still become their own Documents
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();

Co dalšího můžete dělat s GroupDocs.Parser?

Nyní, když přílohy proudí do Spring AI, několik souvisejících schopností je na jeden volání vzdálenost:

Závěr

Oprava pro „see attached“ se ukázala být ve fázi čtečky. S GroupDocs.Parser za DocumentReader Spring AI se přílohy, položky archivu a jednotlivé stránky stávají zdroji, které asistent může citovat, a výměna je jen jedním konstruktorem.

Chcete to vyzkoušet na vlastním korpusu?

Další zdroje