האפליקציה קורסת בעת טעינת קובצי PDF מעל 50 מגה-בייט — תלונה אופיינית במהלך בדיקות QA. ב-90% מהמקרים, טעינה ישירה לזיכרון גורמת ל-OutOfMemoryError באנדרואיד ולביצועים ירודים ב-iOS. פתרנו זאת באמצעות הזרמת נתונים וצינור עיבוד אסינכרוני. המשתמש מצרף קובץ PDF מ-Files.app או מהגלריה, לוחץ על "העלאה", ותוך שניות יכול לשאול שאלות על המסמך. מאחורי השניות הללו עומד צינור עיבוד מורכב: העלאת קובץ לשרת, ניתוח, חלוקה למקטעים, יצירת embeddings וכתיבה למסד נתונים וקטורי. כל שלב הוא מקור פוטנציאלי לבעיות: דליפות זיכרון במובייל, ניתוקי חיבור, חלוקה שגויה, אובדן נתונים. יישמנו צינור כזה מקצה לקצה עבור 5 פרויקטים, כולל אפליקציות עם מיליוני מסמכים. הניסיון שלנו מראה שבלי ארכיטקטורה נכונה, הצינור יכול לקרוס תחת עומס. מאמר זה מפרט את הפתרונות הטכניים לכל שלב — מבחירת ה-API להעלאה ועד טיפול בשגיאות בשרת.
איך להימנע מבעיות זיכרון במהלך העלאת מסמכים?
אנדרואיד: בחירת ה-API הנכון
עבור אנדרואיד 13+ אנו משתמשים ב-ActivityResultContracts.GetContent() עם סוג MIME application/pdf או */*. אנו מקבלים Uri מסוג content://. כדי לקרוא נתונים ללא OutOfMemoryError עבור קבצים גדולים, איננו טוענים הכל אל ByteArray — אנו מעבירים InputStream ישירות דרך RequestBody מותאם אישית ב-OkHttp. דוגמה:
val uri: Uri = // из ActivityResult
val requestBody = object : RequestBody() {
override fun contentType() = "application/octet-stream".toMediaType()
override fun contentLength() = -1 // unknown
override fun writeTo(sink: BufferedSink) {
contentResolver.openInputStream(uri)?.use { input ->
sink.writeAll(input.source())
}
}
}עבור קבצים מעל 50 מגה-בייט — העלאה במקטעים דרך val uri: Uri = // из ActivityResult val requestBody = object : RequestBody() { override fun contentType() = "application/octet-stream".toMediaType() override fun contentLength() = -1 // unknown override fun writeTo(sink: BufferedSink) { contentResolver.openInputStream(uri)?.use { input -> sink.writeAll(input.source()) } } } עם קריאה ל-MultipartBody בחלקים. זה מונע עומס יתר על הזיכרון ומשפר את יציבות האפליקציה.
iOS: UIDocumentPicker והעלאה בהזרמה
ב-iOS אנו משתמשים ב-writeTo עם UIDocumentPickerViewController וכו'. אנו מקבלים UTType.pdf מסוג URL. קריאה דרך file:// עבור קבצים גדולים מ-20 מגה-בייט מסוכנת — עדיף להשתמש ב-Data(contentsOf:), אשר קורא את הקובץ ישירות ממערכת הקבצים ללא טעינה לזיכרון.
let request = URLRequest(url: uploadEndpoint)
let (_, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)מידע נוסף על URLSession — בתיעוד הרשמי של אפל.
התקדמות ההעלאה
פס התקדמות הוא חובה: באנדרואיד — דרך URLSession.uploadTask(with:fromFile:) ב-OkHttp, ב-iOS — דרך המאפיין CountingOutputStream של let request = URLRequest(url: uploadEndpoint) let (_, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL) . המשתמש צריך לראות כמה נשאר. ללא התקדמות, קבצים גדולים מרגישים כאילו הם תקועים. זמן העלאה אופייני לקובץ של 20-30 מגה-בייט הוא כ-5 שניות בחיבור טוב.
שלב 2: צינור השרת — מקובץ לוקטורים
התהליך כולל: 1) העלאת קובץ, 2) חילוץ טקסט, 3) חלוקה למקטעים, 4) יצירת embeddings, 5) הכנסה למסד נתונים וקטורי. לאחר שהקובץ מועלה לשרת, אנו מתחילים צינור אסינכרוני דרך Celery (או תור דומה). הלקוח מקבל מיד URLSession, והסטטוס מנוטר באמצעות Polling או WebSocket.
@celery.task
def process_document(file_path: str, user_id: str, content_type: str):
text = extract_text(file_path, content_type)
chunks = split_into_chunks(text, chunk_size=500, overlap=50)
embeddings = create_embeddings_batch(chunks)
upsert_to_vector_store(chunks, embeddings, user_id)
update_document_status(file_path, "completed")ניתוח מסמכים — טבלה:
| פורמט | כלי | הערות |
|---|---|---|
| PDF (טקסט) | PyMuPDF (fitz) | מהיר, שומר על מבנה, מהיר פי 10 מ-PyPDF2 |
| PDF (סרוק) | Tesseract + pdf2image | איטי, דורש OCR; סריקות עד 2 דקות |
| DOCX | python-docx | ללא תמונות |
| TXT / MD | בצורה טבעית | טריוויאלי |
| HTML | BeautifulSoup | דורש הסרת תגיות |
| XLSX | openpyxl | טבלאות → טקסט שורה אחר שורה |
PyMuPDF הוא הבחירה שלנו עבור PDF: מטפל נכון בקירילית, שומר על מידע גופנים (שימושי לזיהוי כותרות). מקור: PyMuPDF.
שלב 3: אסטרטגיית חלוקה למקטעים — גודל קבוע לעומת סמנטי
| אסטרטגיה | גודל | חפיפה | מקרה שימוש |
|---|---|---|---|
| קבוע | 500 טוקנים | 50 טוקנים | אוניברסלי, פשוט |
| סמנטי | לפי משפטים/פסקאות | 1-2 משפטים | טוב יותר לשאלות, מדויק יותר |
חלוקה קבועה מהירה וצפויה יותר, אך יכולה לשבור בלוקים סמנטיים. חלוקה סמנטית איטית יותר אך משפרת את איכות תשובות RAG ב-15–20% במדידות שלנו. גודל המקטע תלוי במודל ה-embeddings: עבור uploadProgress האופטימלי הוא 512 טוקנים. אם המסמך מכיל טבלאות או קוד, הגדל את החפיפה ל-100 טוקנים. שימוש בחלוקה סמנטית עם מודלים מבוססי transformer יכול לשפר את הדיוק ב-20-30%.
שלב 4: מעקב אחר התקדמות וסטטוס אינדוקס
בזמן שהמסמך מעובד, הלקוח צריך לראות את המצב הנוכחי. שתי גישות:
-
Polling — כל 2–3 שניות שאילתה ל-
{"job_id": "abc", "status": "processing"}. פשוט, עובד בכל מקום, אך יוצר 20-30 בקשות נוספות לדקה. -
WebSocket / SSE — הלקוח נרשם לאירועים עבור
@celery.task def process_document(file_path: str, user_id: str, content_type: str): text = extract_text(file_path, content_type) chunks = split_into_chunks(text, chunk_size=500, overlap=50) embeddings = create_embeddings_batch(chunks) upsert_to_vector_store(chunks, embeddings, user_id) update_document_status(file_path, "completed"). השרת שולח עדכונים:text-embedding-3-small→/api/documents/{job_id}/status→job_id. חוויית משתמש הטובה ביותר, אך קשה יותר ליישום עם עבודה ברקע.
אנו משתמשים ב-SSE כאיזון בין מורכבות לחוויית משתמש. לאחר השלמת האינדוקס — התראה push דרך APNs/FCM. צמצמנו את זמן האינדוקס למסמך אחד ב-40% באמצעות עיבוד מקבילי של מקטעים.
שלב 5: ניהול מסמכי משתמש
המשתמש יכול למחוק מסמכים. בעת מחיקה:
- מחק את הקובץ מהאחסון.
- מחק את כל המקטעים ממסד הנתונים הוקטורי (לפי
{"step": "chunking", "progress": 0.3}+{"step": "embedding", "progress": 0.7}). - עדכן את הרשומה במסד הנתונים הרלציוני.
ב-Pinecone: {"step": "completed"}. ב-pgvector: user_id.
מסמכים נשמרים עם מטא-דאטה: שם, גודל, תאריך העלאה, מספר מקטעים, סטטוס. זה מאפשר שחזור במקרה של כשל.
ארכיטקטורה מפורטת לעיבוד מקבילי של מקטעים
כדי להפחית את זמן האינדוקס, אנו מחלקים את המסמך למקטעים ומעבדים כל מקטע ב-worker נפרד. זה דורש סדר מדויק ומיזוג של embeddings. אנו משתמשים בתור משימות עם רמות עדיפות כדי למנוע מחסור במשאבים. ההאצה היא לינארית עם מספר ה-workers עד לנקודת צוואר הבקבוק של I/O.מה כלול ביישום צינור RAG שלנו
אנו מספקים:
- עיצוב ארכיטקטורת צינור עבור התרחישים שלך
- יישום העלאה עם התקדמות ב-iOS/אנדרואיד
- צד שרת: ניתוח, חלוקה למקטעים, embeddings, אחסון וקטורי
- אינטגרציה עם Firebase Cloud Messaging להתראות
- תיעוד API והוראות פריסה
- הדרכת צוות על המערכת
- אחריות יציבות: בדיקות יחידה ובדיקות עומס
לצוות שלנו יש ניסיון של למעלה מ-5 שנים בפיתוח מובייל והוא יישם בהצלחה צינורות RAG עבור 5+ פרויקטים. קבלו ייעוץ על ארכיטקטורת הצינור.
ציר זמן ועלות
MVP עם תמיכה ב-PDF ו-TXT, חלוקה בסיסית ו-pgvector — 3–4 שבועות (החל מ-$5,000). צינור מלא עם OCR, פורמטים מרובים, תור אסינכרוני וסטטוס WebSocket — 6–8 שבועות (החל מ-$12,000). העלות מחושבת באופן אישי לאחר ניתוח דרישות.
צרו קשר להערכת פרויקט. הזמינו יישום מפתח ביד.







