trustgraph/docs/tech-specs/large-document-loading.he.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

38 KiB
Raw Blame History

layout title parent
default מפרט טכני לטעינת מסמכים גדולים Hebrew (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 אחסון Blob: 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 מגה-בייטים): Gateway: ~700 מגה-בייטים (עקב קידוד base64) Librarian: ~500 מגה-בייטים (בייטים מפוענחים) מפענח PDF: ~500 מגה-בייטים + חוצצים לחילוץ חולק: טקסט שחולץ (משתנה, פוטנציאלית 100 מגה-בייטים+)

הזיכרון המקסימלי יכול לעלות על 2 ג'יגה-בייטים עבור מסמך גדול יחיד.

עיצוב טכני

עקרונות עיצוב

  1. ממשק API: כל האינטראקציות עם הלקוח עוברות דרך ה-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. הספרן מתרגם בין ממשק ההעלאה המקוטעת שלנו לפעולות ה-multipart של S3 באופן פנימי.

פעולות ממשק ה-API של הספרן

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 ייחודיים (UUID עבור אחסון בלובים).
  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 עם מטא-דאטה והפניה לאובייקט.
  4. מוחק את רשומת סשן ההעלאה.
  5. מחזיר את מזהה המסמך ללקוח.

abort-upload

ביטול העלאה בתהליך.

בקשה:

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

הספרן:

  1. מתקשר ל-S3 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 (יש להגדיר על ה-bucket).

טיפול בכשלים ואטומיות

כשל בהעלאת חלק: הלקוח מנסה שוב את החלק שנכשל (באמצעות אותו upload_id ו-chunk-index). UploadPart של S3 היא פעולה אידempotent עבור אותו מספר חלק. הסשן עוקב אחר אילו חלקים הצליחו.

ניתוק לקוח במהלך העלאה: הסשן נשאר ב-Cassandra עם החלקים שהתקבלו. הלקוח יכול לקרוא ל-get-upload-status כדי לראות מה חסר. ניתן לחדש על ידי העלאת רק החלקים החסרים, ולאחר מכן complete-upload.

כשל בהעלאה מלאה: CompleteMultipartUpload של S3 היא פעולה אטומית - או מצליחה לחלוטין או נכשלת. במקרה של כשל, החלקים נשארים והלקוח יכול לנסות שוב את complete-upload. לא ניתן לראות מסמך חלקי.

תפוגת הסשן: TTL של Cassandra מוחקת את רשומת הסשן לאחר 24 שעות. מדיניות מחזור חיים של bucket ב-S3 מנקה העלאות מרובות חלקים לא שלמות. אין צורך בניקוי ידני.

אטומיות של העלאות מרובות חלקים ב-S3

העלאות מרובות חלקים ב-S3 מספקות אטומיות מובנית:

  1. חלקים אינם גלויים: חלקים שהועלו לא ניתנים לגישה כאובייקטים. הם קיימים רק כחלקים של העלאה מרובת חלקים לא שלמה.

  2. השלמה אטומית: CompleteMultipartUpload מצליחה (האובייקט מופיע באופן אטומי) או נכשלת (לא נוצר אובייקט). אין מצב חלקי.

  3. אין צורך בשינוי שם: המפתח של האובייקט הסופי מצוין בזמן CreateMultipartUpload. החלקים משולבים ישירות למפתח זה.

  4. שילוב בצד השרת: S3 משלבת חלקים באופן פנימי. ה-librarian לעולם לא קורא בחזרה חלקים - אין עומס זיכרון ללא קשר לגודל המסמך.

הרחבות 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: 5MB לכל חלק (למעט החלק האחרון) מקסימום עבור S3: 10,000 חלקים לכל העלאה ערך ברירת מחדל מעשי: מקטעים בגודל 5MB מסמך בגודל 500MB = 100 מקטעים מסמך בגודל 5GB = 1,000 מקטעים גרנולריות של התקדמות: מקטעים קטנים יותר = עדכוני התקדמות מדויקים יותר יעילות רשת: מקטעים גדולים יותר = פחות מעברים

גודל המקטע יכול להיות מוגדר על ידי הלקוח בטווח מסוים (5MB - 100MB).

עיבוד מסמכים: אחזור סטרימינג

זרימת ההעלאה מטפלת בהעברת מסמכים לאחסון בצורה יעילה. זרימת העיבוד מטפלת בחילוץ ופיצול מסמכים מבלי לטעון אותם כולם לזיכרון.

עקרון עיצוב: מזהה, לא תוכן

כיום, כאשר עיבוד מופעל, תוכן המסמך זורם באמצעות הודעות Pulsar. זה טוען מסמכים שלמים לזיכרון. במקום זאת:

הודעות Pulsar נושאות רק את מזהה המסמך מעבדים שולפים תוכן מסמך ישירות מהספרן (librarian) השליפה מתבצעת כזרם לקובץ זמני ניתוח ספציפי למסמך (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"
}

תגובה: מקטעי נתונים בינאריים (ולא תגובה יחידה).

עבור ממשק API 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 או מקביל). עבור פריסות מבוססות קונטיינרים, ודאו שספריית הזמנים מכילה מספיק מקום ושנמצאת באחסון מהיר (לא מחובר לרשת, אם אפשר).

ניקוי: יש להשתמש במנהלי הקשר (context managers) (with tempfile...) כדי להבטיח ניקוי גם במקרה של חריגות.

עיבוד מקבילי: כל משימת עיבוד מקבלת קובץ זמני משלה. אין התנגשויות בין עיבוד מקבילי של מסמכים.

שטח דיסק: קבצים זמניים הם קצרי טווח (משך העיבוד). עבור קובץ PDF בגודל 500MB, נדרש שטח זמני של 500MB במהלך העיבוד. ניתן לאכוף מגבלת גודל בזמן ההעלאה אם יש מגבלות על שטח הדיסק.

ממשק עיבוד מאוחד: מסמכים משניים

חילוץ PDF ועיבוד מסמכי טקסט צריכים להזין לאותו צינור עיבוד המשכיות (chunker → embeddings → storage). כדי להשיג זאת עם ממשק "שליפה לפי מזהה" עקבי, חלקי טקסט חלצים מאוחסנים בחזרה ל-librarian כמסמכים משניים.

זרימת עיבוד עם מסמכים משניים

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 משתמש מאומת זהה למסמך הורה

ממשק API של הספרן עבור מסמכים משניים

יצירת מסמכים משניים (פנימי, בשימוש על ידי 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>"
}

עבור טקסט קצר שחולץ (טקסט עמוד טיפוסי הוא פחות מ-100KB), העלאה בפעולה אחת מקובלת. עבור חילוצי טקסט גדולים מאוד, ניתן להשתמש בהעלאה מחולקת.

רשימת מסמכים משניים (לצרכי ניפוי באגים/ניהול):

הצגת מסמכים משניים (לצרכי ניפוי באגים/ניהול):

{
  "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;

רק מסמכים ברמה העליונה (מקור) מופיעים ברשימת המסמכים של המשתמש. מסמכים משניים מסוננים כברירת מחדל.

דגל "הכללה של תת-מסמכים" אופציונלי (למנהל/דיבוג):

{
  "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"

פשרה זו מאפשרת: ממשק "חיתוך" אחיד: "חיתוך" תמיד שולף לפי מזהה התחלה מחדש/ניסיון חוזר: ניתן להתחיל בשלב ה-"חיתוך" מבלי לחלץ מחדש את קובץ ה-PDF ניפוי שגיאות: ניתן לבדוק את הטקסט שחולץ הפרדת אחריות: שירות חילוץ ה-PDF ושירות ה-"חיתוך" הם שירותים עצמאיים

עבור קובץ PDF בגודל 500MB עם 200 עמודים, כאשר בממוצע יש 5KB טקסט לעמוד: אחסון קובץ PDF: 500MB אחסון טקסט שחולץ: כ-1MB בסך הכל תקורה: זניחה

פלט של מופע חילוץ PDF

מופע חילוץ ה-PDF, לאחר עיבוד מסמך:

  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) עבור Python

ערכת הפיתוח תוכנה (trustgraph-base/trustgraph/api/library.py) עבור Python צריכה לטפל בהעלאות מחולקות בצורה שקופה. הממשק הציבורי נשאר ללא שינוי:

# 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)
    """

זה מאפשר לממשקים גרפיים להציג את התקדמות ההעלאה מבלי לשנות את ממשק ה-API הבסיסי.

כלים עבור שורת הפקודה

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 המעודכן.