מאגר חדש ליומוני "כל יום" (עפר טויסטר) כשכבת-גילוי מעל קורפוסי-הפסיקה:
מקור-משני המצביע על פסק הדין המקורי, נקלט לטבלה נפרדת `digests`, נחפש
סמנטית, ומקושר לפסק המקורי בספריית הפסיקה — אך לעולם אינו מצוטט בהחלטה
ואינו מחלץ הלכות.
Phase 0 (spec):
- docs/spec/X12-digests-radar.md — INV-DIG1 (מצביע לא מצוטט) /
INV-DIG2 (מסלול-קליטה נפרד, לא מקביל — מקיים G2) / INV-DIG3 (קישור-לפסק
הוא הגשר; חוסר-קישור = פער גלוי). עדכון אינדקס 00/03/README.
Phase 1 (MVP):
- SCHEMA_V30: טבלת `digests` (HNSW על embedding — לא ivfflat, להימנע מ-recall
cliff בקורפוס קטן/צומח) + GIN/FTS + UNIQUE חלקי ל-idempotent.
- services/digest_metadata_extractor.py — חילוץ-LLM (claude_session local-only,
ייבוא lazy): תג-מושג, כותרת-הלכה, מראה-מקום, שני-תאריכים מובחנים, תגיות.
- services/digest_library.py — מסלול קצר עצמאי (INV-DIG2): extract→hash→LLM→
embedding יחיד→autolink. לא משתמש ב-ingest.ingest_document.
- tools/digests.py + רישום 7 כלים ב-server.py (digest_upload/list/get/link/
relink/delete + search_digests).
- scripts/ingest_digests_batch.py — קליטה ידנית מ-data/digests/incoming.
- legal-researcher.md: שלב 2ב.0 (סריקת-radar לפני אימות) + סעיף-דוח ט +
3 כלים ב-frontmatter. HEARTBEAT §8: ניתוב יומון→digest_upload.
אומת end-to-end: 4 יומונים נקלטו (מטא-דאטה מדויק), חיפוש סמנטי מדרג נכון
("היטל השבחה"→5160, "תמא 38"→5158), link/relink/autolink/revert + מעטפת-MCP.
Invariants: מוסיף INV-DIG1/2/3 (X12). מקיים G2 (bounded context נפרד, לא
מסלול מקביל), G3 (idempotent upsert), G4 (אין בליעה שקטה — פער-קישור מוצף),
G9 (עקיבוּת — היומון מצביע על מקור עקיב). נוגע G7 (RRF) — נדחה, חיפוש
סמנטי-בלבד בשלב 1 (FTS index מוכן).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
187 lines
17 KiB
Markdown
187 lines
17 KiB
Markdown
# 03 — אחזור (Retrieval: Corpora · Hybrid/RRF · Attribution · Eval)
|
||
|
||
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומגדיר את **שכבת-האחזור הקנונית (TARGET)** —
|
||
שלושת הקורפוסים, כלי-החיפוש המכוונים לכל אחד, מנגנון ה-hybrid (dense + lexical) ומיזוג ה-RRF,
|
||
עקיבוּת-המקור והרמוניית-המדידה. הוא אוכף את
|
||
[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) (חוזה-שלמות לפני "ניתן-לחיפוש"),
|
||
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query) (הפרדת-קורפוס בכל query),
|
||
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן) (re-index),
|
||
[G7](00-constitution.md#inv-g7-מיזוג-rrf--לא-סכום-ציונים) (מיזוג RRF),
|
||
[G8](00-constitution.md#inv-g8-איכות-אחזור-נמדדת--precision--recall) (eval) ו-
|
||
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (עקיבוּת-מקור).
|
||
|
||
> **TARGET, לא תיאור-מצב.** כל מקום שבו הקוד בפועל סוטה מהיעד מתועד כ-**audit-finding** (§5),
|
||
> תסמין לתיקון — לא התנהגות תקינה. כל טענה על הקוד מצוטטת `file:line`.
|
||
|
||
כשל-השורש שהקובץ מייבש: **3 קורפוסים שחולקים תשתית-אחזור אחת, אך הפרדת-הקורפוס נאכפת רק על
|
||
חלק ממסלולי-ה-query** — כך שפריט מקורפוס אחד דולף לתוצאה של חיפוש בקורפוס אחר (cross-corpus leak).
|
||
|
||
---
|
||
|
||
## 1. שלושת הקורפוסים וכלי-החיפוש
|
||
|
||
| קורפוס | טבלת-אחסון | `source_kind` | כלי-MCP מכוון | אימות `file:line` |
|
||
|--------|------------|----------------|----------------|--------------------|
|
||
| מסמכי-תיק + קורפוס-סגנון דפנה | `document_chunks` | — (מובחן ב-`case_id`/`practice_area`) | `search_decisions` · `search_case_documents` · `find_similar_cases` | `search.py:15,91,145` → `hybrid_search.py:41` (`search_documents_hybrid`) → `db.search_similar` (`hybrid_search.py:56`) |
|
||
| פסיקה חיצונית סמכותית | `case_law` + `precedent_chunks`/`halachot` | `external_upload` | `search_precedent_library` | `search.py`→`precedent_library.py:235` → `search_library` → `hybrid_search.py:89,101` (`source_kind="external_upload"`) |
|
||
| החלטות ועדות-ערר (פנימי) | `case_law` + `precedent_chunks`/`halachot` | `internal_committee` | `search_internal_decisions` | `search.py:228` → `internal_decisions.py:395,411-418` (`source_kind="internal_committee"`) → `hybrid_search.py:89` |
|
||
|
||
**הבחנת-שם קריטית (לא קורפוס רביעי):** `precedent_search_library` (`server.py:160`) הוא כלי **שונה** —
|
||
מחפש בציטוטים שהיו"ר צירפה ידנית לתיקים (`case_precedents`), לא בקורפוס הפסיקה הסמכותית.
|
||
`search_precedent_library` (`server.py:280`) הוא הכלי לקורפוס החיצוני. אל תבלבל ביניהם.
|
||
|
||
הקורפוס החיצוני והפנימי **חולקים טבלה אחת** (`case_law`), מובחנים ב-`source_kind` בלבד
|
||
([02-data-model §2א](02-data-model.md#2א-case_law--החוזה-הקונקרטי)). שניהם רצים דרך **אותן** פונקציות-DB
|
||
(`search_precedent_library_semantic`/`_lexical`) — לכן הפרדת-הקורפוס היא **תנאי-סינון בתוך אותה שאילתה**,
|
||
ושם נולדת ההפרה ב-§5.
|
||
|
||
> **שכבת-גילוי — יומונים, לא קורפוס-ציטוט.** מעל 3 הקורפוסים יושבת שכבת-radar נפרדת: **יומונים**
|
||
> (סיכומי עפר-טויסטר), בטבלה פיזית נפרדת `digests` עם כלי `search_digests`. היומון הוא **מקור משני
|
||
> המצביע** על הפסק המקורי — **אינו** קורפוס-ציטוט רביעי, **אינו** עקיב-בפלט ([INV-RET5](#inv-ret5-כל-span-מוחזר-עקיב-למקורו)),
|
||
> ו**אינו** נוגע ב-`case_law`/`document_chunks`. ההפרדה כאן **פיזית** (טבלה נפרדת), לא תנאי-סינון —
|
||
> ולכן [INV-RET1](#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query) מתקיים טריוויאלית. מלא ב-
|
||
> [X12-digests-radar.md](X12-digests-radar.md) (INV-DIG1–DIG3).
|
||
|
||
---
|
||
|
||
## 2. עיצוב ה-hybrid retrieval
|
||
|
||
לכל קורפוס שני retrievers הטרוגניים המאוחים ב-RRF, ולא בסכום-ציונים — ראה [INV-RET3](#inv-ret3-מיזוג-retrievers-הטרוגניים-ב-rrf-בלבד):
|
||
|
||
1. **Dense (semantic)** — דמיון-קוסינוס מול `embedding vector(1024)` (voyage). פסיקה:
|
||
`search_precedent_library_semantic` (`db.py:3143`); מסמכי-תיק: `db.search_similar`.
|
||
2. **Lexical (BM25-style)** — `ts_rank_cd` מול `content_tsv`/`rule_tsv`/`meta_tsv` (Postgres FTS).
|
||
פסיקה: `search_precedent_library_lexical` (`db.py:3366`). מופעל כש-`BM25_HYBRID_ENABLED`
|
||
(`hybrid_search.py:139`).
|
||
3. **מיזוג sem+lex** — `_merge_sem_lex` (`hybrid_search.py:240-308`), נוסחת
|
||
`rrf_score = 1/(k+sem_rank) + 1/(k+lex_rank)` (`hybrid_search.py:256`).
|
||
4. **שכבת-multimodal (אופציונלית)** — כש-`MULTIMODAL_ENABLED`, עמודי-תמונה (voyage-multimodal-3)
|
||
מאוחים לטקסט ב-RRF נפרד: `_merge` (`hybrid_search.py:311-389`), `text_weight/(k+rank) +
|
||
img_weight/(k+rank)` (`hybrid_search.py:356-357`).
|
||
5. **Diversity cap (MMR-style)** — `_diversify_by_case_law` (`hybrid_search.py:196-225`): לכל היותר
|
||
`max_per_case_law` hits לכל `case_law_id`, כדי שפסק-דין יחיד לא ישתלט על הרשימה.
|
||
|
||
> **למה RRF ולא סכום משוקלל:** קוסינוס (~0.4–0.7) ו-`ts_rank_cd` (~0.001–0.5, תלוי-אורך-שאילתה)
|
||
> חיים בסקיילים שונים — סכום משוקלל היה נותן לצד אחד להשתלט במקרה. RRF מאחד **לפי דירוג**, ולכן
|
||
> עמיד להבדלי-סקייל (`hybrid_search.py:248-252,319-323`). תואם feedback קיים (RRF, לא weighted-sum).
|
||
|
||
---
|
||
|
||
## 3. Invariants של התחום
|
||
|
||
### INV-RET1: הפרדת-קורפוס נאכפת ב-100% ממסלולי-ה-query
|
||
**כלל:** הפרדת 3 הקורפוסים נאכפת בכל מסלול-אחזור — **גם בסינון ה-chunks וגם בסינון ההלכות**.
|
||
אין פריט מקורפוס אחד שמופיע בתוצאת חיפוש שכוון לקורפוס אחר. כל ענף-SQL (semantic/lexical,
|
||
chunks/halachot) נושא את אותו תנאי-`source_kind`.
|
||
**מקורות:** Pinecone — *Implement multitenancy* (metadata-filter isolation per tenant) · RAG
|
||
attribution (Lewis et al., 2020, NeurIPS — pinned non-leaking provenance) · ISO 8000 (Data
|
||
quality / conformance) | סטטוס: verified
|
||
**אכיפה:** תנאי-`source_kind` בכל ענף-SQL בשכבת-החיפוש; בדיקת-בריאות שמריצה שאילתת-ביקורת
|
||
(חיפוש מכוון-קורפוס שמחזיר פריט בעל `source_kind` זר = כשל). אוכף את
|
||
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
|
||
**הפרה ידועה:** משימה #56 — `halacha_filters` **אינם** כוללים `cl.source_kind` ב-
|
||
`search_precedent_library_semantic` (`db.py:3168`, ענף ה-halacha; לעומת `chunk_filters` שכן —
|
||
`db.py:3169`) **וב**-`search_precedent_library_lexical` (`db.py:3401` מול `db.py:3402`). שני
|
||
ה-`halacha_sql` עושים `JOIN case_law cl` בלי לסנן `source_kind` (`db.py:3236-3238`, `db.py:3475-3477`)
|
||
→ הלכות מהקורפוס הפנימי דולפות לתוצאות החיפוש בקורפוס החיצוני ולהפך → ממצא ל-[audit](../audit-report.md).
|
||
|
||
### INV-RET2: אין החזרה/אינדוקס בלי metadata מלא + locator פתיר
|
||
**כלל:** פריט אינו מוחזר מ-search (ואינו נחשף לאחזור) אלא אם **שדות-החובה שלו מולאו**
|
||
([G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)) **ובידו locator פתיר למקור**
|
||
(`case_law_id`/`document_id` + מזהה-עמוד/chunk). רשומה ללא metadata לא-ריק או ללא chunk עם
|
||
embedding מסומנת `searchable=false` ולא מוחזרת ([02-data-model INV-DM1](02-data-model.md#inv-dm1-searchable-רק-כשחוזה-השלמות-מתקיים)).
|
||
**מקורות:** Pinecone (metadata filtering — completeness לפני שליפה) · RAG attribution (Lewis et
|
||
al., 2020) · ISO 8000 (completeness) | סטטוס: verified
|
||
**אכיפה:** חוזה-שלמות בנקודת-הקליטה ([02-data-model §2](02-data-model.md#2-חוזה-שלמות-לכל-ישות-completeness-contract))
|
||
+ סינון בשכבת-החיפוש (`embedding IS NOT NULL`, `db.py:3239,3271`; `length(trim(content))>=50`,
|
||
`db.py:3274`) + בדיקת-בריאות שחושפת backlog. אוכף את
|
||
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
|
||
**הפרה ידועה:** ערן סופר 8046/24 — נקלטה בלי metadata (headnote/summary/tags ריקים), היעדר
|
||
תיזמון חילוץ-מטא-דאטה במסלול הפנימי ([01-ingest INV-ING3](01-ingest.md#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג)),
|
||
אך ללא דגל-`searchable` מפורש שימנע את חשיפתה לאחזור → ממצא ל-[audit](../audit-report.md).
|
||
|
||
### INV-RET3: מיזוג retrievers הטרוגניים ב-RRF בלבד
|
||
**כלל:** מיזוג תוצאות בין retrievers שונים (semantic↔lexical, text↔image) נעשה **אך ורק
|
||
לפי דירוג (Reciprocal Rank Fusion)** — לעולם לא סכום/ממוצע ציונים גולמיים, שכן ציונים בסקיילים
|
||
שונים אינם בני-השוואה ישירה.
|
||
**מקורות:** Elastic — *Reciprocal Rank Fusion* · Weaviate — *Hybrid Search Explained* · Manning,
|
||
Raghavan & Schütze, *Introduction to Information Retrieval* (CUP, 2008) | סטטוס: verified
|
||
**אכיפה:** מיזוג sem+lex ב-`_merge_sem_lex` (`hybrid_search.py:240-308`, נוסחה ב-`:256`) ומיזוג
|
||
text+image ב-`_merge` (`hybrid_search.py:311-389`, נוסחה ב-`:356-357`), שניהם עם
|
||
`k = MULTIMODAL_RRF_K`. אוכף את [G7](00-constitution.md#inv-g7-מיזוג-rrf--לא-סכום-ציונים).
|
||
**מצב:** **כבר ממומש** (codify, לא gap) — הקוד הקיים מיישם RRF נכון בשני המיזוגים. ה-invariant
|
||
מקבע את ההתנהגות הקיימת כחוזה. **הפרה ידועה:** —
|
||
|
||
### INV-RET4: איכות-אחזור נמדדת ב-eval harness עומד (precision + recall)
|
||
**כלל:** איכות-האחזור **נמדדת אמפירית** — precision **ו**-recall — מול **סט-שאילתות מתויג קבוע**
|
||
(labeled query set) ב-eval harness עומד. כל שינוי בשכבת-האחזור (משקלי-RRF, `k`, סף-chunk, embedder)
|
||
מלווה במדידה לפני/אחרי; אין כוונון "לפי תחושה".
|
||
**מקורות:** Manning, Raghavan & Schütze, *Introduction to Information Retrieval* (CUP, 2008 — fixed
|
||
relevance judgments, precision/recall) · RAG evaluation literature (Lewis et al., 2020 ואחריו) ·
|
||
Elastic — *relevance evaluation guidance* | סטטוס: verified
|
||
**אכיפה:** eval harness עם gold-set מתויג + בדיקת-בריאות תקופתית; שער-CI על שינוי שכבת-האחזור.
|
||
אוכף את [G8](00-constitution.md#inv-g8-איכות-אחזור-נמדדת--precision--recall).
|
||
**הפרה ידועה (GAP):** אין כיום eval harness ולא gold-set — קיים רק `telemetry.log_search_bg`
|
||
(`search.py:62,118,190,271`; `precedent_library.py:280`) שמתעד שאילתות בפועל, אך **אינו מודד
|
||
precision/recall מול תיוג** (תצפית, לא הערכה). היעד: harness שמריץ סט קבוע ומחזיר metrics →
|
||
ממצא ל-[audit](../audit-report.md).
|
||
|
||
### INV-RET5: כל span מוחזר עקיב למקורו
|
||
**כלל:** כל קטע מוחזר נושא **עקיבוּת-מקור מלאה** — מזהה-מסמך/פסק-דין (`case_law_id`/`document_id`/
|
||
`case_number`) **ו**-locator בתוכו (`page_number` / `chunk_id` / `supporting_quote` להלכה). פלט
|
||
ללא ייחוס פתיר אינו תקין; היו"ר חייבת לאמת כל ציטוט מול מקורו.
|
||
**מקורות:** Council of Europe / CEPEJ — *European Ethical Charter on AI in judicial systems*
|
||
(2018, traceability) · RAG attribution (Lewis et al., 2020) · ISO 15489-1:2016 (records
|
||
authenticity/integrity) | סטטוס: verified
|
||
**אכיפה:** כל פורמטר-תוצאה כולל מזהה + locator: `search.py:77-86` (case_number/page/section),
|
||
`_format_internal_row` (`search.py:322-343`: case_number/case_name/court + content/page או
|
||
rule/quote להלכה). עקיבוּת מלאה מפורטת ב-[X5-audit-provenance.md](X5-audit-provenance.md). אוכף את
|
||
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||
**הפרה ידועה:** —
|
||
|
||
---
|
||
|
||
## 4. re-index ושינוי-תוכן (G6)
|
||
|
||
האחזור מסתמך על embeddings מסונכרנים מול התוכן. ה-tsvectors (`content_tsv`/`rule_tsv`/`meta_tsv`)
|
||
הם `GENERATED ALWAYS … STORED` (`db.py:778,782,1086`) ולכן מתעדכנים אוטומטית; אך ה-`embedding
|
||
vector(1024)` **אינו** generated — הוא תלוי-טריגר-חיצוני, נקודת-drift אפשרית
|
||
([02-data-model INV-DM3](02-data-model.md#inv-dm3-שינוי-תוכן--re-index)). שינוי-תוכן חייב להפעיל
|
||
re-embed; בדיקת-בריאות מגלה embeddings מיושנים. אוכף את
|
||
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן).
|
||
|
||
---
|
||
|
||
## 5. מצב קיים מול יעד — audit-findings
|
||
|
||
ההבדלים בין הקוד בפועל ל-TARGET. **אלו תסמינים, לא התנהגויות תקינות.** כל פריט אומת מול הקוד.
|
||
|
||
- **דליפת-הלכות חוצת-קורפוס (משימה #56).** `halacha_filters` נפתחים רק עם `review_status`
|
||
(`db.py:3168`, `db.py:3401`) ואינם מוסיפים `cl.source_kind`, בעוד `chunk_filters` כן
|
||
(`db.py:3169`, `db.py:3402`). שני ה-`halacha_sql` עושים `JOIN case_law` בלי סינון
|
||
(`db.py:3236-3242`, `db.py:3463-3482`). **תסמין:** חיפוש בקורפוס החיצוני
|
||
(`search_precedent_library`, `source_kind="external_upload"`) יכול להחזיר הלכה שמקורה
|
||
בהחלטת-ועדה פנימית — ולהפך עבור `search_internal_decisions` (`source_kind="internal_committee"`,
|
||
`internal_decisions.py:418`). **יעד:** `halacha_filters` יתחילו ב-`cl.source_kind = '{source_kind}'`
|
||
בדיוק כמו `chunk_filters` ([INV-RET1](#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query)).
|
||
- **אין eval harness — מדידת-איכות לא קיימת.** רק `telemetry.log_search_bg` מתעד שאילתות
|
||
(`search.py:62,118,190,271`); אין gold-set מתויג ואין precision/recall. **יעד:** harness עומד
|
||
([INV-RET4](#inv-ret4-איכות-אחזור-נמדדת-ב-eval-harness-עומד-precision--recall)).
|
||
- **`search_decisions` מתעד אזהרה כשאין `practice_area` אך לא חוסם.** ללא פילטר-תחום החיפוש
|
||
עלול לערבב תחומים משפטיים (`search.py:45-49,172-176` — `logger.warning`, ממשיך). **יעד:** הפרדה
|
||
לפי תחום נאכפת, לא מומלצת בלבד — תואם את עקרון ההפרדה ב-[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
|
||
- **`embedding` אינו `GENERATED` (בניגוד ל-tsvector).** נקודת-drift אפשרית בין תוכן ל-embedding
|
||
אחרי עדכון ([§4](#4-re-index-ושינוי-תוכן-g6); תואם [02-data-model](02-data-model.md#inv-dm3-שינוי-תוכן--re-index)).
|
||
**יעד:** טריגר re-embed מובטח + health-check.
|
||
|
||
---
|
||
|
||
## 6. הפניות-אחיות
|
||
|
||
- [00-constitution.md](00-constitution.md) — invariants גלובליים (G4–G9) + כללי-הנדסה.
|
||
- [01-ingest.md](01-ingest.md) — חוזה-הקליטה שמייצר את ה-chunks/embeddings שהאחזור שולף.
|
||
- [02-data-model.md](02-data-model.md) — חוזה-השלמות (searchable) + re-index שהאחזור מסנן לפיהם.
|
||
- [05-qa-review.md](05-qa-review.md) — שער-הלכה הידני (`review_status`) שמגדיר אילו הלכות searchable.
|
||
- [X5-audit-provenance.md](X5-audit-provenance.md) — עקיבוּת-מקור מלאה של כל span מוחזר (בסיס ל-INV-RET5).
|
||
- [X12-digests-radar.md](X12-digests-radar.md) — שכבת-הגילוי (יומונים) שמעל הקורפוסים — מצביעה, לא מצוטטת.
|