# 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).
**הפרה ידועה — ✅ נסגרה (FU-4, GAP-10, משימה #56):** בעבר `halacha_filters` לא כללו
`cl.source_kind` (בניגוד ל-`chunk_filters` שכן) → הלכות מהקורפוס הפנימי דלפו לחיפוש בקורפוס
החיצוני ולהפך. **נסגר:** שני ה-`halacha_filters` (semantic + lexical) מתחילים כעת ב-
`cl.source_kind = '{source_kind}'` בדיוק כמו `chunk_filters` (`db.py:7516,7519` ומקבילו ב-
`search_precedent_library_lexical`). מאומת ב-`test_precedent_corpus_isolation.py` (semantic+lexical,
שני ה-source_kind) → תיעוד-מקור: [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).
**הפרה ידועה — ✅ נסגרה (FU-5, GAP-11):** בעבר היה רק `telemetry.log_search_bg` (תצפית, לא
הערכה). **נסגר:** `scripts/eval_retrieval.py` מריץ את **מסלול-האחזור בייצור** (אותן פונקציות-
שירות שכלי-ה-MCP קוראים) מול gold-set מתויג (`data/eval/gold-set.jsonl`, נבנה ב-
`scripts/eval_gold_bootstrap.py`) ומחזיר precision/recall/MRR/nDCG@{5,10} — סך-הכול, פר-קורפוס
ופר-`practice_area` — מול baseline מקובע (`data/eval/baseline.json`). מטריקות-הליבה נבדקות
offline ב-`eval_retrieval.py --self-test` ("ALL PASS"). **משמעת-CI:** להריץ לפני/אחרי כל שינוי
שכבת-אחזור (משקלי-RRF/`k`/סף-chunk/embedder/rerank) ולהשוות ל-baseline. תיעוד-מקור: [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) — ✅ נסגר (FU-4, GAP-10).** בעבר `halacha_filters` לא
  כללו `cl.source_kind` (בניגוד ל-`chunk_filters`) → הלכה מהקורפוס הפנימי יכלה לחזור בחיפוש
  בקורפוס החיצוני, ולהפך. **נסגר:** `halacha_filters` מתחילים כעת ב-`cl.source_kind = '{source_kind}'`
  + `cl.searchable = true` בדיוק כמו `chunk_filters` (`db.py:7514-7519`), בשתי הפונקציות
  (semantic+lexical). מאומת ב-`test_precedent_corpus_isolation.py`
  ([INV-RET1](#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query)).
- **eval harness — ✅ נסגר (FU-5, GAP-11).** בעבר רק `telemetry.log_search_bg` (תצפית).
  **נסגר:** `scripts/eval_retrieval.py` (precision/recall/MRR/nDCG מול `data/eval/gold-set.jsonl`
  + baseline; `--self-test` עובר offline) — מדידה אמפירית מול תיוג קבוע
  ([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) — שכבת-הגילוי (יומונים) שמעל הקורפוסים — מצביעה, לא מצוטטת.
