💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
מסמך-ל-Markdown-RAG-חלקים-Java

בעיית דלפק התביעות: למה קבלת RAG מתקלקלת בקבצים אמיתיים

צינור קבלת RAG במקומות פנימיים הוא שכבת ניתוח מסמכים שהופכת את הקבצים שכבר יש לצוות לטקסט שניתן לחיפוש ולציטוט ללא שליחה לצד שלישי. תפעול ביטוחי מראה מדוע זה חשוב. מטפל בתביעות שואל את העוזר הפנימי “מה אמר המעריך לגבי נזקי מים בפוליסה הזו?”, והתשובה נמצאת במייל משמר מרץ האחרון, בתוך דוח PDF מצורף, תחת כותרת בשם “Findings”. העוזר יכול למצוא זאת רק אם הקבלה קראה את הקובץ המצורף, שמרה על הכותרת וזכרה באיזה מייל הדוח הגיע.

רוב משימות הקבלה טועות באחת משלוש השגיאות. מחלץ טקסט פשוט מחזיר את גוף המייל ומתעלם מהקובץ המצורף. מחלק קבוע של 512‑טוקנים חותך את הדוח באמצע “Findings”. ומפרש מאוחסן ב‑cloud פותר את שני הבעיות, אך רק אחרי שהקובץ עם הפרטים האישיים והרפואיים של התביעה הועלה לשרתים של מישהו אחר וחוייב לפי עמוד.

מה שהצוות צריך הוא משהו צר וקונקרטי: לקרוא קבצי Word, PDF, EML ו‑ZIP בתוך שירות Java שכבר מחזיק בנתונים, לשמור על המבנה שלהם כ‑Markdown, לחלק לפי כותרות המחבר, ולצרף את המקור וההורה לכל חלק.

תמונת שוק: המדריכים של 2026 על “ניתוח מסמכים ל‑RAG” שבדקנו משווים מפרשי Python קוד פתוח עם API‑ים בענן המחויבים לפי עמוד. ספרייה ב‑Java שרצה על החומרה שלך כמעט ולא מוזכרת באותה שיחה.

למה צינורות RAG צריכים Markdown במקום טקסט פשוט?

Markdown שומר על החלקים של המסמך שמסבירים למחזיר מידע היכן רעיון מסתיים והבא מתחיל. כותרות הופכות לשורות שמתחילות ב‑#, טבלאות נשארות כשורות ועמודות, ורשימות שומרות על הפריטים שלהן. מחלק (chunker) יכול לחלק לפי הכותרות, וה‑LLM הקורא את החלק רואה את אותה מתווה שאדם היה רואה. טקסט פשוט זורק את המתווה, ומשאיר את הגודל כסימן היחיד לחילוק.

למה פתרונות כלליים לא עובדים כאן

צוותים בדרך כלל מנסים אחת משלוש האפשרויות לפני שהם בונים שכבת קבלה נכונה:

  • חילוץ טקסט פשוט + מחלק טוקנים: מהיר להקמה, אך החלקים מערבבים קטעים ומאבדים את הכותרת שהיית מצטט.
  • API ניתוח בענן: מבנה טוב, אך הקבצים עוזבים את הרשת והחשבון גדל עם כל עמוד שמוחזר לאינדקס.
  • מפרש פנימי לכל פורמט: ספרייה אחת ל‑DOCX, אחרת ל‑PDF, מפרש MIME למיילים ו‑java.util.zip לארכיונים, שלכל אחד יש מקרים קצה משלו.

GroupDocs.Parser עבור Java מכסה את כל ארבעת הפורמטים מאחורי מחלק Parser אחד ופועל בתהליך.

הפתרון: GroupDocs.Parser בתהליך קבלת תביעות

GroupDocs.Parser עבור Java קורא מסמך ויכול להחזיר את הטקסט שלו כ‑Markdown דרך getFormattedText. מיילים וארכיונים נחשפים כמכולות שהפריטים שלהם נפתחים כמופעי Parser חדשים, כך ש‑PDF מצורף למייל מעובד בדיוק כמו PDF על הדיסק. פרויקט הדוגמה עוטף זאת בשש שיטות סטטיות וכותב את התוצאה כ‑JSON Lines, שכל משימת הטמעה או חנות וקטורים יכולה לטעון. אין מסגרת RAG מעורבת; פורמט הפלט הוא נקודת האינטגרציה.

כדי לעקוב אחרי זה תצטרך:

  • JDK 11 או גרסה מאוחרת יותר עבור pom.xml שנשלח (הקוד עצמו נשאר על תכונות Java 8)
  • GroupDocs.Parser עבור Java 26.10 (קבל רישיון זמני)
  • חבילה של קבצי DOCX, PDF, EML או ZIP

קובץ pom.xml של הפרויקט מצהיר על מאגר https://releases.groupdocs.com/java/repo/ והתלות היחידה, ולכן בנייה היא פקודה אחת:

mvn compile exec:java

מימוש שכבת הקבלה שלב אחר שלב

השלבים למטה עוקבים אחרי קובץ תביעה אחד מתיבת הדואר עד האינדקס. כל שלב ממופה לשיטה אחת או שתיים ב‑RagChunksDemo.java. שמרנו על השיטות כסטטיות פשוטות בכוונה, כך שניתן להרים כל אחת מהן לעובד קיים בלי לאמץ את השאר.

שלב 1 – פתיחת כל קובץ והפצתו

כל קובץ שהועלה מקבל Parser משלו. שם הקובץ הופך למזהה המקור, וקובץ ברמה העליונה אין לו הורה.

// One call per uploaded claim document
String sourceId = new File(path).getName();
try (Parser parser = new Parser(path)) {
    return chunkParser(parser, sourceId, null, 0);  // null parent: top-level file
} catch (UnsupportedDocumentFormatException ex) {
    // An unknown format is logged and skipped, the batch keeps going
    System.out.println("skipped (unsupported format): " + sourceId);
    return new ArrayList<>();
}

chunkParser לאחר מכן מחלץ את הגוף, מחלק אותו, ויורד לקבצים המצורפים כל עוד העומק מתחת ל‑2.

שלב 2 – בקשת Markdown, קבלת טקסט פשוט

שלב החילוץ בודק מה הפורמט תומך במקום לקבוע רשימת הרחבות קבועה.

if (parser.getFeatures().isFormattedText()) {
    // Headings, lists and tables survive as Markdown
    FormattedTextOptions options =
            new FormattedTextOptions(FormattedTextMode.Markdown);
    try (TextReader reader = parser.getFormattedText(options)) {
        if (reader != null) {
            return new ExtractedText("markdown", reader.readToEnd());
        }
    } catch (IOException ex) {
        throw new RuntimeException(ex);
    }
}
try (TextReader reader = parser.getText()) {
    if (reader != null) {  // fallback for formats without formatted text
        return new ExtractedText("plain", reader.readToEnd());
    }
} catch (IOException ex) {
    throw new RuntimeException(ex);
}
return new ExtractedText("none", "");  // e.g. a photo with no OCR

למה ההגדרות האלה חשובות לנתוני תביעות:

  • FormattedTextMode.Markdown: כותרת “Findings” של המעריך מגיעה למחלק כ‑## Findings, ולכן היא יכולה לשמש נקודת חלוקה וציטוט.
  • isFormattedText(): בריצה לדוגמה ה‑DOCX והמייל חוזרים כ‑Markdown בעוד שה‑PDF חוזר לטקסט פשוט, ושדה format מתעד מה קרה.
  • התוצאה none: תמונות תביעה בתוך ZIP אינן מייצרות חלקים ללא OCR, מה שמאפשר ניטור הוגן ופשוט.

שלב 3 – חלוקה לפי כותרות המעריך

המחלק שומר מחסנית של הכותרת הנוכחית בכל אחד מששת רמות ה‑Markdown. כותרת חדשה סוגרת את הקטע הרץ ומאתחלת את כל מה שמתחת לרמה שלה.

for (String line : markdown.split("\\r?\\n", -1)) {
    Matcher heading = HEADING.matcher(line);   // "#" to "######" + title
    if (heading.matches()) {
        // Close the previous section under the old path
        addSection(chunks, section.toString(), sourceId, parentId, path, format);
        section.setLength(0);
        int level = heading.group(1).length();
        stack[level - 1] = heading.group(2).trim();
        for (int i = level; i < stack.length; i++) {
            stack[i] = null;                     // forget deeper headings
        }
        path = joinPath(stack);                  // "Heading 1 > Heading 1.2"
    }
    section.append(line).append('\n');
}

קטעים ארוכים יותר מ‑1500 תווים נחתכים בנקודות פסקה, כך שמקטע מציאת ארוך הופך למספר חלקים שחולקים את נתיב הכותרת הזהה. בקובץ Word המוגש זה יוצר נתיבים כגון Heading 1 > Heading 1.2 > Heading 1.2.1.

שלב 4 – מעקב אחרי קבצים מצורפים וכתיבת JSON Lines

זהו השלב שהדסק של תביעות מתעניין בו ביותר. getContainer() מציג קבצים מצורפים במייל וכניסות ZIP; כל פריט נפתח כמסמך בן ולורש שלו יורש את מזהה ההורה.

Iterable<ContainerItem> items = parser.getContainer();
if (items == null) {
    return chunks;                    // not an email or archive
}
for (ContainerItem item : items) {
    // "claim-0412.eml/report.pdf" style ids keep the lineage readable
    String childId = sourceId + "/" + item.getFilePath();
    try (Parser child = item.openParser()) {
        chunks.addAll(chunkParser(child, childId, sourceId, depth + 1));
    } catch (UnsupportedDocumentFormatException ex) {
        System.out.println("skipped (unsupported format): " + childId);
    }
}

writeJsonl כותב לאחר מכן שורה אחת ב‑UTF‑8 לכל חלק עם id, source, parent, heading_path, format ו‑text. עם ארבעת הקבצים לדוגמה הריצה מייצרת 22 שורות. החלק מהקובץ המצורף למייל הוא email-with-attachment.eml/attachment.txt#0, וה‑parent שלו הוא email-with-attachment.eml. זוג זה מאפשר לעוזר לומר “מהדוח המצורף למייל של 14 במרץ” במקום “מאיזשהו מקום בארכיון”.

זרימת עבודה אמיתית: אינדוקס תביעות שבועי

דמיינו צוות תפעול תביעות שמייצא את תיבות הדואר של תביעות סגורות של השבוע כל יום שישי כקבצי EML וחבילות ZIP של מסמכים תומכים. עובד Java כבר מעביר את הייצוא לחנות המסמכים. הוספת קבלה משמעותה קריאה ל‑chunkDocument עבור כל קובץ והוספת פלט ה‑JSON Lines לתור ההטמעה.

שלב 4 הוא מה ששומר על דוח רזרבי מצורף למייל שהביא אותו. שלב 3 מבטיח שכאשר העוזר מצטט “Findings”, הציטוט מציין את הקובץ ואת הכותרת במקום מספר חלק אקראי. כלל האצבע שלי למין אינדקס זה: אם חלק אינו ניתן לציטוט חזרה לקובץ ולכותרת, הוא לא צריך להיות באינדקס כלל.

השפעה עסקית: מה זה משנה

היבט תצורה טיפוסית לפני עם צינור GroupDocs.Parser
היכן הקבצים מנותחים נקודת קצה של צד שלישי בתוך ה‑JVM שלך
מודל עלות ניתוח לפי עמוד, בכל אינדוקס מחדש ללא עלות ענן לפי עמוד
גבולות חלקים חלונות טוקנים קבועים כותרות המחבר, מגבלה של 1500 תווים
קבצים מצורפים במייל לעיתים נדחים או משטחים מנותחים כמסמכי בן
נתוני ציטוט שם קובץ לכל היותר source, parent ו‑heading_path
תלויות בזמן ריצה SDK + גישה לרשת ארטיפקט Maven אחד

האפקט המשולב הוא אינדקס שהצוות הרגולטורי יכול לאשר ועוזר שהתגובות שלו מצביעות על מסמך אמיתי וקטע ספציפי.

תרחישים ביטוחיים נוספים שבהם GroupDocs.Parser מתאים

לאחר שהשכבה קיימת, אותו קוד משרת משימות אחרות כבדות מסמכים:

  • חיפוש בנוסח פוליסה: מסמכי Word של פוליסות מחולקים לפי כותרות סעיפים, כך שהתגובות מצטטות את הסעיף המדויק.
  • הגשות ברוקר: העלאות ZIP עם PDF ו‑Spreadsheet משולבים שמאונדקסים פריט‑פריט, כל אחד מקושר להגשה.
  • טיפול בתלונות: מיילים של לקוחות והקבצים המצורפים להם מאונדקסים יחד, כאשר המייל הוא ההורה לכל חלק מצורף.

התחלה בסביבת התביעות שלכם

שכפלו את הדוגמה, הריצו אותה על כמה מהמיילים המיוצאים שלכם, ופתחו את output/chunks.jsonl כדי לבדוק את נתיבי הכותרת ומזהי ההורה לפני שחיברתם אותה למשימת ההטמעה. ההערות על ארכיטקטורת שלב‑בשלב נמצאות בתיעוד מקרה השימוש לצינור זה, והפניה לפלט Markdown מפרטת איזה עיצוב השיטה שומרת.

משאבים