💡 Pełny działający przykład dostępny na GitHub: spring-ai-document-reader-using-groupdocs-parser-java
Stara metoda: Załączniki, które nigdy nie trafiają do indeksu
Spring AI DocumentReader jest pierwszym etapem potoku ETL Spring AI: zamienia plik w obiekty Document, które konsumuje splitter i magazyn wektorów, więc to, co zostanie odrzucone, znika na dobre. Wyobraź sobie asystenta, o którego poprosił Twój zespół wsparcia. Eksportujesz wspólną skrzynkę pocztową, indeksujesz treść wiadomości z plików .eml, dzielisz, osadzisz i wysyłasz. Pojawia się pierwsze prawdziwe pytanie, a odpowiedź cytuje e‑mail, którego cała treść brzmi „Proszę zobaczyć załączoną politykę zwrotów”. Sama polityka, załącznik PDF, nigdy nie została odczytana.
Nic w dół strumienia nie może to naprawić. TokenTextSplitter dzieli jedyne zdanie, które otrzymał, a magazyn wektorów zwraca je jako najbliższe dopasowanie do „jaki jest nasz okres zwrotu?”. Każdy komponent wykonał swoją pracę. Luka znajduje się na samym początku potoku, w decyzji, co liczy się jako dokument. Eksport skrzynki pocztowej to drzewo: wiadomości, załączniki i czasem archiwa wewnątrz załączników. Czytnik, który widzi tylko poziom najwyższy, indeksuje jedynie opakowanie.
Moja pierwsza wersja robiła dokładnie to. Naprawiłem to w oczywisty sposób: rozpakowałem pakiety wydania w jednym pomocniku, wyciągnąłem załączniki z wiadomości w drugim, przeglądałem PDF‑y trzecim i wymyśliłem nazwy metadanych w każdym. Działało, dopóki nie pojawił się czwarty format i pomocnicy nie zgadzali się, jak nazwać stronę.
Rzeczywisty koszt: w projekcie przykładowym, 2 z 4 plików wejściowych, e‑mail i ZIP, zawierają tekst, którego odczyt tylko z treści nigdy nie indeksuje. W korpusie wsparcia ten stosunek decyduje, czy asystent jest użyteczny.
Lepsze rozwiązanie: Jeden czytnik dla każdego wejścia
GroupDocs.Parser for Java odczytuje ponad 65 formatów plików w procesie. Wykrywa format z zawartości strumienia, zwraca tekst per strona i wymienia załączniki e‑maila lub wpisy ZIP‑a poprzez jedno API kontenera. Owiń te wywołania w interfejs DocumentReader Spring AI, a reszta potoku nie odróżni różnicy, oprócz tego, że Dokumenty teraz zawierają numery stron i pochodzenie.
Własne czytniki Spring AI pozostają dobrymi wyborami dla swoich przypadków: [odniesienie do ETL pipeline] dokumentuje TikaDocumentReader na Apache Tika i PagePdfDocumentReader na Apache PDFBox. Punkt jest węższy. Gdy załączniki i elementy archiwów muszą być źródłami pierwszej klasy, czytnik, który przegląda kontenery, oszczędza Ci zoo pomocników.
Zanim zaczniemy, będziesz potrzebować:
- Java 17 lub nowsza, podstawa dla Spring AI 2.x
- GroupDocs.Parser for Java 26.10 (uzyskaj tymczasową licencję) oraz
org.springframework.ai:spring-ai-commons2.0.2 - Wyeksportowany e‑mail z załącznikiem lub cztery przykładowe pliki z repozytorium
Sklonuj przykład i uruchom go jednym poleceniem:
mvn compile exec:java
Co musi zwracać Spring AI DocumentReader dla cytowań RAG?
Potrzebuje List<Document>, w której każdy wpis jest wystarczająco mały, by go cytować, i odpowiednio oznaczony, by go śledzić. W praktyce oznacza to jeden Dokument na stronę z page_number, source nazwą pliku oraz, dla wszystkiego, co przybyło wewnątrz e‑maila lub archiwum, parent_source nazwą kontenera. TokenTextSplitter kopiuje te metadane do każdego fragmentu, więc cytat przetrwa aż do magazynu wektorów.
Nowe podejście: Załączniki jako dokumenty pierwszej klasy
Oto to samo zadanie w jednej klasie, GroupDocsParserDocumentReader. Trzy kroki to obejmują: otwórz zasób, odczytaj strony, a potem przejdź przez kontenery i przekaż wszystko splitterowi.
Krok 1 – Otwórz dowolny zasób jako strumień
Czytnik przyjmuje Spring Resource, więc plik, wpis classpath i upload wszystkie idą tą samą drogą. Parser odczytuje bajty, co oznacza, że blob bez rozszerzenia nadal się parsuje.
// 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 – Czytaj strona po stronie
Tryb stron jest domyślny. Zostawiłbym go włączony dla wszystkiego dłuższego niż jedna‑dwie strony, ponieważ recenzent sprawdzający odpowiedź chce od razu przejść do strony 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
}
}
Kluczowe punkty:
pagePerDocument:truedaje każdemu fragmentowipage_number; konstruktor z trzema argumentami wyłącza to dla krótkich notatek.isTextPage(): chroni formaty bez stron; w takich przypadkach czytnik wywołujegetText()raz i nadal zapisujesource,file_typeorazpage_count.
Krok 3 – Przejdź przez kontenery, a potem podziel
Każdy załącznik lub element archiwum otrzymuje własny Parser i wraca przez tę samą logikę stron, oznaczony swoją ścieżką wewnątrz rodzica.
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);
}
}
Etap splittera to standardowy Spring AI i zachowuje metadane czytnika w każdym fragmencie:
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(300) // tokens per chunk in the sample run
.build();
List<Document> chunks = splitter.apply(documents);
Na 4 przykładowych plikach czytnik zwraca 12 Dokumentów, a splitter tworzy 20 fragmentów. Demo zapisuje je do output/spring-ai-documents.jsonl zamiast vectorStore.add(chunks), więc możesz odczytać source, parent_source, page_number oraz chunk_index splittera w każdej linii przed zbudowaniem prawdziwego indeksu.
Ten plik jest wart pięciu minut Twojego czasu. Przeszukaj go pod kątem parent_source, a zobaczysz dokładnie, które fragmenty pochodziły z e‑maila i ZIP‑a; przeszukaj pod kątem page_number, a zobaczysz, gdzie skończyła się każda strona. Gdy po raz pierwszy uruchomiłem to na rzeczywistym eksporcie skrzynki, te dwa wyszukiwania odpowiedziały na więcej pytań o jakość wyszukiwania niż dzień dopasowywania promptów.
Porównanie: Przed vs. Po
Oto, co się zmieniło między wersją z pomocnikami per format a pojedynczym czytnikiem:
| Przed (odczyt tylko treści + pomocnicy dla każdego formatu) | Po (GroupDocs.Parser DocumentReader) | |
|---|---|---|
| Załączniki e‑mail | zgubione lub wyodrębnione przez oddzielnego pomocnika | odczytywane jako Dokumenty z parent_source |
| Elementy ZIP | rozpakowywane najpierw na dysk | odczytywane w miejscu, source = sample.zip/sample.pdf |
| Cytaty | tylko nazwa pliku | nazwa pliku plus page_number |
| Kod do utrzymania | jeden pomocnik na format | jedna klasa DocumentReader |
| Nieczytelny plik | zależy od pomocnika | zalogowany, pominięty, partia kontynuuje |
Potok po czytniku nie zmienił się wcale; zmieniło się jedynie to, co do niego wpływa.
Przykład z rzeczywistości: Nocny indeks wsparcia
Zespół wsparcia codziennie w nocy eksportuje wspólną skrzynkę pocztową i otrzymuje ZIP z zaktualizowanymi podręcznikami przy każdym wydaniu produktu. Zaplanowane zadanie w ich usłudze Spring Boot wymienia nowe pliki, odczytuje je przy pomocy GroupDocsParserDocumentReader, dzieli i zapisuje do magazynu wektorów za asystentem. Gdy agent pyta o zwroty, odpowiedź teraz cytuje manuals-2026-10.zip/refunds.pdf, strona 4, a filtr na parent_source może ograniczyć wyszukiwanie do najnowszego pakietu wydania.
Kod z trzech kroków mapuje się na to zadanie jeden‑do‑jeden. Krok 1 przyjmuje cokolwiek produkuje nocny eksport, w tym pliki, które utraciły rozszerzenie po drodze. Krok 2 utrzymuje strony podręcznika oddzielnie, więc agent może otworzyć stronę 4 przed odpowiedzią klientowi. Krok 3 czyni ZIP i skrzynkę pocztową widocznymi rodzicami zamiast nieprzezroczystych blobów, a wywołanie splittera pozostaje niezmienione w stosunku do każdego innego potoku Spring AI, który zespół już uruchamia.
Krótkie notatki wewnętrzne nie potrzebują podziału na strony, więc zespół odczytuje je w trybie całego pliku i zachowuje załączniki:
// One Document per note; attachments still become their own Documents
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();
Co jeszcze możesz zrobić z GroupDocs.Parser?
Teraz, gdy załączniki płyną do Spring AI, kilka powiązanych możliwości jest na wyciągnięcie ręki:
- Read from streams of any origin: the załaduj dokument ze strumienia guide covers the
Parser(InputStream)constructor the reader uses. - Work with containers directly: wyodrębnij dane z załączników i archiwów ZIP explains
getContainerandopenParser. - Read PDF pages outside Spring AI: wyodrębnij tekst z dokumentów PDF shows page-level text extraction on its own.
- Handle email directly: wyodrębnij załączniki z e‑maili walks through attachment extraction from email messages without the Spring AI layer.
Wnioski
Naprawa „zobacz załącznik” okazała się leżeć w etapie czytnika. Z GroupDocs.Parser za Spring AI DocumentReader, załączniki, elementy archiwów i poszczególne strony stają się źródłami, które asystent może cytować, a wymiana polega na jednym konstruktorze.
Gotowy, aby wypróbować to na własnym korpusie?
- Uzyskaj tymczasową licencję aby uruchomić bez limitów oceny
- Postępuj zgodnie z przewodnikiem integracji w dokumentacji aby uzyskać wzorce, ustawienia i listę kontrolną produkcji
- Sklonuj przykładowy projekt na GitHub i skieruj go na kopię eksportu swojej skrzynki pocztowej