💡 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-commons2.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:truedává každému úsekupage_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_typeapage_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:
- Čtení ze streamů libovolného původu: průvodce načíst dokument ze streamu popisuje konstruktor
Parser(InputStream), který čtečka používá. - Práce s kontejnery přímo: extrahovat data z příloh a ZIP archivů vysvětluje
getContaineraopenParser. - Čtení PDF stránek mimo Spring AI: extrahovat text z PDF dokumentů ukazuje extrakci textu po stránkách samostatně.
- Zpracování e‑mailu přímo: extrahovat přílohy z e‑mailů prochází extrakci příloh z e‑mailových zpráv bez vrstvy Spring AI.
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?
- Získat dočasnou licenci pro běh bez omezení hodnocení
- Postupujte podle průvodce integrací v dokumentaci pro vzory, nastavení a kontrolní seznam pro produkci
- Klonejte ukázkový projekt na GitHubu a nasměrujte ho na kopii vašeho exportu poštovní schránky