💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
spring-ai-document-reader-using-groupdocs-parser-java
הדרך הישנה: קבצים מצורפים שלעולם לא מגיעים לאינדקס
Spring AI DocumentReader הוא השלב הראשון בצינור ETL של Spring AI: הוא ממיר קובץ לאובייקטי Document שה‑splitter וה‑vector store צורכים, ולכן כל מה שהוא משאיר נעלם לצמיתות. דמיינו את העוזר שהצוות שלכם ביקש. אתם מייצאים את תיבת הדואר המשותפת, מאנדקסים את גופי ההודעות של קבצי .eml, מחלקים, משבצים ושולחים. השאלה הראשונה מגיעה, והתשובה מציינת אימייל שהגוף שלו כולו הוא “Please see the attached refund policy.” המדיניות עצמה, קובץ PDF מצורף, מעולם לא נקראה.
שום דבר במורד הצינור לא יכול לתקן זאת. TokenTextSplitter מחלק את המשפט היחיד שניתן לו, וה‑vector store מחזיר אותו כהתאמה הקרובה ביותר ל‑“what is our refund window?”. כל רכיב עשה את תפקידו. הפער נמצא בתחילת הצינור, בהחלטה מה נחשב למסמך. ייצוא תיבת דואר הוא עץ: הודעות, קבצים מצורפים, ולפעמים ארכיונים בתוך קבצים מצורפים. קורא שרואה רק את הרמה העליונה מאנדקס את העטיפה.
הגרסה הראשונה שלי עשתה בדיוק זאת. תיקנתי זאת בצורה הברורה: חילצתי את חבילות השחרור בקובץ עזר אחד, חילצתי קבצים מצורפים מהודעות בקובץ עזר שני, עברתי על דפי PDF בקובץ עזר שלישי, והמצאתי שמות מטא‑דטה בכל אחד. זה עבד, עד שהפורמט הרביעי הגיע והעוזרים לא הסכימו איך לקרוא לדף.
העלות האמיתית: בפרויקט הדוגמה, 2 מתוך 4 קבצים קלט – האימייל וה‑ZIP – מכילים טקסט שהקריאה רק‑גוף לעולם לא תינדקס. בקורפוס תמיכה, היחס הזה מחליט אם העוזר שימושי.
דרך טובה יותר: קורא אחד לכל קלט
GroupDocs.Parser for Java קורא יותר מ‑65 פורמטים של קבצים בתהליך. הוא מזהה את הפורמט מתוכן ה‑stream, מחזיר טקסט לפי דף, ומציין את הקבצים המצורפים של אימייל או את רשומות ה‑ZIP דרך API של מכולה אחת. עטפו קריאות אלו בממשק DocumentReader של Spring AI והצינור כולו לא יוכל להבחין בשינוי, פרט לכך שה‑Documents כעת נושאים מספרי דפים וירושה.
הקוראים של Spring AI עצמם נשארים בחירות טובות למקרים שלהם: ה‑הפנייה לצינור ETL מתעדת TikaDocumentReader על Apache Tika ו‑PagePdfDocumentReader על Apache PDFBox. הנקודה כאן צרה יותר. כאשר קבצים מצורפים וחברי ארכיון צריכים להיות מקורות ראשוניים, קורא שמטייל במכולות חוסך לכם את חיית העוזרים.
לפני שנתחיל, תזדקקו ל‑:
- Java 17 או גרסה מאוחרת יותר, הבסיס ל‑Spring AI 2.x
- GroupDocs.Parser for Java 26.10 (קבל רישיון זמני) ו‑
org.springframework.ai:spring-ai-commons2.0.2 - אימייל מיוצא עם קובץ מצורף, או ארבעת קבצי הדוגמה מהמאגרים
שכפלו את הדוגמה והפעלו אותה עם פקודה אחת:
mvn compile exec:java
מה צריך Spring AI DocumentReader להחזיר לציטוטי RAG?
הוא צריך List<Document> שבו כל ערך קטן מספיק לציטוט ומתויג היטב למעקב. בפועל משמעות הדבר היא מסמך אחד לכל דף עם page_number, source שמציין את שם הקובץ, ו‑parent_source שמציין את המכולה עבור כל מה שהגיע מתוך אימייל או ארכיון. TokenTextSplitter מעתיק מטא‑דטה זו לכל חלק, כך שהציטוט נשאר עד סוף הדרך אל ה‑vector store.
הדרך החדשה: קבצים מצורפים כמסמכים ראשוניים
הנה אותה משימה עם מחלקה יחידה, GroupDocsParserDocumentReader. שלושה שלבים מכסים זאת: פתיחת המשאב, קריאת דפים, ואז מעקב אחרי המכולות והעברת הכול ל‑splitter.
שלב 1 - פתיחת כל משאב כ‑זרם
הקורא מקבל Resource של Spring, ולכן קובץ, ערך classpath והעלאה כולם עוברים באותו הנתיב. ה‑Parser קורא בתים, מה שאומר ש‑blob ללא סיומת עדיין מתפרש.
// 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<>();
}
שלב 2 - קריאה עמוד אחרי עמוד
מצב דף הוא ברירת המחדל. הייתי משאיר אותו פעיל לכל דבר ארוך מדף או שניים, מכיוון שסוקר שבודק תשובה רוצה לקפוץ ישר לדף 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
}
}
נקודות מפתח:
pagePerDocument:trueנותן לכל חלקpage_number; הבנאי עם שלושה ארגומנטים מכבה זאת עבור פתקים קצרים.isTextPage(): מגן על פורמטים ללא דפים; עבור כאלה הקורא קוראgetText()פעם אחת ועדיין רושםsource,file_typeו‑page_count.
שלב 3 - עקוב אחרי המכולות, ואז פצל
כל קובץ מצורף או חבר ארכיון מקבל Parser משלו וחוזר דרך אותה לוגיקת דפים, מתויג עם הנתיב שלו בתוך ההורה.
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);
}
}
שלב ה‑splitter הוא Spring AI סטנדרטי, והוא משאיר את מטא‑דטה של הקורא על כל חלק:
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(300) // tokens per chunk in the sample run
.build();
List<Document> chunks = splitter.apply(documents);
ב‑4 קבצי הדוגמה הקורא מחזיר 12 Documents וה‑splitter יוצר 20 chunks. ההדגמה כותבת אותם ל‑output/spring-ai-documents.jsonl במקום vectorStore.add(chunks), כך שניתן לקרוא source, parent_source, page_number ואת chunk_index של ה‑splitter בכל שורה לפני בניית אינדקס אמיתי.
הקובץ שווה חמש דקות מזמנכם. חפשו בו parent_source ותראו בדיוק אילו chunks הגיעו מתוך האימייל וה‑ZIP; חפשו page_number ותראו היכן נחתכו גבולות הדפים. כשפעם ראשונה הרצתי זאת על ייצוא תיבת דואר אמיתית, שני החיפושים הללו ענו על יותר שאלות לגבי איכות השאיבה מאשר יום של כוונון פרומפטים.
השוואה: לפני vs. אחרי
הנה מה שהשתנה בין גרסת העוזרים לכל פורמט לגרסה עם קורא יחיד:
| לפני (קריאה רק‑גוף + עוזרים לכל פורמט) | אחרי (GroupDocs.Parser DocumentReader) | |
|---|---|---|
| קבצים מצורפים של אימייל | אבדו, או חולצו על‑ידי עוזר נפרד | נקראו כ‑Documents עם parent_source |
| חברי ZIP | נפרקו לדיסק תחילה | נקראו במקום, source = sample.zip/sample.pdf |
| ציטוטים | שם קובץ בלבד | שם קובץ + page_number |
| קוד לתחזוקה | עוזר אחד לכל פורמט | מחלקת DocumentReader אחת |
| קובץ בלתי קריא | תלוי בעוזר | נרשם, נדלג, האצווה ממשיכה |
הצינור אחרי הקורא לא השתנה כלל; רק מה זורם לתוכו השתנה.
דוגמה מהעולם האמיתי: אינדקס תמיכה לילה‑לילה
צוות תמיכה מייצא את תיבת הדואר המשותפת שלו כל לילה ומקבל ZIP של מדריכים מעודכנים עם כל שחרור מוצר. משימה מתוזמנת בשירות Spring Boot שלהם מציגה קבצים חדשים, קוראת אותם עם GroupDocsParserDocumentReader, מחלקת וכותבת ל‑vector store מאחורי העוזר. כאשר עוזר שואל על החזרים, התשובה כעת מציינת manuals-2026-10.zip/refunds.pdf, דף 4, והמסנן על parent_source יכול להגביל חיפוש לחבילה האחרונה.
הקוד משלושת השלבים ממופה על משימה זו אחד‑לאחד. שלב 1 מקבל כל מה שהייצוא הלילי מייצר, כולל קבצים שהסיומת שלהם אבדה בדרך. שלב 2 משאיר דפי המדריך נפרדים, כך שהעוזר יכול לפתוח דף 4 לפני שמגיב ללקוח. שלב 3 הופך את ה‑ZIP ותיבת הדואר להורים גלויים במקום ל‑blobs אטומים, והקריאה ל‑splitter נשארת ללא שינוי מכל צינור Spring AI אחר שהצוות כבר מריץ.
פתקים פנימיים קצרים אינם זקוקים לפיצול דפים, ולכן הצוות קורא אותם במצב קובץ שלם ומשאיר קבצים מצורפים פעילים:
// One Document per note; attachments still become their own Documents
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();
מה עוד אפשר לעשות עם GroupDocs.Parser?
עכשיו שהקבצים המצורפים זורמים ל‑Spring AI, כמה יכולות קשורות נמצאות במרחק קריאה אחת:
- טעינת מסמך מזרם: המדריך ל‑load document from stream מכסה את הבנאי
Parser(InputStream)שהקורא משתמש בו. - עבודה ישירה עם מכולות: המדריך ל‑extract data from attachments and ZIP archives מסביר
getContainerו‑openParser. - קריאת דפי PDF מחוץ ל‑Spring AI: המדריך ל‑extract text from PDF documents מציג חילוץ טקסט ברמת דף באופן עצמאי.
- טיפול ישיר באימייל: המדריך ל‑extract attachments from emails מראה כיצד לחלץ קבצים מצורפים מהודעות אימייל ללא שכבת Spring AI.
סיכום
התיקון ל‑“see attached” נמצא למעשה בשלב הקורא. עם GroupDocs.Parser מאחורי DocumentReader של Spring AI, קבצים מצורפים, חברי ארכיון ודפים בודדים הופכים למקורות שהעוזר יכול לצטט, והחלפתו היא בנייה אחת.
מוכנים לנסות זאת על הקורפוס שלכם?
- קבל רישיון זמני להפעלה ללא מגבלות הערכה
- עקבו אחרי מדריך האינטגרציה בתיעוד לקבלת תבניות, הגדרות ורשימת בדיקה לייצור
- שכפלו את פרויקט הדוגמה ב‑GitHub והצביעו אותו על עותק של ייצוא תיבת הדואר שלכם