💡 Contoh lengkap yang berfungsi tersedia di GitHub: spring-ai-document-reader-using-groupdocs-parser-java

Cara Lama: Lampiran yang Tidak Pernah Mencapai Indeks

Spring AI DocumentReader adalah tahap pertama dari pipeline ETL Spring AI: ia mengubah file menjadi objek Document yang dikonsumsi oleh splitter dan vector store, sehingga apa pun yang dilepaskannya hilang selamanya. Bayangkan asisten yang diminta tim dukungan Anda. Anda mengekspor mailbox bersama, mengindeks isi pesan file .eml, memecah, menyematkan, dan mengirim. Pertanyaan nyata pertama muncul, dan jawabannya mengutip email yang seluruh isinya adalah “Please see the attached refund policy.” Kebijakan itu sendiri, lampiran PDF, tidak pernah dibaca.

Tidak ada yang di hilir yang dapat memperbaikinya. TokenTextSplitter memotong satu kalimat yang diberikan, dan vector store mengembalikannya sebagai kecocokan terdekat untuk “what is our refund window?”. Setiap komponen melakukan tugasnya. Celah berada di awal pipeline, pada keputusan apa yang dihitung sebagai dokumen. Ekspor mailbox adalah pohon: pesan, lampiran, dan kadang arsip di dalam lampiran. Pembaca yang hanya melihat level atas mengindeks kertas pembungkus.

Versi pertama saya melakukan hal ini persis. Saya memperbaikinya dengan cara yang jelas: mengekstrak bundel rilis dalam satu helper, menarik lampiran dari pesan dalam helper lain, menelusuri PDF halaman demi halaman dengan yang ketiga, dan membuat nama metadata di masing‑masing. Itu berhasil, sampai format keempat muncul dan helper tidak sepakat tentang apa yang disebut halaman.

Biaya sebenarnya: dalam proyek contoh, 2 dari 4 file masukan, email dan ZIP, membawa teks yang pembacaan hanya isi badan tidak pernah mengindeks. Dalam korpus dukungan rasio ini menentukan apakah asisten berguna.

Cara Lebih Baik: Satu Pembaca untuk Setiap Input

GroupDocs.Parser for Java membaca lebih dari 65 format file secara in‑process. Ia mendeteksi format dari konten stream, mengembalikan teks per halaman, dan mencantumkan lampiran email atau entri ZIP melalui satu API kontainer. Bungkus panggilan itu dalam antarmuka DocumentReader Spring AI dan sisanya tidak dapat membedakan, kecuali Document kini membawa nomor halaman dan garis keturunan.

Pembaca bawaan Spring AI tetap pilihan yang baik untuk kasus mereka: referensi pipeline ETL mendokumentasikan TikaDocumentReader pada Apache Tika dan PagePdfDocumentReader pada Apache PDFBox. Intinya di sini lebih sempit. Ketika lampiran dan anggota arsip harus menjadi sumber kelas pertama, pembaca yang menelusuri kontainer menghemat Anda kebun helper.

Sebelum kita mulai, Anda memerlukan:

  • Java 17 atau lebih baru, baseline untuk Spring AI 2.x
  • GroupDocs.Parser for Java 26.10 (dapatkan lisensi sementara) dan org.springframework.ai:spring-ai-commons 2.0.2
  • Email yang diekspor dengan lampiran, atau empat file contoh dari repositori

Klon repositori contoh dan jalankan dengan satu perintah:

mvn compile exec:java

Apa yang Perlu Spring AI DocumentReader Kembalikan untuk Sitasi RAG?

Ia memerlukan List<Document> di mana setiap entri cukup kecil untuk disitasi dan diberi label cukup jelas untuk dilacak. Praktiknya berarti satu Document per halaman dengan page_number, source yang menamai file, dan, untuk apa pun yang datang di dalam email atau arsip, parent_source yang menamai kontainer. TokenTextSplitter menyalin metadata ini ke setiap potongan, sehingga sitasi bertahan hingga ke vector store.

Cara Baru: Lampiran sebagai Dokumen Kelas Pertama

Berikut pekerjaan yang sama dengan satu kelas, GroupDocsParserDocumentReader. Tiga langkah mencakupnya: membuka sumber daya, membaca halaman, lalu mengikuti kontainer dan menyerahkan semuanya ke splitter.

Langkah 1 - Buka Sumber Daya Apa Pun sebagai Stream

Pembaca menerima Spring Resource, sehingga file, entri classpath, dan unggahan semuanya menggunakan jalur yang sama. Parser membaca byte, yang berarti blob tanpa ekstensi tetap dapat diparse.

// 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<>();
}

Langkah 2 - Baca Halaman per Halaman

Mode halaman adalah default. Saya akan membiarkannya aktif untuk apa pun yang lebih panjang dari satu atau dua halaman, karena peninjau yang memeriksa jawaban ingin langsung melompat ke halaman 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
    }
}

Poin penting:

  • pagePerDocument: true memberikan setiap potongan page_number; konstruktor tiga argumen mematikannya untuk catatan singkat.
  • isTextPage(): melindungi format tanpa halaman; untuk format tersebut pembaca memanggil getText() sekali dan tetap mencatat source, file_type, dan page_count.

Langkah 3 - Ikuti Kontainer, Lalu Bagi

Setiap lampiran atau anggota arsip mendapatkan Parser‑nya masing‑masing dan kembali melalui logika halaman yang sama, diberi label dengan jalurnya di dalam induk.

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

Tahap splitter adalah Spring AI standar, dan ia mempertahankan metadata pembaca pada setiap potongan:

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

Pada 4 file contoh, pembaca mengembalikan 12 Document dan splitter menghasilkan 20 potongan. Demo menuliskannya ke output/spring-ai-documents.jsonl menggantikan vectorStore.add(chunks), sehingga Anda dapat membaca source, parent_source, page_number dan chunk_index splitter pada setiap baris sebelum membangun indeks nyata.

File itu layak lima menit waktu Anda. Cari parent_source dan Anda akan melihat tepat potongan mana yang berasal dari dalam email dan ZIP; cari page_number dan Anda akan melihat di mana batas halaman masing‑masing berada. Saat pertama kali saya menjalankannya pada ekspor mailbox nyata, dua pencarian itu menjawab lebih banyak pertanyaan tentang kualitas pengambilan daripada sehari mengutak‑utak prompt.

Perbandingan: Sebelum vs. Sesudah

Berikut apa yang berubah antara versi helper‑per‑format saya dan pembaca tunggal:

Sebelum (baca hanya isi badan plus pembantu per format) Sesudah (GroupDocs.Parser DocumentReader)
Email attachments hilang, atau diekstrak oleh pembantu terpisah dibaca sebagai Document dengan parent_source
ZIP members diekstrak ke disk terlebih dahulu dibaca di tempat, source = sample.zip/sample.pdf
Citations hanya nama file nama file plus page_number
Code to maintain satu pembantu per format satu kelas DocumentReader
Unreadable file tergantung pada pembantu dicatat, dilewati, batch berlanjut

Pipeline setelah pembaca tidak berubah sama sekali; hanya apa yang mengalir ke dalamnya yang berubah.

Contoh Dunia Nyata: Indeks Dukungan Harian

Tim dukungan mengekspor mailbox bersama setiap malam dan menerima ZIP manual yang diperbarui setiap rilis produk. Job terjadwal di layanan Spring Boot mereka mencantumkan file baru, membacanya dengan GroupDocsParserDocumentReader, memecah, dan menulis ke vector store di belakang asisten agen. Ketika agen menanyakan tentang pengembalian dana, jawabannya kini mengutip manuals-2026-10.zip/refunds.pdf, halaman 4, dan filter pada parent_source dapat membatasi pencarian ke bundel rilis terbaru.

Kode dari tiga langkah tersebut dipetakan satu‑ke‑satu ke job itu. Langkah 1 menerima apa pun yang dihasilkan ekspor malam, termasuk file yang ekstensi­nya hilang di tengah jalan. Langkah 2 menjaga halaman manual terpisah, sehingga agen dapat membuka halaman 4 sebelum membalas pelanggan. Langkah 3 menjadikan ZIP dan mailbox orang tua yang terlihat alih‑alih blob tak transparan, dan pemanggilan splitter tidak berubah dari pipeline Spring AI lain yang sudah dijalankan tim.

Catatan internal singkat tidak memerlukan pemisahan halaman, jadi tim membacanya dalam mode seluruh‑file dan tetap menyertakan lampiran:

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

Apa Lagi yang Bisa Anda Lakukan dengan GroupDocs.Parser?

Sekarang lampiran mengalir ke Spring AI, beberapa kemampuan terkait hanya satu panggilan saja:

Kesimpulan

Perbaikan untuk “see attached” ternyata berada di tahap pembaca. Dengan GroupDocs.Parser di belakang DocumentReader Spring AI, lampiran, anggota arsip, dan halaman individual menjadi sumber yang dapat disitasi asisten, dan menggantinya cukup dengan satu konstruktor.

Siap mencobanya pada korpus Anda sendiri?

Sumber Daya Tambahan