💡 Ví dụ hoạt động đầy đủ có trên GitHub:
spring-ai-document-reader-using-groupdocs-parser-java
Cách Cũ: Tệp Đính Kèm Không Bao Giờ Được Đánh Chỉ Mục
Một Spring AI DocumentReader là giai đoạn đầu của pipeline ETL Spring AI: nó chuyển một tệp thành các đối tượng Document mà một splitter và một vector store tiêu thụ, vì vậy bất cứ gì nó thả ra sẽ mất mãi mãi. Hãy tưởng tượng trợ lý mà đội hỗ trợ của bạn yêu cầu. Bạn xuất hộp thư chung, đánh chỉ mục nội dung tin nhắn của các tệp .eml, tách, nhúng và gửi đi. Câu hỏi thực sự đầu tiên đến, và câu trả lời trích dẫn một email có toàn bộ nội dung là “Vui lòng xem chính sách hoàn trả đính kèm.” Chính sách đó, một tệp PDF đính kèm, chưa bao giờ được đọc.
Không có gì ở phía hạ nguồn có thể sửa chữa điều đó. TokenTextSplitter chia nhỏ câu duy nhất mà nó nhận được, và vector store trả về nó như là kết quả gần nhất với “thời gian hoàn trả của chúng tôi là bao lâu?”. Mỗi thành phần đã làm công việc của mình. Khoảng trống nằm ngay đầu pipeline, trong quyết định về những gì được tính là tài liệu. Một xuất khẩu hộp thư là một cây: tin nhắn, tệp đính kèm và đôi khi các tệp nén bên trong tệp đính kèm. Một trình đọc chỉ nhìn thấy cấp độ trên cùng sẽ đánh chỉ mục lớp bao bọc.
Phiên bản đầu tiên của tôi đã làm đúng như vậy. Tôi sửa nó theo cách hiển nhiên: giải nén các gói phát hành trong một helper, tách các tệp đính kèm ra khỏi tin nhắn trong một helper khác, duyệt qua các PDF bằng một helper thứ ba, và tự tạo các tên metadata trong mỗi helper. Nó hoạt động, cho đến khi định dạng thứ tư xuất hiện và các helper không đồng ý về cách gọi một trang.
Chi phí thực tế: trong dự án mẫu, 2 trong số 4 tệp đầu vào, email và ZIP, chứa văn bản mà việc chỉ đọc phần thân không bao giờ đánh chỉ mục. Trong một tập hợp hỗ trợ, tỷ lệ này quyết định liệu trợ lý có hữu ích hay không.
Cách Tốt Hơn: Một Trình Đọc cho Mọi Đầu Vào
GroupDocs.Parser cho Java đọc hơn 65 định dạng tệp (xem danh sách định dạng hỗ trợ) trong cùng một tiến trình. Nó phát hiện định dạng từ nội dung của luồng, trả về văn bản theo trang, và liệt kê các tệp đính kèm của một email hoặc các mục của một ZIP thông qua một API container duy nhất. Đóng gói các cuộc gọi này trong giao diện DocumentReader của Spring AI và phần còn lại của pipeline không thể nhận ra sự khác biệt, ngoại trừ việc các Document giờ mang số trang và nguồn gốc.
Các trình đọc riêng của Spring AI vẫn là lựa chọn tốt cho các trường hợp của chúng: tài liệu tham khảo pipeline ETL mô tả TikaDocumentReader dựa trên Apache Tika và PagePdfDocumentReader dựa trên Apache PDFBox. Điểm ở đây hẹp hơn. Khi các tệp đính kèm và các thành viên trong archive phải là nguồn cấp lớp đầu tiên, một trình đọc đi qua các container sẽ tiết kiệm cho bạn việc duy trì nhiều helper.
Trước khi bắt đầu, bạn sẽ cần:
- Java 17 trở lên, là nền tảng cho Spring AI 2.x
- GroupDocs.Parser cho Java 26.10 (lấy giấy phép tạm thời) và
org.springframework.ai:spring-ai-commons2.0.2 - Một email đã xuất khẩu có đính kèm, hoặc bốn tệp mẫu từ kho lưu trữ
Sao chép dự án mẫu và chạy nó bằng một lệnh:
mvn compile exec:java
Spring AI DocumentReader Cần Trả Về Gì Để Trích Dẫn RAG?
Nó cần một List<Document> trong đó mỗi mục đủ nhỏ để trích dẫn và được gắn nhãn đủ để truy vết. Thực tế, điều này có nghĩa là một Document cho mỗi trang với page_number, một source chỉ tên tệp, và, đối với bất kỳ thứ gì xuất hiện bên trong email hoặc archive, một parent_source chỉ tên container. TokenTextSplitter sao chép metadata này vào mỗi chunk, vì vậy trích dẫn tồn tại tới khi được lưu vào vector store.
Cách Mới: Tệp Đính Kèm Như Các Tài Liệu Cấp Đầu
Dưới đây là cùng một công việc nhưng chỉ với một lớp duy nhất, GroupDocsParserDocumentReader. Ba bước bao phủ toàn bộ quy trình: mở tài nguyên, đọc các trang, sau đó duyệt các container và chuyển mọi thứ cho splitter.
Bước 1 - Mở Bất Kỳ Tài Nguyên nào Thành Luồng
Trình đọc nhận một Resource của Spring, vì vậy một tệp, một mục trong classpath và một tệp tải lên đều đi cùng một con đường. Parser đọc byte, có nghĩa là một blob không có phần mở rộng vẫn có thể phân tích.
// 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<>();
}
Bước 2 - Đọc Trang Theo Trang
Chế độ trang là mặc định. Tôi sẽ để nó bật cho bất kỳ tài liệu nào dài hơn một hoặc hai trang, vì người xem câu trả lời muốn nhảy thẳng tới trang 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
}
}
Các điểm chính:
pagePerDocument:truesẽ gán mỗi chunk mộtpage_number; hàm khởi tạo ba tham số sẽ tắt tính năng này cho các ghi chú ngắn.isTextPage(): bảo vệ các định dạng không có trang; với những định dạng này, trình đọc gọigetText()một lần và vẫn ghi lạisource,file_typevàpage_count.
Bước 3 - Duyệt Các Container, Sau Đó Tách
Mỗi tệp đính kèm hoặc thành viên trong archive sẽ có một Parser riêng và quay lại qua cùng một logic trang, được gắn nhãn với đường dẫn bên trong parent.
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);
}
}
Giai đoạn splitter là Spring AI chuẩn, và nó giữ lại metadata của trình đọc trên mỗi chunk:
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(300) // tokens per chunk in the sample run
.build();
List<Document> chunks = splitter.apply(documents);
Với 4 tệp mẫu, trình đọc trả về 12 Document và splitter tạo ra 20 chunk. Bản demo ghi chúng vào output/spring-ai-documents.jsonl thay cho vectorStore.add(chunks), vì vậy bạn có thể đọc source, parent_source, page_number và chunk_index của splitter trên mỗi dòng trước khi xây dựng chỉ mục thực tế.
Tệp này đáng để bạn dành năm phút. Tìm kiếm parent_source và bạn sẽ thấy chính xác chunk nào đến từ bên trong email và ZIP; tìm kiếm page_number và bạn sẽ thấy ranh giới mỗi trang nằm ở đâu. Khi tôi lần đầu chạy nó trên một xuất khẩu hộp thư thực, hai tìm kiếm này trả lời nhiều câu hỏi về chất lượng truy xuất hơn một ngày tinh chỉnh prompt.
So Sánh: Trước và Sau
Đây là những gì đã thay đổi giữa phiên bản dùng helper‑per‑format và trình đọc duy nhất:
| Trước (đọc chỉ phần thân + helper per‑format) | Sau (GroupDocs.Parser DocumentReader) | |
|---|---|---|
| Tệp đính kèm Email | bị mất, hoặc được tách ra bằng một helper riêng | đọc thành Document với parent_source |
| Thành viên ZIP | giải nén ra đĩa trước | đọc ngay tại chỗ, source = sample.zip/sample.pdf |
| Trích dẫn | chỉ tên tệp | tên tệp + page_number |
| Mã cần bảo trì | một helper cho mỗi định dạng | một lớp DocumentReader |
| Tệp không đọc được | phụ thuộc vào helper | được ghi log, bỏ qua, batch tiếp tục |
Pipeline sau trình đọc không thay đổi chút nào; chỉ có những gì chảy vào nó thay đổi.
Ví Dụ Thực Tế: Chỉ Mục Hỗ Trợ Hàng Đêm
Một đội hỗ trợ xuất khẩu hộp thư chung mỗi đêm và nhận một ZIP các tài liệu hướng dẫn cập nhật cùng mỗi lần phát hành sản phẩm. Một công việc được lên lịch trong dịch vụ Spring Boot của họ liệt kê các tệp mới, đọc chúng bằng GroupDocsParserDocumentReader, tách và ghi vào vector store phía sau trợ lý. Khi một trợ lý hỏi về hoàn trả, câu trả lời giờ trích dẫn manuals-2026-10.zip/refunds.pdf, trang 4, và một bộ lọc trên parent_source có thể hạn chế tìm kiếm chỉ trong gói phát hành mới nhất.
Mã từ ba bước được ánh xạ trực tiếp vào công việc đó. Bước 1 chấp nhận bất kỳ đầu ra xuất khẩu đêm nào, kể cả các tệp mất phần mở rộng trên đường đi. Bước 2 giữ các trang hướng dẫn riêng biệt, để trợ lý có thể mở trang 4 trước khi trả lời khách hàng. Bước 3 biến ZIP và hộp thư thành các parent có thể nhìn thấy thay vì các blob mờ, và lời gọi splitter không thay đổi so với bất kỳ pipeline Spring AI nào khác mà đội đã chạy.
Các ghi chú nội bộ ngắn không cần tách trang, vì vậy đội đọc chúng ở chế độ toàn tệp và vẫn giữ các tệp đính kèm:
// One Document per note; attachments still become their own Documents
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();
Bạn Còn Có Thể Làm Gì Với GroupDocs.Parser?
Bây giờ các tệp đính kèm đã chảy vào Spring AI, một vài khả năng liên quan chỉ cách một lời gọi:
- Đọc từ luồng bất kỳ nguồn nào: hướng dẫn tải tài liệu từ luồng mô tả constructor
Parser(InputStream)mà trình đọc sử dụng. - Làm việc trực tiếp với container: trích xuất dữ liệu từ tệp đính kèm và archive ZIP giải thích
getContainervàopenParser. - Đọc trang PDF ngoài Spring AI: trích xuất văn bản từ tài liệu PDF trình bày cách trích xuất văn bản theo trang một cách độc lập.
- Xử lý email trực tiếp: trích xuất tệp đính kèm từ email hướng dẫn cách lấy tệp đính kèm từ tin nhắn email mà không cần lớp Spring AI.
Kết Luận
Giải pháp cho “see attached” thực chất nằm ở giai đoạn trình đọc. Với GroupDocs.Parser phía sau DocumentReader của Spring AI, các tệp đính kèm, thành viên archive và các trang riêng lẻ trở thành nguồn mà trợ lý có thể trích dẫn, và việc thay thế nó chỉ cần một constructor.
Sẵn sàng thử trên tập dữ liệu của riêng bạn?
- Lấy giấy phép tạm thời để chạy mà không bị giới hạn đánh giá
- Tham khảo hướng dẫn tích hợp trong tài liệu để biết các mẫu, cài đặt và danh sách kiểm tra sản xuất
- Sao chép dự án mẫu trên GitHub và trỏ nó tới một bản sao của xuất khẩu hộp thư của bạn