trustgraph/docs/tech-specs/large-document-loading.ar.md
Alex Jenkins 8954fa3ad7 Feat: TrustGraph i18n & Documentation Translation Updates (#781)
Native CLI i18n: The TrustGraph CLI has built-in translation support
that dynamically loads language strings. You can test and use
different languages by simply passing the --lang flag (e.g., --lang
es for Spanish, --lang ru for Russian) or by configuring your
environment's LANG variable.

Automated Docs Translations: This PR introduces autonomously
translated Markdown documentation into several target languages,
including Spanish, Swahili, Portuguese, Turkish, Hindi, Hebrew,
Arabic, Simplified Chinese, and Russian.
2026-04-14 12:08:32 +01:00

42 KiB

layout title parent
default المواصفات الفنية لتحميل المستندات الكبيرة Arabic (Beta)

المواصفات الفنية لتحميل المستندات الكبيرة

Beta Translation: This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.

نظرة عامة

تتناول هذه المواصفات مشكلات قابلية التوسع وتجربة المستخدم عند تحميل المستندات الكبيرة في TrustGraph. تعالج البنية الحالية تحميل المستندات كعملية ذرية واحدة، مما يتسبب في ضغط الذاكرة في نقاط متعددة في المسار ولا يوفر أي ملاحظات أو خيارات استرداد للمستخدمين.

تستهدف هذه التنفيذ حالات الاستخدام التالية:

  1. معالجة ملفات PDF الكبيرة: تحميل ومعالجة ملفات PDF متعددة الميجابايت دون استنفاد الذاكرة
  2. التحميل القابل للاستئناف: السماح للتحميلات المتقطعة بمواصلة من حيث توقفت بدلاً من إعادة البدء
  3. ملاحظات التقدم: تزويد المستخدمين برؤية في الوقت الفعلي لعملية التحميل والمعالجة
  4. معالجة فعالة للذاكرة: معالجة المستندات بطريقة متدفقة دون الاحتفاظ بملفات كاملة في الذاكرة

الأهداف

التحميل التدريجي: دعم تحميل المستندات على شكل أجزاء عبر REST و WebSocket التحويلات القابلة للاستئناف: تمكين الاسترداد من التحميلات المتقطعة رؤية التقدم: توفير ملاحظات حول تقدم التحميل/المعالجة للعملاء كفاءة الذاكرة: القضاء على التخزين المؤقت للمستند بأكمله في جميع أنحاء المسار التوافق مع الإصدارات السابقة: تظل سير العمل الحالية للمستندات الصغيرة دون تغيير المعالجة المتدفقة: تعمل فك ترميز PDF وتقطيع النص على التدفقات

الخلفية

البنية الحالية

يمر تدفق إرسال المستندات عبر المسار التالي:

  1. يرسل العميل المستند عبر REST (POST /api/v1/librarian) أو WebSocket
  2. يتلقى بوابة API الطلب الكامل مع محتوى المستند المشفر بـ base64
  3. يترجم LibrarianRequestor الطلب إلى رسالة Pulsar
  4. يتلقى خدمة Librarian الرسالة، ويفك تشفير المستند في الذاكرة
  5. يقوم BlobStore بتحميل المستند إلى Garage/S3
  6. تخزن Cassandra البيانات الوصفية مع مرجع الكائن
  7. للمعالجة: يتم استرداد المستند من S3، وفك ترميزه، وتقطيعه - كل ذلك في الذاكرة

الملفات الرئيسية: نقطة الدخول REST/WebSocket: trustgraph-flow/trustgraph/gateway/service.py النواة الأساسية لـ Librarian: trustgraph-flow/trustgraph/librarian/librarian.py تخزين الكائنات الثنائية: trustgraph-flow/trustgraph/librarian/blob_store.py جداول Cassandra: trustgraph-flow/trustgraph/tables/library.py مخطط API: trustgraph-base/trustgraph/schema/services/library.py

القيود الحالية

تحتوي التصميم الحالي على عدة مشكلات مركبة في الذاكرة وتجربة المستخدم:

  1. عملية تحميل ذرية: يجب إرسال المستند بأكمله في طلب واحد. تتطلب المستندات الكبيرة طلبات طويلة الأمد بدون مؤشر للتقدم ولا توجد آلية لإعادة المحاولة في حالة فشل الاتصال.

  2. تصميم API: تتوقع كل من واجهات برمجة تطبيقات REST و WebSocket المستند بأكمله في رسالة واحدة. يحتوي المخطط (LibrarianRequest) على content حقل واحد يحتوي على المستند المشفر بـ base64 بأكمله.

  3. ذاكرة Librarian: تقوم خدمة Librarian بفك تشفير المستند بأكمله في الذاكرة قبل تحميله إلى S3. بالنسبة لملف PDF بحجم 500 ميجابايت، هذا يعني الاحتفاظ بـ 500 ميجابايت + في ذاكرة العملية.

  4. ذاكرة فك ترميز PDF: عند بدء المعالجة، يقوم فك ترميز PDF بتحميل ملف PDF بأكمله في الذاكرة لاستخراج النص. تتطلب مكتبات PyPDF وما شابه ذلك عادةً الوصول إلى المستند بأكمله.

  5. ذاكرة القطع: يتلقى برنامج القطع النص المستخرج بالكامل ويحتفظ به في الذاكرة أثناء إنتاج الأجزاء.

مثال على تأثير الذاكرة (ملف PDF بحجم 500 ميجابايت): البوابة: ~700 ميجابايت (نفقات ترميز base64) Librarian: ~500 ميجابايت (بايت مُفككة) فك ترميز PDF: ~500 ميجابايت + مخازن مؤقتة للاستخراج القطع: نص مستخرج (متغير، قد يصل إلى 100 ميجابايت +)

يمكن أن تتجاوز الذاكرة القصوى لملف مستند كبير واحد 2 جيجابايت.

التصميم الفني

مبادئ التصميم

  1. واجهة API: يمر كل تفاعل عميل من خلال واجهة برمجة تطبيقات Librarian. العملاء ليس لديهم وصول مباشر أو معرفة بالتخزين الأساسي S3/Garage.

  2. تحميل متعدد الأجزاء لـ S3: استخدم تحميل متعدد الأجزاء قياسي لـ S3. هذا مدعوم على نطاق واسع في الأنظمة المتوافقة مع S3 (AWS S3، MinIO، Garage، Ceph، DigitalOcean Spaces، Backblaze B2، إلخ) مما يضمن إمكانية النقل.

  3. الإكمال الذري: عمليات تحميل متعددة الأجزاء لـ S3 ذرية بطبيعتها - يتم تحميل الأجزاء التي تم تحميلها والتي لا تظهر حتى يتم استدعاء CompleteMultipartUpload. لا توجد ملفات مؤقتة أو عمليات إعادة تسمية مطلوبة.

  4. حالة قابلة للتتبع: يتم تتبع جلسات التحميل في Cassandra، مما يوفر رؤية للتحميلات غير المكتملة ويمكنه تمكين إمكانية الاستئناف.

تدفق التحميل المقسم

Client                    Librarian API                   S3/Garage
  │                            │                              │
  │── begin-upload ───────────►│                              │
  │   (metadata, size)         │── CreateMultipartUpload ────►│
  │                            │◄── s3_upload_id ─────────────│
  │◄── upload_id ──────────────│   (store session in          │
  │                            │    Cassandra)                │
  │                            │                              │
  │── upload-chunk ───────────►│                              │
  │   (upload_id, index, data) │── UploadPart ───────────────►│
  │                            │◄── etag ─────────────────────│
  │◄── ack + progress ─────────│   (store etag in session)    │
  │         ⋮                  │         ⋮                    │
  │   (repeat for all chunks)  │                              │
  │                            │                              │
  │── complete-upload ────────►│                              │
  │   (upload_id)              │── CompleteMultipartUpload ──►│
  │                            │   (parts coalesced by S3)    │
  │                            │── store doc metadata ───────►│ Cassandra
  │◄── document_id ────────────│   (delete session)           │

العميل لا يتفاعل أبدًا مع S3 مباشرةً. يقوم المكتبار بترجمة البيانات بين واجهة برمجة التطبيقات الخاصة بنا لتحميل البيانات على أجزاء و عمليات S3 متعددة الأجزاء داخليًا.

عمليات واجهة برمجة التطبيقات الخاصة بالمكتبار

begin-upload

تهيئة جلسة تحميل بيانات على أجزاء.

الطلب:

{
  "operation": "begin-upload",
  "document-metadata": {
    "id": "doc-123",
    "kind": "application/pdf",
    "title": "Large Document",
    "user": "user-id",
    "tags": ["tag1", "tag2"]
  },
  "total-size": 524288000,
  "chunk-size": 5242880
}

الرد:

{
  "upload-id": "upload-abc-123",
  "chunk-size": 5242880,
  "total-chunks": 100
}

أمين المكتبة:

  1. يقوم بإنشاء رمز فريد upload_id و object_id (معرف فريد لتخزين الكائنات)
  2. يستدعي S3 CreateMultipartUpload، ويتلقى s3_upload_id
  3. يقوم بإنشاء سجل جلسة في Cassandra
  4. يعيد upload_id إلى العميل

upload-chunk

تحميل جزء واحد.

الطلب:

{
  "operation": "upload-chunk",
  "upload-id": "upload-abc-123",
  "chunk-index": 0,
  "content": "<base64-encoded-chunk>"
}

الرد:

{
  "upload-id": "upload-abc-123",
  "chunk-index": 0,
  "chunks-received": 1,
  "total-chunks": 100,
  "bytes-received": 5242880,
  "total-bytes": 524288000
}

أمين المكتبة:

  1. البحث عن الجلسة باستخدام upload_id
  2. التحقق من الملكية (يجب أن يتطابق المستخدم مع منشئ الجلسة)
  3. استدعاء S3 UploadPart مع بيانات الجزء، وتلقي etag
  4. تحديث سجل الجلسة بفهرس الجزء وعلامة etag
  5. إرجاع التقدم إلى العميل

يمكن إعادة محاولة الأجزاء الفاشلة - فقط أرسل chunk-index مرة أخرى.

complete-upload

إنهاء التحميل وإنشاء المستند.

الطلب:

{
  "operation": "complete-upload",
  "upload-id": "upload-abc-123"
}

الرد:

{
  "document-id": "doc-123",
  "object-id": "550e8400-e29b-41d4-a716-446655440000"
}

أمين المكتبة:

  1. يبحث عن الجلسة، ويتحقق من استلام جميع الأجزاء.
  2. يستدعي S3 CompleteMultipartUpload مع علامات الجزء (S3 تدمج الأجزاء داخليًا - لا توجد تكلفة ذاكرة لأمين المكتبة). 3. ينشئ سجل مستند في Cassandra مع البيانات الوصفية وإشارة الكائن.
  3. يحذف سجل جلسة التحميل.
  4. يعيد معرف المستند إلى العميل.

abort-upload

إلغاء عملية تحميل قيد التقدم.

الطلب:

{
  "operation": "abort-upload",
  "upload-id": "upload-abc-123"
}

أمين المكتبة:

  1. يستدعي AbortMultipartUpload لتنظيف الأجزاء.
  2. يحذف سجل الجلسة من Cassandra.

get-upload-status

حالة استعلام عن عملية تحميل (لإمكانية الاستئناف).

الطلب:

{
  "operation": "get-upload-status",
  "upload-id": "upload-abc-123"
}

الرد:

{
  "upload-id": "upload-abc-123",
  "state": "in-progress",
  "chunks-received": [0, 1, 2, 5, 6],
  "missing-chunks": [3, 4, 7, 8],
  "total-chunks": 100,
  "bytes-received": 36700160,
  "total-bytes": 524288000
}

list-uploads

عرض قائمة التحميلات غير المكتملة لمستخدم.

الطلب:

{
  "operation": "list-uploads"
}

الرد:

{
  "uploads": [
    {
      "upload-id": "upload-abc-123",
      "document-metadata": { "title": "Large Document", ... },
      "progress": { "chunks-received": 43, "total-chunks": 100 },
      "created-at": "2024-01-15T10:30:00Z"
    }
  ]
}

تخزين جلسات التحميل

تتبع عمليات التحميل الجارية في Cassandra:

CREATE TABLE upload_session (
    upload_id text PRIMARY KEY,
    user text,
    document_id text,
    document_metadata text,      -- JSON: title, kind, tags, comments, etc.
    s3_upload_id text,           -- internal, for S3 operations
    object_id uuid,              -- target blob ID
    total_size bigint,
    chunk_size int,
    total_chunks int,
    chunks_received map<int, text>,  -- chunk_index → etag
    created_at timestamp,
    updated_at timestamp
) WITH default_time_to_live = 86400;  -- 24 hour TTL

CREATE INDEX upload_session_user ON upload_session (user);

سلوك TTL: تنتهي الجلسات بعد 24 ساعة إذا لم تكتمل. عندما تنتهي صلاحية TTL في Cassandra، يتم حذف سجل الجلسة. يتم تنظيف أجزاء S3 المهجورة بواسطة سياسة دورة حياة S3 (يتم التكوين على الحاوية).

معالجة الأخطاء والتماسك

فشل تحميل الأجزاء: يحاول العميل إعادة تحميل الجزء الذي فشل (بنفس upload_id و chunk-index). عملية S3 UploadPart متسقة لنفس رقم الجزء. تتتبع الجلسة الأجزاء التي نجحت.

انقطاع اتصال العميل أثناء التحميل: تظل الجلسة موجودة في Cassandra مع تسجيل الأجزاء المستلمة. يمكن للعميل استدعاء get-upload-status لمعرفة ما هو مفقود. يمكن استئناف التحميل عن طريق تحميل الأجزاء المفقودة فقط، ثم complete-upload.

فشل التحميل الكامل: عملية S3 CompleteMultipartUpload ذرية - إما أنها تنجح بالكامل أو تفشل. في حالة الفشل، تظل الأجزاء موجودة ويمكن للعميل إعادة محاولة complete-upload. لا يمكن رؤية أي مستند جزئي على الإطلاق.

انتهاء صلاحية الجلسة: تحذف Cassandra سجل الجلسة بعد 24 ساعة. تقوم سياسة دورة حياة حاوية S3 بتنظيف عمليات التحميل متعددة الأجزاء غير المكتملة. لا يلزم أي تنظيف يدوي.

تماسك S3 متعدد الأجزاء

توفر عمليات تحميل S3 متعددة الأجزاء تماسكًا مدمجًا:

  1. الأجزاء غير مرئية: لا يمكن الوصول إلى الأجزاء التي تم تحميلها ككائنات. إنها موجودة فقط كأجزاء من عملية تحميل متعددة الأجزاء غير مكتملة.

  2. الإكمال الذري: CompleteMultipartUpload إما أنها تنجح (يظهر الكائن بشكل ذري) أو تفشل (لا يتم إنشاء كائن). لا توجد حالة جزئية.

  3. لا حاجة لإعادة التسمية: يتم تحديد مفتاح الكائن النهائي في وقت CreateMultipartUpload. يتم دمج الأجزاء مباشرة في هذا المفتاح.

  4. الدمج من جانب الخادم: تقوم S3 بدمج الأجزاء داخليًا. لا يقرأ أمين المكتبة الأجزاء مرة أخرى - لا توجد تكلفة إضافية للذاكرة بغض النظر عن حجم المستند.

امتدادات BlobStore

الملف: trustgraph-flow/trustgraph/librarian/blob_store.py

أضف طرق التحميل متعددة الأجزاء:

class BlobStore:
    # Existing methods...

    def create_multipart_upload(self, object_id: UUID, kind: str) -> str:
        """Initialize multipart upload, return s3_upload_id."""
        # minio client: create_multipart_upload()

    def upload_part(
        self, object_id: UUID, s3_upload_id: str,
        part_number: int, data: bytes
    ) -> str:
        """Upload a single part, return etag."""
        # minio client: upload_part()
        # Note: S3 part numbers are 1-indexed

    def complete_multipart_upload(
        self, object_id: UUID, s3_upload_id: str,
        parts: List[Tuple[int, str]]  # [(part_number, etag), ...]
    ) -> None:
        """Finalize multipart upload."""
        # minio client: complete_multipart_upload()

    def abort_multipart_upload(
        self, object_id: UUID, s3_upload_id: str
    ) -> None:
        """Cancel multipart upload, clean up parts."""
        # minio client: abort_multipart_upload()

اعتبارات حجم الجزء

الحد الأدنى لـ S3: 5 ميجابايت لكل جزء (باستثناء الجزء الأخير) الحد الأقصى لـ S3: 10,000 جزء لكل عملية تحميل الإعداد الافتراضي العملي: أجزاء بحجم 5 ميجابايت مستند بحجم 500 ميجابايت = 100 جزء مستند بحجم 5 جيجابايت = 1,000 جزء دقة التقدم: أجزاء أصغر = تحديثات تقدم أكثر دقة كفاءة الشبكة: أجزاء أكبر = عدد أقل من عمليات الإرسال

يمكن تكوين حجم الجزء من قبل العميل ضمن الحدود (5 ميجابايت - 100 ميجابايت).

معالجة المستندات: الاسترجاع المتدفق

يهدف سير عمل التحميل إلى إدخال المستندات إلى التخزين بكفاءة. يهدف سير عمل المعالجة إلى استخراج وتقسيم المستندات دون تحميلها بالكامل في الذاكرة.

مبدأ التصميم: المعرف، وليس المحتوى

حاليًا، عند بدء المعالجة، يتدفق محتوى المستند عبر رسائل Pulsar. يؤدي هذا إلى تحميل المستندات بأكملها في الذاكرة. بدلاً من ذلك:

تحمل رسائل Pulsar فقط معرف المستند تقوم المعالجات باسترداد محتوى المستند مباشرة من المكتبة يتم الاسترداد كـ تدفق إلى ملف مؤقت يعمل التحليل الخاص بالمستند (PDF، نص، إلخ) مع الملفات، وليس مخازن الذاكرة

هذا يجعل المكتبة غير معتمدة على هيكل المستند. يظل تحليل PDF واستخراج النص ومنطق محدد التنسيق الأخرى في فك التشفير الخاص بها.

سير المعالجة

Pulsar              PDF Decoder                Librarian              S3
  │                      │                          │                  │
  │── doc-id ───────────►│                          │                  │
  │  (processing msg)    │                          │                  │
  │                      │                          │                  │
  │                      │── stream-document ──────►│                  │
  │                      │   (doc-id)               │── GetObject ────►│
  │                      │                          │                  │
  │                      │◄── chunk ────────────────│◄── stream ───────│
  │                      │   (write to temp file)   │                  │
  │                      │◄── chunk ────────────────│◄── stream ───────│
  │                      │   (append to temp file)  │                  │
  │                      │         ⋮                │         ⋮        │
  │                      │◄── EOF ──────────────────│                  │
  │                      │                          │                  │
  │                      │   ┌──────────────────────────┐              │
  │                      │   │ temp file on disk        │              │
  │                      │   │ (memory stays bounded)   │              │
  │                      │   └────────────┬─────────────┘              │
  │                      │                │                            │
  │                      │   PDF library opens file                    │
  │                      │   extract page 1 text ──►  chunker          │
  │                      │   extract page 2 text ──►  chunker          │
  │                      │         ⋮                                   │
  │                      │   close file                                │
  │                      │   delete temp file                          │

واجهة برمجة تطبيقات (API) لخدمة أمين المكتبة.

إضافة عملية استرجاع مستندات متدفقة:

stream-document

الطلب:

{
  "operation": "stream-document",
  "document-id": "doc-123"
}

الاستجابة: أجزاء ثنائية متدفقة (وليست استجابة واحدة).

بالنسبة لواجهة برمجة التطبيقات REST، فإن هذا يُرجع استجابة متدفقة مع Transfer-Encoding: chunked.

بالنسبة للمكالمات الداخلية بين الخدمات (من المعالج إلى أمين المكتبة)، يمكن أن يكون ذلك: بث مباشر من S3 عبر عنوان URL مسبق التوقيع (إذا سمحت الشبكة الداخلية بذلك). استجابات مقسمة عبر بروتوكول الخدمة. نقطة نهاية مخصصة للبث.

المتطلب الأساسي: تتدفق البيانات في أجزاء، ولا يتم تخزينها بالكامل مؤقتًا في أمين المكتبة.

تغييرات في فك ترميز PDF.

التنفيذ الحالي (يستهلك الكثير من الذاكرة):

def decode_pdf(document_content: bytes) -> str:
    reader = PdfReader(BytesIO(document_content))  # full doc in memory
    text = ""
    for page in reader.pages:
        text += page.extract_text()  # accumulating
    return text  # full text in memory

تنفيذ جديد (ملف مؤقت، تدريجي):

def decode_pdf_streaming(doc_id: str, librarian_client) -> Iterator[str]:
    """Yield extracted text page by page."""

    with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
        # Stream document to temp file
        for chunk in librarian_client.stream_document(doc_id):
            tmp.write(chunk)
        tmp.flush()

        # Open PDF from file (not memory)
        reader = PdfReader(tmp.name)

        # Yield pages incrementally
        for page in reader.pages:
            yield page.extract_text()

        # tmp file auto-deleted on context exit

ملف تعريف الذاكرة: ملف مؤقت على القرص: حجم ملف PDF (القرص رخيص). في الذاكرة: صفحة واحدة من النص في كل مرة. أقصى استخدام للذاكرة: محدود، ولا يعتمد على حجم المستند.

تغييرات في وحدة فك ترميز المستندات النصية.

بالنسبة للمستندات النصية العادية، الأمر أبسط حتى - لا حاجة لملف مؤقت:

def decode_text_streaming(doc_id: str, librarian_client) -> Iterator[str]:
    """Yield text in chunks as it streams from storage."""

    buffer = ""
    for chunk in librarian_client.stream_document(doc_id):
        buffer += chunk.decode('utf-8')

        # Yield complete lines/paragraphs as they arrive
        while '\n\n' in buffer:
            paragraph, buffer = buffer.split('\n\n', 1)
            yield paragraph + '\n\n'

    # Yield remaining buffer
    if buffer:
        yield buffer

يمكن لملفات النصوص أن تُرسل مباشرةً دون الحاجة إلى ملف مؤقت نظرًا لأنها مُنظمة بشكل خطي.

تكامل وحدة تقسيم البيانات (Chunker).

تتلقى وحدة تقسيم البيانات مُكررًا للنصوص (صفحات أو فقرات) وتُنتج أجزاء بشكل تدريجي:

class StreamingChunker:
    def __init__(self, chunk_size: int, overlap: int):
        self.chunk_size = chunk_size
        self.overlap = overlap

    def process(self, text_stream: Iterator[str]) -> Iterator[str]:
        """Yield chunks as text arrives."""
        buffer = ""

        for text_segment in text_stream:
            buffer += text_segment

            while len(buffer) >= self.chunk_size:
                chunk = buffer[:self.chunk_size]
                yield chunk
                # Keep overlap for context continuity
                buffer = buffer[self.chunk_size - self.overlap:]

        # Yield remaining buffer as final chunk
        if buffer.strip():
            yield buffer

خط أنابيب المعالجة الشاملة

async def process_document(doc_id: str, librarian_client, embedder):
    """Process document with bounded memory."""

    # Get document metadata to determine type
    metadata = await librarian_client.get_document_metadata(doc_id)

    # Select decoder based on document type
    if metadata.kind == 'application/pdf':
        text_stream = decode_pdf_streaming(doc_id, librarian_client)
    elif metadata.kind == 'text/plain':
        text_stream = decode_text_streaming(doc_id, librarian_client)
    else:
        raise UnsupportedDocumentType(metadata.kind)

    # Chunk incrementally
    chunker = StreamingChunker(chunk_size=1000, overlap=100)

    # Process each chunk as it's produced
    for chunk in chunker.process(text_stream):
        # Generate embeddings, store in vector DB, etc.
        embedding = await embedder.embed(chunk)
        await store_chunk(doc_id, chunk, embedding)

في أي نقطة، لا يتم الاحتفاظ بالمستند الكامل أو النص المستخرج بالكامل في الذاكرة.

اعتبارات ملفات مؤقتة

الموقع: استخدم دليل النظام المؤقت (/tmp أو ما يعادله). بالنسبة إلى عمليات النشر في حاويات، تأكد من أن الدليل المؤقت لديه مساحة كافية وأنه موجود على مساحة تخزين سريعة (وليس مثبتًا على الشبكة إن أمكن).

التنظيف: استخدم مديري السياق (with tempfile...) لضمان التنظيف حتى في حالة حدوث استثناءات.

المعالجة المتزامنة: تحصل كل مهمة معالجة على ملف مؤقت خاص بها. لا توجد تعارضات بين معالجة المستندات المتوازية.

مساحة القرص: الملفات المؤقتة قصيرة الأجل (مدة المعالجة). بالنسبة إلى ملف PDF بحجم 500 ميجابايت، يلزم وجود مساحة مؤقتة تبلغ 500 ميجابايت أثناء المعالجة. يمكن تطبيق حد الحجم في وقت التحميل إذا كانت مساحة القرص محدودة.

واجهة المعالجة الموحدة: المستندات الفرعية

يجب أن تساهم عملية استخراج PDF ومعالجة المستندات النصية في نفس خط المعالجة اللاحق (تقسيم إلى أجزاء → تضمينات → تخزين). لتحقيق ذلك باستخدام واجهة "جلب حسب المعرف" متسقة، يتم تخزين كتل النص المستخرجة مرة أخرى في المكتبة كمستندات فرعية.

سير العمل مع المستندات الفرعية

PDF Document                                         Text Document
     │                                                     │
     ▼                                                     │
pdf-extractor                                              │
     │                                                     │
     │ (stream PDF from librarian)                         │
     │ (extract page 1 text)                               │
     │ (store as child doc → librarian)                    │
     │ (extract page 2 text)                               │
     │ (store as child doc → librarian)                    │
     │         ⋮                                           │
     ▼                                                     ▼
[child-doc-id, child-doc-id, ...]                    [doc-id]
     │                                                     │
     └─────────────────────┬───────────────────────────────┘
                           ▼
                       chunker
                           │
                           │ (receives document ID)
                           │ (streams content from librarian)
                           │ (chunks incrementally)
                           ▼
                    [chunks → embedding → storage]

تمتلك وحدة التقسيم واجهة موحدة واحدة: استقبال معرف المستند (عبر Pulsar) تدفق المحتوى من أمين المكتبة تقسيم المحتوى

لا تعرف أو تهتم ما إذا كان المعرف يشير إلى: مستند نصي تم تحميله بواسطة المستخدم جزء نصي مستخرج من صفحة PDF أي نوع مستند مستقبلي

بيانات وصفية للمستندات الفرعية

قم بتوسيع مخطط المستند لتتبع العلاقات بين المستندات الرئيسية والفرعية:

-- Add columns to document table
ALTER TABLE document ADD parent_id text;
ALTER TABLE document ADD document_type text;

-- Index for finding children of a parent
CREATE INDEX document_parent ON document (parent_id);

أنواع المستندات:

document_type الوصف
source مستند تم تحميله بواسطة المستخدم (PDF، نص، إلخ)
extracted مشتق من مستند مصدر (مثل نص صفحة PDF)

حقول البيانات الوصفية:

الحقل المستند المصدر المستند الفرعي المستخرج
id مقدم من المستخدم أو تم إنشاؤه تم إنشاؤه (مثل {parent-id}-page-{n})
parent_id NULL معرف المستند الأصل
document_type source extracted
kind application/pdf، إلخ. text/plain
title مقدم من المستخدم تم إنشاؤه (مثل "الصفحة 3 من Report.pdf")
user مستخدم مصرح له نفس المستند الأصل

واجهة برمجة تطبيقات أمين المكتبة للمستندات الفرعية

إنشاء المستندات الفرعية (داخلي، يستخدم بواسطة pdf-extractor):

{
  "operation": "add-child-document",
  "parent-id": "doc-123",
  "document-metadata": {
    "id": "doc-123-page-1",
    "kind": "text/plain",
    "title": "Page 1"
  },
  "content": "<base64-encoded-text>"
}

بالنسبة للنصوص الصغيرة المستخرجة (عادةً ما يكون نص الصفحة أقل من 100 كيلوبايت)، فإن التحميل في عملية واحدة مقبول. بالنسبة لاستخراج النصوص الكبيرة جدًا، يمكن استخدام التحميل المقسم.

عرض المستندات الفرعية (لأغراض التصحيح/الإدارة):

عرض المستندات الفرعية (لأغراض التصحيح/الإدارة):

{
  "operation": "list-children",
  "parent-id": "doc-123"
}

الرد:

{
  "children": [
    { "id": "doc-123-page-1", "title": "Page 1", "kind": "text/plain" },
    { "id": "doc-123-page-2", "title": "Page 2", "kind": "text/plain" },
    ...
  ]
}

السلوك الظاهر للمستخدم

السلوك الافتراضي list-documents:

SELECT * FROM document WHERE user = ? AND parent_id IS NULL;

تظهر فقط المستندات الرئيسية (المصدر) في قائمة المستندات الخاصة بالمستخدم. يتم استبعاد المستندات الفرعية افتراضيًا.

علامة "include-children" اختيارية (للمسؤولين/تصحيح الأخطاء):

{
  "operation": "list-documents",
  "include-children": true
}

حذف متسلسل

عندما يتم حذف مستند رئيسي، يجب حذف جميع العناصر التابعة له:

def delete_document(doc_id: str):
    # Find all children
    children = query("SELECT id, object_id FROM document WHERE parent_id = ?", doc_id)

    # Delete child blobs from S3
    for child in children:
        blob_store.delete(child.object_id)

    # Delete child metadata from Cassandra
    execute("DELETE FROM document WHERE parent_id = ?", doc_id)

    # Delete parent blob and metadata
    parent = get_document(doc_id)
    blob_store.delete(parent.object_id)
    execute("DELETE FROM document WHERE id = ? AND user = ?", doc_id, user)

اعتبارات التخزين

النصوص المستخرجة قد تتضمن تكرارًا للمحتوى: ملف PDF الأصلي مُخزن في "Garage". النص المستخرج لكل صفحة مُخزن أيضًا في "Garage".

هذا الحل يتيح: واجهة موحدة للتقطيع: يقوم "Chunker" دائمًا باسترداد البيانات بمعرف (ID). الاستئناف/إعادة المحاولة: يمكن إعادة تشغيل العملية في مرحلة "Chunker" دون إعادة استخراج ملف PDF. تصحيح الأخطاء: يمكن فحص النص المستخرج. فصل المهام: خدمة استخراج ملف PDF وخدمة "Chunker" هما خدمتان مستقلتان.

بالنسبة لملف PDF بحجم 500 ميجابايت و200 صفحة بمتوسط 5 كيلوبايت من النص لكل صفحة: تخزين ملف PDF: 500 ميجابايت. تخزين النص المستخرج: حوالي 1 ميجابايت إجمالاً. التكلفة الإضافية: ضئيلة.

مخرجات مُستخرج ملف PDF

يقوم مُستخرج ملف PDF (pdf-extractor)، بعد معالجة المستند:

  1. يقوم بتحميل ملف PDF من "librarian" إلى ملف مؤقت.
  2. يستخرج النص صفحةً تلو الأخرى.
  3. لكل صفحة، يقوم بتخزين النص المستخرج كمستند فرعي عبر "librarian".
  4. يرسل معرفات المستندات الفرعية إلى قائمة انتظار "Chunker".
async def extract_pdf(doc_id: str, librarian_client, output_queue):
    """Extract PDF pages and store as child documents."""

    with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
        # Stream PDF to temp file
        for chunk in librarian_client.stream_document(doc_id):
            tmp.write(chunk)
        tmp.flush()

        # Extract pages
        reader = PdfReader(tmp.name)
        for page_num, page in enumerate(reader.pages, start=1):
            text = page.extract_text()

            # Store as child document
            child_id = f"{doc_id}-page-{page_num}"
            await librarian_client.add_child_document(
                parent_id=doc_id,
                document_id=child_id,
                kind="text/plain",
                title=f"Page {page_num}",
                content=text.encode('utf-8')
            )

            # Send to chunker queue
            await output_queue.send(child_id)

يتلقى المعالج هذه المعرفات الفرعية ويقوم بمعالجتها بنفس الطريقة التي يعالج بها مستند نصي تم تحميله بواسطة المستخدم. كيف كان سيعالجها.

تحديثات العميل

حزمة تطوير البرمجيات (SDK) بلغة بايثون

يجب أن تتعامل حزمة تطوير البرمجيات (SDK) بلغة بايثون (trustgraph-base/trustgraph/api/library.py) مع عمليات التحميل المقسمة بشفافية. يظل الواجهة العامة دون تغيير:

# Existing interface - no change for users
library.add_document(
    id="doc-123",
    title="Large Report",
    kind="application/pdf",
    content=large_pdf_bytes,  # Can be hundreds of MB
    tags=["reports"]
)

داخليًا، يكتشف الـ SDK حجم المستند ويغير الاستراتيجية:

class Library:
    CHUNKED_UPLOAD_THRESHOLD = 2 * 1024 * 1024  # 2MB

    def add_document(self, id, title, kind, content, tags=None, ...):
        if len(content) < self.CHUNKED_UPLOAD_THRESHOLD:
            # Small document: single operation (existing behavior)
            return self._add_document_single(id, title, kind, content, tags)
        else:
            # Large document: chunked upload
            return self._add_document_chunked(id, title, kind, content, tags)

    def _add_document_chunked(self, id, title, kind, content, tags):
        # 1. begin-upload
        session = self._begin_upload(
            document_metadata={...},
            total_size=len(content),
            chunk_size=5 * 1024 * 1024
        )

        # 2. upload-chunk for each chunk
        for i, chunk in enumerate(self._chunk_bytes(content, session.chunk_size)):
            self._upload_chunk(session.upload_id, i, chunk)

        # 3. complete-upload
        return self._complete_upload(session.upload_id)

استدعاءات ردود الأفعال للتقدم (تحسين اختياري):

def add_document(self, ..., on_progress=None):
    """
    on_progress: Optional callback(bytes_sent, total_bytes)
    """

هذا يسمح لواجهات المستخدم بعرض تقدم التحميل دون تغيير واجهة برمجة التطبيقات الأساسية.

أدوات سطر الأوامر

tg-add-library-document يستمر في العمل دون تغيير:

# Works transparently for any size - SDK handles chunking internally
tg-add-library-document --file large-report.pdf --title "Large Report"

يمكن إضافة عرض اختياري للتقدم:

tg-add-library-document --file large-report.pdf --title "Large Report" --progress
# Output:
# Uploading: 45% (225MB / 500MB)

تمت إزالة الأدوات القديمة:

tg-load-pdf - قديمة، استخدم tg-add-library-document tg-load-text - قديمة، استخدم tg-add-library-document

أوامر الإدارة/التصحيح (اختياري، أولوية منخفضة):

# List incomplete uploads (admin troubleshooting)
tg-add-library-document --list-pending

# Resume specific upload (recovery scenario)
tg-add-library-document --resume upload-abc-123 --file large-report.pdf

قد تكون هذه خيارات في الأمر الحالي بدلاً من أدوات منفصلة.

تحديثات مواصفات واجهة برمجة التطبيقات (API)

تحتاج مواصفات OpenAPI (specs/api/paths/librarian.yaml) إلى تحديثات فيما يلي:

عمليات جديدة:

begin-upload - تهيئة جلسة تحميل مقسمة upload-chunk - تحميل جزء فردي complete-upload - إنهاء التحميل abort-upload - إلغاء التحميل get-upload-status - الاستعلام عن تقدم التحميل list-uploads - قائمة بالتحميلات غير المكتملة للمستخدم stream-document - استرجاع المستندات المتدفقة add-child-document - تخزين النص المستخرج (داخلي) list-children - قائمة بالمستندات الفرعية (للمسؤول)

عمليات معدلة:

list-documents - إضافة معلمة include-children

مخططات جديدة:

ChunkedUploadBeginRequest ChunkedUploadBeginResponse ChunkedUploadChunkRequest ChunkedUploadChunkResponse UploadSession UploadProgress

تحديثات مواصفات WebSocket (specs/websocket/):

قم بمحاكاة العمليات REST لعملاء WebSocket، مما يتيح تحديثات التقدم في الوقت الفعلي أثناء التحميل.

اعتبارات تجربة المستخدم (UX)

تتيح تحديثات مواصفات واجهة برمجة التطبيقات (API) تحسينات في الواجهة الأمامية:

واجهة مستخدم لتقدم التحميل: شريط تقدم يوضح الأجزاء التي تم تحميلها الوقت المقدر المتبقي إمكانية الإيقاف المؤقت/الاستئناف

استعادة الأخطاء: خيار "استئناف التحميل" للتحميلات المتقطعة قائمة بالتحميلات المعلقة عند إعادة الاتصال

التعامل مع الملفات الكبيرة: اكتشاف حجم الملف من جانب العميل تحميل مقسم تلقائي للملفات الكبيرة ملاحظات واضحة أثناء عمليات التحميل الطويلة

تتطلب هذه التحسينات في تجربة المستخدم (UX) عملًا في الواجهة الأمامية يتم توجيهه بواسطة مواصفات واجهة برمجة التطبيقات (API) المحدثة.