💡 مثال كامل يعمل متاح على GitHub:
document-to-markdown-rag-chunks-java
مشكلة مكتب المطالبات: لماذا فشل استيعاب RAG مع الملفات الحقيقية
خط أنابيب استيعاب RAG داخل المؤسسة هو طبقة تحليل مستندات تحول الملفات التي يمتلكها الفريق بالفعل إلى نص قابل للبحث والاقتباس دون إرسالها إلى طرف ثالث. تُظهر عمليات التأمين لماذا هذا مهم. يطرح معالج المطالبات سؤالًا على المساعد الداخلي: “ماذا قال المُعدل عن أضرار المياه في هذه الوثيقة؟"، وتكمن الإجابة في بريد إلكتروني من مارس الماضي، داخل تقرير PDF مرفق به، تحت عنوان يُسمى “Findings”. لا يمكن للمساعد العثور عليها إلا إذا قرأ الاستيعاب المرفق، وحافظ على العنوان، وتذكر أي بريد إلكتروني جاء معه التقرير.
معظم وظائف الاستيعاب تخطئ أحد هذه الأخطاء الثلاثة. مستخرج النص العادي يُعيد نص البريد ويتجاهل المرفق. مقسم ثابت بـ 512 رمزًا يقطع التقرير في منتصف “Findings”. ومحلل مستضاف يحل كلا المشكلتين، لكن فقط بعد رفع ملف المطالبة، بما يحتويه من تفاصيل شخصية وطبية، إلى خوادم شخص آخر وتحصيل رسوم لكل صفحة.
ما يحتاجه الفريق هو شيء محدد وملموس: قراءة ملفات Word وPDF وEML وZIP داخل خدمة Java التي تملك البيانات بالفعل، الحفاظ على هيكلها كـ Markdown، التقسيم عند عناوين المؤلف، وإرفاق المصدر والوالد بكل جزء.
لمحة عن السوق: الأدلة لعام 2026 حول “تحليل المستندات لـ RAG” التي راجعناها تقارن محللات Python مفتوحة المصدر بواجهات برمجة تطبيقات سحابية تُحاسب حسب الصفحة. مكتبة Java تعمل على عتادك الخاص غائبة إلى حد كبير عن تلك المناقشة.
لماذا تحتاج خطوط أنابيب RAG إلى Markdown بدلاً من النص العادي؟
يحافظ Markdown على الأجزاء في المستند التي تخبر المسترجع أين تنتهي فكرة وأين تبدأ التالية. تصبح العناوين أسطرًا تبدأ بـ #، وتبقى الجداول كصفوف وأعمدة، وتحتفظ القوائم بعناصرها. يمكن لمقسم القطع التقسيم عند تلك العناوين، ويقرأ نموذج اللغة الكبيرة (LLM) القطعة كما يرى المخطط نفسه الذي يراه الإنسان. النص العادي يزيل هذا المخطط، ويترك الحجم كالإشارة الوحيدة للتقسيم.
لماذا لا تعمل الحلول العامة هنا
عادةً ما تحاول الفرق أحد ثلاثة أشياء قبل بناء طبقة استيعاب مناسبة:
- استخراج النص العادي مع مقسم الرموز: سريع الإعداد، لكن القطع تمزج الأقسام وتفقد العنوان الذي قد تستشهد به.
- واجهة برمجة تطبيقات تحليل سحابية: بنية جيدة، لكن الملفات تغادر الشبكة وتزداد الفاتورة مع كل صفحة تُعاد فهرستها.
- محلل مُصمم داخليًا لكل تنسيق: مكتبة واحدة لـ DOCX، أخرى لـ PDF، محلل MIME للبريد الإلكتروني و
java.util.zipللأرشيفات، كلٌ له حالات حافة خاصة به.
يغطي GroupDocs.Parser for Java جميع الصيغ الأربعة خلف فئة Parser واحدة ويعمل داخل العملية.
الحل: GroupDocs.Parser في سير عمل استيعاب المطالبات
يقرأ GroupDocs.Parser for Java مستندًا ويمكنه إرجاع نصه كـ Markdown عبر getFormattedText. تُعرض رسائل البريد الإلكتروني والأرشيفات كحاويات تُفتح عناصرها ككائنات Parser جديدة، لذا يُعالج PDF المرفق ببريد إلكتروني تمامًا كما يُعالج PDF موجودًا على القرص. يلف المشروع النموذجي ذلك في ست طرق ثابتة ويكتب النتيجة كخطوط JSON، يمكن لأي مهمة تضمين أو مخزن متجهات تحميلها. لا يُستخدم أي إطار عمل RAG؛ تنسيق الإخراج هو نقطة التكامل.
للمتابعة تحتاج إلى:
- JDK 11 أو أحدث للملف
pom.xmlالمرفق (الكود نفسه يلتزم بميزات Java 8) - GroupDocs.Parser for 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 حرف تُقطع عند فواصل الفقرات، لذا يصبح قسم “Findings” الطويل عدة قطع تشترك في مسار عنوان واحد. في ملف Word المرفق ينتج مسارات مثل Heading 1 > Heading 1.2 > Heading 1.2.1.
الخطوة 4 - متابعة المرفقات وكتابة خطوط JSON
هذه هي الخطوة التي يهتم بها مكتب المطالبات أكثر. 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، ووالدها هو email-with-attachment.eml. هذا الزوج هو ما يسمح للمساعد أن يقول “من التقرير المرفق بالبريد الإلكتروني بتاريخ 14 مارس” بدلًا من “من مكان ما في الأرشيف”.
سير العمل الحقيقي: إعادة فهرسة المطالبات أسبوعيًا
تخيل فريق عمليات مطالبات يصدر صناديق بريد المطالبات المغلقة للأسبوع كل جمعة كملفات EML وحزم ZIP للوثائق الداعمة. عامل Java موجود بالفعل ينقل تلك الصادرات إلى مخزن المستندات. إضافة الاستيعاب يعني استدعاء chunkDocument لكل ملف وإلحاق ناتج خطوط JSON إلى طابور التضمين.
الخطوة 4 هي ما يحافظ على تقرير احتياطي مرفق بالبريد الذي سلمها. الخطوة 3 تعني أنه عندما يقتبس المساعد “Findings”، يذكر الاستشهاد اسم الملف والعنوان بدلًا من رقم قطعة مجرد. مبدئي لهذا النوع من الفهارس: إذا لم تستطع القطعة أن تُستشهد بملف وعنوان، فلا ينبغي أن تكون في الفهرس أصلاً.
تأثير الأعمال: ما الذي يغيّره هذا
| الجانب | الإعداد النموذجي قبل | مع خط أنابيب GroupDocs.Parser |
|---|---|---|
| أين يتم تحليل الملفات | نقطة نهاية طرف ثالث | داخل JVM الخاص بك |
| نموذج تكلفة التحليل | حسب الصفحة، في كل إعادة فهرسة | لا رسوم سحابية لكل صفحة |
| حدود القطع | نوافذ رمزية ثابتة | عناوين المؤلف، حد 1500 حرف |
| مرفقات البريد الإلكتروني | غالبًا ما يتم تخطيها أو تسويتها | تحليلها كمستندات فرعية |
| بيانات الاستشهاد | اسم الملف كحد أقصى | source، parent و heading_path |
| اعتمادات وقت التشغيل | SDK بالإضافة إلى الوصول إلى الشبكة | قطعة Maven واحدة |
التأثير المركب هو فهرس يمكن لفريق الامتثال الموافقة عليه ومساعد تُظهر إجاباته مستندًا حقيقيًا وقسمًا.
سيناريوهات تأمين أخرى حيث يناسب GroupDocs.Parser
بمجرد وجود طبقة الاستيعاب، يخدم نفس الكود أعمالًا أخرى كثيفة المستندات:
- بحث في نصوص السياسات: تُقسم مستندات Word للسياسات حسب عناوين الفقرات، لذا تُقتبس الإجابات القسم الدقيق.
- تقديمات السماسرة: تحميلات ZIP تحتوي على PDFs وجداول مختلطة تُفهرس عضوًا بعض، كلٌ مرتبط بالتقديم.
- معالجة الشكاوى: تُفهرس رسائل العملاء ومرفقاتها معًا، مع البريد كوالد لكل قطعة مرفق.
البدء في بيئة المطالبات الخاصة بك
استنسخ العينة، شغّلها على بعض رسائل البريد الإلكتروني التي صدّرتها، وافتح output/chunks.jsonl لتفحص مسارات العناوين ومعرفات الوالد قبل ربطها بمهمة التضمين. ملاحظات الهندسة المعمارية خطوة بخطوة موجودة في حالة الاستخدام للوثائق لهذا الخط الأنابيب، ومرجع إخراج Markdown يوضح أي تنسيقات يحافظ عليها الوضع.