💡 مثال كامل يعمل متاح على GitHub: spring-ai-document-reader-using-groupdocs-parser-java
الطريقة القديمة: المرفقات التي لا تصل أبداً إلى الفهرس
قارئ Spring AI DocumentReader هو المرحلة الأولى من خط أنابيب Spring AI ETL: فهو يحول الملف إلى كائنات Document التي يستهلكها القاطع ومتجر المتجهات، لذا أي شيء يتركه يختفي إلى الأبد. تخيّل المساعد الذي طلبه فريق الدعم لديك. تقوم بتصدير صندوق البريد المشترك، تفهرس نصوص رسائل ملفات .eml، تقسم، تُضمّن، وتُرسل. يأتي السؤال الحقيقي الأول، وتستشهد الإجابة برسالة بريد إلكتروني يكون نصها بالكامل “يرجى الاطلاع على سياسة الاسترداد المرفقة.” سياسة الاسترداد نفسها، مرفق PDF، لم تُقرأ أبداً.
لا شيء في المراحل اللاحقة يمكنه إصلاح ذلك. يقوم TokenTextSplitter بتقسيم الجملة الوحيدة التي أعطيته إياها، ويعيد متجر المتجهات النتيجة كأقرب مطابقة لـ “ما هي نافذة الاسترداد لدينا؟”. كل مكوّن قام بعمله. الفجوة تكمن في بداية خط الأنابيب، في القرار حول ما يُعد وثيقة. تصدير صندوق البريد هو شجرة: رسائل، مرفقات، وأحياناً أرشيفات داخل المرفقات. القارئ الذي يرى المستوى العلوي فقط يفهرس الغلاف فقط.
نسختي الأولى فعلت ذلك بالضبط. أصلحتها بالطريقة الواضحة: فك ضغط حزم الإصدار في مساعد واحد، استخراج المرفقات من الرسائل في آخر، تصفح صفحات PDF في ثالث، واختراع أسماء بيانات وصفية في كل منها. عمل ذلك، حتى وصل التنسيق الرابع واختلف المساعدون حول ما يُسمى الصفحة.
التكلفة الحقيقية: في مشروع العينة، 2 من 4 ملفات إدخال، البريد الإلكتروني وملف ZIP، يحملان نصاً لا يقوم القراءة التي تقتصر على النص الأساسي بفهرسته أبداً. في مجموعة دعم، هذه النسبة تقرر ما إذا كان المساعد مفيداً.
طريقة أفضل: قارئ واحد لكل مدخل
GroupDocs.Parser for Java يقرأ أكثر من 65 تنسيق ملفًا داخل العملية. يكتشف التنسيق من محتوى التيار، يُعيد النص لكل صفحة، ويُدرج مرفقات البريد الإلكتروني أو مداخل ZIP عبر واجهة برمجة حاوية واحدة. غلف هذه الاستدعاءات بواجهة DocumentReader الخاصة بـ Spring AI ولا يمكن لبقية خط الأنابيب تمييز الفرق، باستثناء أن المستندات الآن تحمل أرقام الصفحات والنسب.
ما زالت قراءات 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 هذه البيانات الوصفية إلى كل جزء، لذا يبقى الاستشهاد حتى يصل إلى متجر المتجهات.
الطريقة الجديدة: المرفقات كوثائق من الدرجة الأولى
إليك نفس المهمة باستخدام فئة واحدة، GroupDocsParserDocumentReader. تغطيها ثلاث خطوات: فتح المورد، قراءة الصفحات، ثم متابعة الحاويات وتسليم كل شيء إلى القاطع.
الخطوة 1 - فتح أي مورد كتيار
القارئ يقبل Resource من Spring، لذا فإن الملف، أو مدخل مسار الفئة، أو التحميل جميعًا يتبعون نفس المسار. يقرأ Parser البايتات، مما يعني أن الكتلة بدون امتداد لا تزال تُحلل.
// 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);
}
}
مرحلة القاطع هي Spring AI القياسية، وتحتفظ ببيانات القارئ الوصفية على كل جزء:
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(300) // tokens per chunk in the sample run
.build();
List<Document> chunks = splitter.apply(documents);
على الملفات الأربعة النموذجية يُعيد القارئ 12 مستندًا ويُنتج القاطع 20 جزءًا. يكتب العرض التجريبي هذه إلى output/spring-ai-documents.jsonl بدلاً من vectorStore.add(chunks), بحيث يمكنك قراءة source، parent_source، page_number ومؤشر القاطع chunk_index على كل سطر قبل بناء فهرس حقيقي.
هذا الملف يستحق خمس دقائق من وقتك. ابحث فيه عن parent_source وسترى بالضبط أي أجزاء جاءت من داخل البريد الإلكتروني وZIP؛ ابحث عن page_number وسترى أين سقط كل حد صفحة. عندما شغّلتها لأول مرة على تصدير صندوق بريد حقيقي، أجابت هاتان البحثتان عن أسئلة أكثر حول جودة الاسترجاع مما فعلت يومًا كاملًا من تعديل المطالبات.
مقارنة جنبًا إلى جنب: قبل vs. بعد
إليك ما تغير بين نسخة المساعد لكل تنسيق والقارئ الواحد:
| قبل (قراءة النص فقط بالإضافة إلى مساعدات لكل تنسيق) | بعد (GroupDocs.Parser DocumentReader) | |
|---|---|---|
| مرفقات البريد الإلكتروني | مفقودة، أو تم استخراجها بواسطة مساعد منفصل | تُقرأ كوثائق مع parent_source |
| عناصر ZIP | يتم فك ضغطها إلى القرص أولاً | تُقرأ في المكان، source = sample.zip/sample.pdf |
| التوثيقات | اسم الملف فقط | اسم الملف بالإضافة إلى page_number |
| الكود المطلوب صيانته | مساعد واحد لكل تنسيق | فئة DocumentReader واحدة |
| ملف غير قابل للقراءة | يعتمد على المساعد | مسجل، تم تخطيه، تستمر الدفعة |
خط الأنابيب بعد القارئ لم يتغير على الإطلاق؛ فقط ما يتدفق إليه هو ما تغير.
مثال واقعي: فهرس دعم ليلي
يقوم فريق الدعم بتصدير صندوق البريد المشترك كل ليلة ويتلقى ملف ZIP يحتوي على كتيبات محدثة مع كل إصدار منتج. مهمة مجدولة في خدمة Spring Boot الخاصة بهم تُدرج الملفات الجديدة، تقرأها بـ GroupDocsParserDocumentReader، تقسمها وتكتبها إلى متجر المتجهات خلف المساعد الوكيل. عندما يسأل الوكيل عن الاستردادات، تستشهد الإجابة الآن بـ manuals-2026-10.zip/refunds.pdf، الصفحة 4، ويمكن لمرشح parent_source تقييد البحث إلى أحدث حزمة إصدار.
الكود من الخطوات الثلاث يطابق تلك المهمة خطوة بخطوة. الخطوة 1 تقبل أي شيء ينتجه التصدير الليلي، بما في ذلك الملفات التي فقد امتدادها في الطريق. الخطوة 2 تحافظ على صفحات الكتيب منفصلة، بحيث يمكن للوكيل فتح الصفحة 4 قبل الرد على العميل. الخطوة 3 تجعل ZIP وصندوق البريد آباء مرئيين بدلاً من كتل غير شفافة، واستدعاء القاطع يبقى كما هو في أي خط أنابيب 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، بعض القدرات ذات الصلة على بعد مكالمة واحدة:
- قراءة من تدفقات من أي مصدر: دليل تحميل المستند من التيار يغطي مُنشئ
Parser(InputStream)الذي يستخدمه القارئ. - العمل مع الحاويات مباشرة: دليل استخراج البيانات من المرفقات وأرشيفات ZIP يشرح
getContainerوopenParser. - قراءة صفحات PDF خارج Spring AI: دليل استخراج النص من مستندات PDF يُظهر استخراج النص على مستوى الصفحة بشكل مستقل.
- معالجة البريد الإلكتروني مباشرة: دليل استخراج المرفقات من رسائل البريد الإلكتروني يمر عبر استخراج المرفقات من رسائل البريد دون طبقة Spring AI.
الخلاصة
اكتشف أن الإصلاح لـ “انظر المرفق” يكمن في مرحلة القارئ. مع GroupDocs.Parser خلف DocumentReader الخاص بـ Spring AI، تصبح المرفقات، وأعضاء الأرشيف، والصفحات الفردية مصادر يمكن للمساعد الاستشهاد بها، وتبديلها يتم ببساطة عبر مُنشئ واحد.
هل أنت مستعد لتجربتها على مجموعة البيانات الخاصة بك؟
- احصل على ترخيص مؤقت لتشغيله دون حدود التقييم
- اتبع دليل التكامل في الوثائق للأنماط والإعدادات وقائمة التحقق للإنتاج
- استنسخ مشروع العينة على GitHub ووجّهّه إلى نسخة من تصدير صندوق بريدك