Files
legal-ai/docs/voyage-upgrades-plan.md
Chaim cb0b4b6a8b
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
ops: switch embeddings to voyage-3 + plan for context-3 + multimodal-3.5
Phase A — voyage-3 migration (executed):

- VOYAGE_MODEL=voyage-3 set in Coolify (legal-ai app) and ~/.env
- scripts/reembed_voyage.py: re-embeds document_chunks (6157),
  case_law_embeddings (9), precedent_chunks (385), and halachot (400)
  using the new model. paragraph_embeddings was empty. 6951 rows
  re-embedded in 93s, ~75 rows/sec.
- Same 1024 dim → no schema change needed.

Why voyage-3 over voyage-law-2: benchmark on 3 Hebrew legal queries
with real passages from the corpus gave voyage-3 perfect ordering on
3/3 tests AND the largest separation (+0.483 vs voyage-law-2's
+0.238). voyage-4 family had bigger separation but missed top-1 on
the hardest test.

Phase B (voyage-context-3) and Phase C (voyage-multimodal-3.5 for
scanned + appraiser docs) are designed in docs/voyage-upgrades-plan.md
but deferred — to be picked up in a fresh conversation. The plan
includes:
- Phase B: contextualized embeddings refactor (~49% recall lift on
  legal docs per Anthropic's research). Same dim, but ingestion
  pipeline must pass full doc context per chunk.
- Phase C: page-level image embeddings via voyage-multimodal-3.5,
  stored in a parallel *_image_embeddings table. Hybrid text+image
  search. Targets appraiser report tables and scanned PDFs where
  current OCR loses layout.

After this commit: MCP server needs a /mcp reconnect to pick up the
new VOYAGE_MODEL env, and the legal-ai container will pick it up on
its next redeploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:43:48 +00:00

14 KiB
Raw Blame History

שדרוגי 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):

embs = await embed_texts(chunk_texts, input_type="document")

קוד חדש:

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.pyprocess_document() מעביר את text המלא + chunks
  • mcp-server/.../services/precedent_library.pyingest_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 של הקורפוס הקיים

# 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 חדש

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:

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:

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 (תיק):

# אחרי 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.

חדש ב-db.py:search_precedent_library_hybrid:

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
  • ~/.envVOYAGE_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: השינויים במסמך הזה (לא מבוצעים עדיין).