# שדרוגי Voyage — תכנית מפורטת תכנית 3-שלבית לשדרוג שכבת ה-retrieval של עוזר משפטי. שלב A מבוצע בתאריך התכנית; שלבים B ו-C ממתינים לשיחה החדשה. **הקשר**: Voyage = חיפוש (find), Claude = הבנה+כתיבה (read+write). שני המנועים מנותקים ארכיטקטונית — שינוי שכבת ה-retrieval לא משפיע על קלוד עצמו, רק על איזה chunks מגיעים אליו לקריאה. --- ## שלב A — מעבר ל-voyage-3 (✅ מבוצע) ### למה voyage-3 ולא voyage-law-2? Benchmark על 3 שאילתות עברית-משפטית עם passages אמיתיים מהקורפוס: | מודל | Perfect orderings | Total Separation | |---|---|---| | **voyage-3** | **3/3** | **+0.483** | | voyage-3.5 | 3/3 | +0.278 | | voyage-law-2 *(היה)* | 3/3 | +0.238 | | voyage-4 | 2/3 | +0.423 | | voyage-4-large | 2/3 | +0.353 | voyage-3 **מנצח כפול** — דירוג מושלם + מרווחים גדולים פי-2 מ-voyage-law-2. מימד נשאר 1024 → אין שינוי schema. ### מה בוצע 1. **Coolify env**: `VOYAGE_MODEL=voyage-3` בקונטיינר 2. **Local env (`~/.env`)**: `VOYAGE_MODEL=voyage-3` 3. **Re-embed של 5 טבלאות** באמצעות `scripts/reembed_voyage.py`: - `document_chunks` — מסמכי תיקים (~6K rows) - `paragraph_embeddings` — קורפוס סגנון (כעת ריק) - `case_law_embeddings` — stubs מצוטטים אוטו' - `precedent_chunks` — פסיקה שהועלתה (~385) - `halachot.embedding` — 400 הלכות (rule_statement + reasoning) 4. **MCP server restart** — טעינה מחדש של `embeddings.py` עם המודל החדש ### Verification - `search_precedent_library` על "תכנית רחביה" → 403/17 holding ראשון - `search_decisions` על "השבחה" → תוצאות עקביות - ה-counts בטבלאות לא ירדו (כל row עודכן, לא נמחק) ### Rollback אם משהו נשבר - `VOYAGE_MODEL=voyage-law-2` ב-Coolify + `~/.env` - הרצה מחדש של `scripts/reembed_voyage.py` (חוזרים לקודם) - 10 דקות סך-הכל --- ## שלב B — voyage-context-3 (לביצוע בשיחה החדשה) ### הבעיה שהוא פותר Embeddings רגילים מטמיעים **chunk בנפרד** מהקשר המסמך. פסקה שאומרת "כפי שקבענו לעיל, הפטור אינו חל" — לא יודעת על "פטור ממה" / "לעיל איפה". פסיקה משפטית מלאה בהפניות הקשר תלויות (ראה סעיף 7 לעיל; להבדיל מהמקרה ב-וע 1126/25; וכו') — והן אבודות לחלוטין. ### מה voyage-context-3 עושה אחרת API שונה: `client.contextualized_embed(inputs=[[full_doc, chunk_1], ...])`. כל chunk מוטמע **עם המסמך כולו כקונטקסט**. ה-embedding "יודע" שזו פסקה 14 מתוך פסק דין על תמ"א 38 — והקשרים פנימיים נשמרים. Anthropic פרסמו מדידה: **שיפור 49% בדיוק חיפוש** למסמכים משפטיים ארוכים. ### תכנית יישום #### B.1 — Refactor של pipeline ה-ingestion קוד נוכחי (`embeddings.py`): ```python embs = await embed_texts(chunk_texts, input_type="document") ``` קוד חדש: ```python embs = await embed_texts_with_context( document_full_text=text, chunks=chunk_texts, input_type="document", ) ``` מקומות שצריכים שינוי: - `mcp-server/.../services/embeddings.py` — פונקציה חדשה `embed_with_context` שעוטפת `client.contextualized_embed` - `mcp-server/.../services/processor.py` — `process_document()` מעביר את `text` המלא + chunks - `mcp-server/.../services/precedent_library.py` — `ingest_precedent` מעביר `text` + chunks - `mcp-server/.../services/halacha_extractor.py` — לכל הלכה, מעביר את הפסק המלא כקונטקסט (`case_law.full_text`) + `rule_statement` שמוטמע #### B.2 — Query embedding נפרד Queries מטמיעים בלי קונטקסט (`client.embed()` רגיל עם `model="voyage-context-3"` ו-`input_type="query"`). חשוב: queries ו-documents חייבים להיות באותו model space. ב-`embeddings.py:embed_query()` — מחליפים model אבל לא ה-API. #### B.3 — Re-embed של הקורפוס הקיים ```python # Pseudo-code for table in [document_chunks, precedent_chunks, halachot, ...]: rows = SELECT id, content, parent_doc_id FROM table for row in rows: full_doc = SELECT full_text FROM parent_table WHERE id = row.parent_doc_id embedding = contextualized_embed(full_doc, row.content) UPDATE table SET embedding = embedding WHERE id = row.id ``` הבעיה: כל chunk שולח את **המסמך כולו** כקונטקסט. לכן עלות לטוקן עולה משמעותית. אומדן: 178K תווים × 50 chunks = 8.9M תווים פר פסיקה, פי-50 לעומת voyage-3. החישוב לקורפוס הנוכחי (~7K rows): שווה ערך לכ-700M תווים. בtier החינמי של voyage קיים מגבלה — חשוב לבדוק לפני הרצה גדולה. **Mitigation**: לחלץ summary של 500-1000 תווים מכל מסמך (קלוד עושה את זה היום ב-`metadata_extractor`) ולהעביר ה-summary במקום הטקסט המלא. שמירת 95% מהיתרון בעלות 5%. #### B.4 — Schema אין שינוי. אותו `vector(1024)` column. #### B.5 — Benchmark לפני החלטה סופית לפני re-embed של 6951 rows: 1. לקחת 10 שאילתות אמיתיות + passages עם תיוג נכון 2. להריץ benchmark voyage-3 vs voyage-context-3 (אותו pipeline כמו `/tmp/voyage_compare.py`) 3. אם השיפור < 15% → לא שווה את העלות. נשאר ב-voyage-3 4. אם השיפור ≥ 15% → ל-go ל-context-3 #### B.6 — בדיקת זמן + עלות לאחר ה-benchmark: - אם בtier החינמי לא מספיק טוקנים → לבחור: רק documents (לא re-embed הקיים), רק פסיקה חדשה והלאה - או: לעבור ל-context-3 רק על קורפוס הפסיקה (4 פסיקות, ~785 chunks + halachot) — הקרפוס הקריטי ביותר ל-`search_precedent_library` ### החלטות שנשארו פתוחות (תיקח החלטה בשיחה החדשה) - ✋ Re-embed הכל בבת-אחת או רק חדש? - ✋ context-3 לכל הקורפוסים או רק לפסיקה (הקריטי ביותר)? - ✋ Document context = full_text או summary של 1K? --- ## שלב C — voyage-multimodal-3.5 (לביצוע בשיחה החדשה) ### הבעיה שהוא פותר תיקים סרוקים ודוחות שמאי מאבדים מידע ב-OCR: - ✗ פריסת טבלאות (שורות נתונים מתבלגנות) - ✗ חתימות וחותמות - ✗ דיאגרמות, מפות, תרשימים אדריכליים - ✗ נוסחאות מתמטיות OCR קיים (Google Cloud Vision) ממיר תמונות לטקסט אבל מטפל בעמוד כשורה- אחר-שורה. תוצאה: בדוח שמאי "שווי לפני | שווי אחרי | ≈ 1.5M ש"ח" הופך ל-"שווי לפני שווי אחרי 1.5M ש"ח" — חיפוש "שומה ל-1.5M" לא תמיד מוצא. ### מה voyage-multimodal-3.5 עושה API: `client.multimodal_embed(inputs=[[image, text?], ...])`. מקבל תמונה (PIL Image או URL) ומחזיר embedding שכולל: - את הטקסט שעל העמוד - את **המבנה הוויזואלי** (טבלה, חתימה, מיקומי גוש) - תרשימים ודיאגרמות Searchable יחד עם text embeddings — query טקסטואלית רגילה מוצאת גם פסקאות עם טבלה רלוונטית. ### תכנית יישום #### C.1 — Schema חדש ```sql CREATE TABLE document_image_embeddings ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), document_id UUID REFERENCES documents(id) ON DELETE CASCADE, page_number INTEGER NOT NULL, image_thumbnail_path TEXT, -- לסרגל תוצאות חיפוש embedding vector(1024), created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_doc_img_emb_vec ON document_image_embeddings USING ivfflat (embedding vector_cosine_ops); CREATE TABLE precedent_image_embeddings ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), case_law_id UUID REFERENCES case_law(id) ON DELETE CASCADE, page_number INTEGER NOT NULL, image_thumbnail_path TEXT, embedding vector(1024), created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_prec_img_emb_vec ON precedent_image_embeddings USING ivfflat (embedding vector_cosine_ops); ``` #### C.2 — Pipeline שינוי חדש ב-`extractor.py`: ```python async def render_pages_as_images(pdf_path: str) -> list[bytes]: """PyMuPDF render of each page → PNG bytes for multimodal embedding.""" import fitz doc = fitz.open(pdf_path) images = [] for page in doc: pix = page.get_pixmap(dpi=144) # decent resolution for embeddings images.append(pix.tobytes("png")) return images ``` חדש ב-`embeddings.py`: ```python async def embed_images(images: list[bytes], input_type: str = "document") -> list[list[float]]: """Embed page images via voyage-multimodal-3.5.""" from PIL import Image import io pil_images = [Image.open(io.BytesIO(img)) for img in images] response = _get_client().multimodal_embed( inputs=[[img] for img in pil_images], model="voyage-multimodal-3.5", input_type=input_type, ) return response.embeddings ``` #### C.3 — Integration ב-ingest pipelines `processor.py:process_document` (תיק): ```python # אחרי extract+chunk+embed הטקסטואלי: images = await extractor.render_pages_as_images(file_path) img_embs = await embeddings.embed_images(images) await db.store_document_image_embeddings(document_id, img_embs, thumbnails) ``` `precedent_library.py:ingest_precedent`: אותו pattern, על `precedent_image_embeddings`. #### C.4 — Hybrid search חדש ב-`db.py:search_precedent_library_hybrid`: ```python async def search_precedent_library_hybrid(query, limit=10): query_emb = await embeddings.embed_query(query) query_img_emb = await embeddings.embed_query_for_multimodal(query) text_results = ... # cosine on precedent_chunks (top 30) image_results = ... # cosine on precedent_image_embeddings (top 30) # Merge: weighted score (text 0.6, image 0.4 — tunable) merged = {} for r in text_results: merged[r.case_law_id] = r.score * 0.6 for r in image_results: merged[r.case_law_id] = merged.get(r.case_law_id, 0) + r.score * 0.4 return sorted(merged.items(), key=lambda x: -x[1])[:limit] ``` #### C.5 — UI: thumbnails בתוצאות חיפוש ב-`/precedents` חיפוש סמנטי, התוצאות עם רכיב image יציגו thumbnail קטן של העמוד. לחיצה תפתח את ה-PDF במקום הרלוונטי. #### C.6 — סדר עדיפויות לדיגום 1. **דוחות שמאי** — הזכייה הגדולה (טבלאות = ערכים מספריים שכרגע הולכים לאיבוד ב-OCR) 2. **תיקים סרוקים ישנים** — שיפור ה-recall של חיפוש 3. **פסיקה עם דיאגרמות** (תרשימי גוש/חלקה) — minor #### C.7 — עלות + tier voyage-multimodal-3.5 הוא מוצר נפרד. בdoc'ים פר-עמוד: - תיק ממוצע: 50-200 עמודים - 100 תיקים = 5,000-20,000 עמודים - Free tier: 200M tokens/month — אבל multimodal נמדד ב-tokens שונה (התמונה צורכת ~1000-2000 tokens לעמוד) הערכה: 100 תיקים × 100 עמודים × 1500 tokens = 15M tokens. בthe free tier בקלות. צריך לבדוק תקרת שימוש בפועל בdocs של voyage. #### C.8 — שלבים מומלצים 1. **POC** — תיק אחד עם דו"ח שמאי. embed → search → השוואה לתוצאות טקסט-בלבד. 2. **A/B test** — חצי מהתיקים החדשים עם multimodal, חצי בלי. 4 שבועות בדיקה — האם דפנה מוצאת תוצאות מדויקות יותר? 3. **Rollout** — אם המבחן חיובי, לעבד את הקורפוס הקיים ברקע ### החלטות שנשארו פתוחות - ✋ DPI לרינדור: 144 (סביר), 200 (איכות), 96 (מהיר)? - ✋ נשמור thumbnails ב-disk או רק את ה-embeddings? - ✋ משקלות hybrid search: 0.6/0.4 או יותר נטוי לטקסט? --- ## רצף עבודה בשיחה החדשה > 1. פתחי `docs/voyage-upgrades-plan.md` (זה המסמך) > 2. אם A הצליח (verify ב-Coolify env), נמשיך ל-B (context-3) > 3. **B.5 קודם** — benchmark לפני re-embed גדול > 4. אם B מצליח, רץ ל-C — אבל ב-2 צעדים זהירים (POC → A/B → rollout) --- ## נספח: רשימה של קבצים שנגעו ב-Voyage היום קוד שנכתב/שונה: - `scripts/reembed_voyage.py` — חדש, סקריפט re-embed - `~/.env` — `VOYAGE_MODEL=voyage-3` - Coolify env (legal-ai app) — `VOYAGE_MODEL=voyage-3` קבצים שלא צריכים שינוי (CONFIRM): - `mcp-server/src/legal_mcp/services/embeddings.py` — קורא ל-config.VOYAGE_MODEL - `mcp-server/src/legal_mcp/config.py` — default ל-voyage-law-2 אבל env בקוולפיי + מקומית מנצח - כל הסוכנים (legal-writer, etc.) — לא קוראים ל-Voyage ישירות עבור B + C: השינויים במסמך הזה (לא מבוצעים עדיין).