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.
38 KiB
| 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. הארכיטקטורה הנוכחית מתייחסת להעלאת מסמכים כפעולה אטומית אחת, דבר הגורם ללחץ זיכרון במספר נקודות במערכת ואינו מספק משוב או אפשרויות התאוששות למשתמשים.
יישום זה מכוון למקרים הבאים:
- עיבוד PDF גדול: העלאה ועיבוד של קבצי PDF בגודל מאות מגה-בייטים מבלי למצות את הזיכרון
- העלאות ניתנות לחידוש: אפשרות להמשיך העלאות שהופסקו מאותה נקודה במקום להתחיל מחדש
- משוב התקדמות: מתן למשתמשים תצוגה חיה של התקדמות ההעלאה והעיבוד
- עיבוד חסכוני בזיכרון: עיבוד מסמכים בצורה רציפה מבלי לשמור את כל הקבצים בזיכרון
מטרות
העלאה הדרגתית: תמיכה בהעלאה מקוטעת של מסמכים באמצעות REST ו-WebSocket העברות ניתנות לחידוש: אפשרות להתאושש מהעלאות שהופסקו נראות של התקדמות: מתן משוב על התקדמות ההעלאה/עיבוד ללקוחות יעילות זיכרון: ביטול אחסון מלא של מסמכים לאורך כל המערכת תאימות לאחור: זרימות עבודה קיימות עבור מסמכים קטנים ממשיכות לפעול ללא שינוי עיבוד רציף: פענוח PDF וחלוקת טקסט מתבצעות על זרמים
רקע
ארכיטקטורה נוכחית
זרימת הגשת מסמכים עוברת דרך הנתיב הבא:
- לקוח מגיש מסמך באמצעות REST (
POST /api/v1/librarian) או WebSocket - שער API מקבל בקשה שלמה עם תוכן המסמך מקודד ב-base64
- LibrarianRequestor מתרגם את הבקשה להודעת Pulsar
- שירות Librarian מקבל את ההודעה, מפענח את המסמך לזיכרון
- BlobStore מעלה את המסמך ל-Garage/S3
- Cassandra שומר מטא-דאטה עם הפניה לאובייקט
- לצורך עיבוד: המסמך נשלף מ-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
מגבלות נוכחיות
לעיצוב הנוכחי יש מספר בעיות מורכבות של זיכרון וחוויית משתמש:
-
פעולת העלאה אטומית: יש להעביר את כל המסמך בבקשה אחת. מסמכים גדולים דורשים בקשות ארוכות עם אינדיקציה מועטה להתקדמות וללא מנגנון ניסיון חוזר אם החיבור נכשל.
-
עיצוב API: גם ממשקי REST וגם WebSocket מצפים לקבל את כל המסמך בהודעה אחת. הסכימה (
LibrarianRequest) כוללת שדהcontentיחיד המכיל את תוכן המסמך המקודד ב-base64. -
זיכרון של Librarian: שירות ה-librarian מפענח את כל המסמך לזיכרון לפני העלאתו ל-S3. עבור קובץ PDF בגודל 500 מגה-בייטים, זה אומר שמירת 500 מגה-בייטים+ בזיכרון התהליך.
-
זיכרון של מפענח PDF: כאשר העיבוד מתחיל, מפענח ה-PDF טוען את ה-PDF כולו לזיכרון כדי לחלץ טקסט. ספריות כמו PyPDF דורשות בדרך כלל גישה מלאה למסמך.
-
זיכרון של חולק הטקסט: החולק של הטקסט מקבל את הטקסט שחולץ ומחזיק אותו בזיכרון תוך יצירת חלקים.
דוגמה להשפעה על הזיכרון (PDF בגודל 500 מגה-בייטים): Gateway: ~700 מגה-בייטים (עקב קידוד base64) Librarian: ~500 מגה-בייטים (בייטים מפוענחים) מפענח PDF: ~500 מגה-בייטים + חוצצים לחילוץ חולק: טקסט שחולץ (משתנה, פוטנציאלית 100 מגה-בייטים+)
הזיכרון המקסימלי יכול לעלות על 2 ג'יגה-בייטים עבור מסמך גדול יחיד.
עיצוב טכני
עקרונות עיצוב
-
ממשק API: כל האינטראקציות עם הלקוח עוברות דרך ה-API של ה-librarian. ללקוחות אין גישה ישירה או ידע לגבי אחסון S3/Garage הבסיסי.
-
העלאה מרובת חלקים של S3: שימוש בהעלאה מרובת חלקים סטנדרטית של S3. זה נתמך באופן נרחב במערכות התואמות ל-S3 (AWS S3, MinIO, Garage, Ceph, DigitalOcean Spaces, Backblaze B2 וכו') ומבטיח ניידות.
-
השלמה אטומית: העלאות מרובות חלקים של S3 הן מטבען אטומיות - חלקים שהועלו אינם גלויים עד ש-
CompleteMultipartUploadנקרא. אין צורך בקבצים זמניים או פעולות שינוי שם. -
מצב הניתן למעקב: סשנים של העלאה נרשמים ב-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
}
הספרן:
- מייצר
upload_idו-object_idייחודיים (UUID עבור אחסון בלובים). - קורא ל-S3
CreateMultipartUpload, מקבלs3_upload_id. - יוצר רשומת סשן ב-Cassandra.
- מחזיר
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
}
הספרן:
- מחפש את הסשן לפי
upload_id - מאמת בעלות (המשתמש חייב להתאים ליוצר הסשן)
- קורא ל-S3
UploadPartעם נתוני החלק, מקבלetag - מעדכן את רשומת הסשן עם אינדקס החלק ו-etag
- מחזיר התקדמות ללקוח
חלקים שנכשלו ניתנים לניסיון חוזר - פשוט שלחו שוב את chunk-index.
complete-upload
השלמת ההעלאה ויצירת המסמך.
בקשה:
{
"operation": "complete-upload",
"upload-id": "upload-abc-123"
}
תגובה:
{
"document-id": "doc-123",
"object-id": "550e8400-e29b-41d4-a716-446655440000"
}
הספרן:
- מחפש את הסשן, מוודא שכל החלקים התקבלו.
- קורא ל-S3
CompleteMultipartUploadעם תגיות חלק (S3 ממזגת חלקים באופן פנימי - אין עלות זיכרון לספרן). - יוצר רשומת מסמך ב-Cassandra עם מטא-דאטה והפניה לאובייקט.
- מוחק את רשומת סשן ההעלאה.
- מחזיר את מזהה המסמך ללקוח.
abort-upload
ביטול העלאה בתהליך.
בקשה:
{
"operation": "abort-upload",
"upload-id": "upload-abc-123"
}
הספרן:
- מתקשר ל-S3
AbortMultipartUploadכדי לנקות חלקים. - מוחק את רשומת הסשן מ-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 מספקות אטומיות מובנית:
-
חלקים אינם גלויים: חלקים שהועלו לא ניתנים לגישה כאובייקטים. הם קיימים רק כחלקים של העלאה מרובת חלקים לא שלמה.
-
השלמה אטומית:
CompleteMultipartUploadמצליחה (האובייקט מופיע באופן אטומי) או נכשלת (לא נוצר אובייקט). אין מצב חלקי. -
אין צורך בשינוי שם: המפתח של האובייקט הסופי מצוין בזמן
CreateMultipartUpload. החלקים משולבים ישירות למפתח זה. -
שילוב בצד השרת: 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, לאחר עיבוד מסמך:
- מוריד את קובץ ה-PDF מה-"librarian" לקובץ זמני
- מחלץ טקסט עמוד אחר עמוד
- עבור כל עמוד, שומר את הטקסט שחולץ כמסמך משני דרך ה-"librarian"
- שולח מזהי מסמכים משניים לתור ה-"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 המעודכן.