docs(spec): FU-3 re-index on content change design (GAP-09) + close #61.2 not-applicable
content_hash/indexed_hash change detection + reindex_case_law from stored full_text (no re-OCR) + drift health-check. Verified vs 3+ sources (content- hash change detection, RAG re-embed-on-edit). #61.2 multimodal backfill closed: 42 rows are text-ingested (document_id NULL, no source PDF) — page-images impossible without a PDF to render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2128,8 +2128,8 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
"details": "מקור: בדיקת DB 2026-05-30 (precedent_image_embeddings JOIN case_law). internal_committee: 14/56 עם page-images, 42 בלי. נגזר מ-GAP-02/FU-1 boundary discussion. לא פער-תקינות — שיפור multimodal coverage.",
|
"details": "מקור: בדיקת DB 2026-05-30 (precedent_image_embeddings JOIN case_law). internal_committee: 14/56 עם page-images, 42 בלי. נגזר מ-GAP-02/FU-1 boundary discussion. לא פער-תקינות — שיפור multimodal coverage. | CLOSED not-applicable 2026-05-30: כל 42 הרשומות document_id=NULL + אין PDF בדיסק; multimodal דורש רינדור PDF → בלתי-אפשרי לרשומות-טקסט. אם יועלה PDF — ingest רגיל מטפל.",
|
||||||
"status": "pending",
|
"status": "cancelled",
|
||||||
"testStrategy": "",
|
"testStrategy": "",
|
||||||
"parentId": "61"
|
"parentId": "61"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# FU-3 — Re-Index on Content Change — עיצוב
|
||||||
|
|
||||||
|
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-30 · **ענף:** TBD
|
||||||
|
**מכסה:** GAP-09 · **מספק:** INV-DM3, INV-G6, INV-ING4 (freshness) · **משימה:** TaskMaster #61
|
||||||
|
**תלוי ב:** FU-1 (#59) · **סוג:** pure-code + backfill-hash זול (אפס re-embed בריצה רגילה)
|
||||||
|
**מיגרציה:** V23 additive (2 עמודות-hash) + backfill-hash דטרמיניסטי הפיך. אין re-embed המוני.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הבעיה (מאומת בקוד)
|
||||||
|
|
||||||
|
`embedding` אינו עמודת `GENERATED` (בניגוד ל-tsvectors שמתעדכנים אוטומטית בשינוי-תוכן). חילוץ
|
||||||
|
embedding דורש קריאת-API, ולכן אי-אפשר להפוך אותו ל-GENERATED. הממצא של מיפוי-הקוד:
|
||||||
|
|
||||||
|
- **re-ingest דרך `ingest_document` כבר מבצע re-index נכון** — `_chunk_embed_store` רץ ללא-תנאי
|
||||||
|
ו-`store_precedent_chunks(_hierarchical)` הן DELETE-then-INSERT. אז המסלול המלא תקין.
|
||||||
|
- **3 פערים אמיתיים:** (א) אין **גילוי שינוי-תוכן** (אין `content_hash`/`updated_at` ב-case_law);
|
||||||
|
(ב) אין **נקודת re-index עצמאית** — כדי להטמיע מחדש חייבים לקלוט מחדש את ה**קובץ**, אך רשומות
|
||||||
|
רבות (למשל 42 החלטות-ועדה) נקלטו מ-`text` בלי קובץ; (ג) אין **גילוי-drift** בין תוכן ל-embeddings.
|
||||||
|
|
||||||
|
**אכיפת INV-G6** ("re-index בכל שינוי-תוכן") כשהטמעה אינה GENERATED = **גילוי (hash) + כלי-reindex
|
||||||
|
מתוכן-שמור + health-check** — בדיוק כדפוס ה-drift של FU-7 (detect-don't-auto-magic).
|
||||||
|
|
||||||
|
## 2. הכרעות אדריכליות (מאומתות ≥3 מקורות)
|
||||||
|
|
||||||
|
| החלטה | נימוק | מקורות |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| `content_hash` (SHA-256 של full_text) לגילוי-שינוי, לא timestamp | hash תוכן הוא הדפוס המומלץ כשאין timestamp מהימן; דטרמיניסטי + collision-safe | Hash-based change detection (DeepWiki); Andy Dote content-hash; moby#9391 |
|
||||||
|
| re-index **מ-full_text שמור**, לא מ-re-extract/re-OCR | OCR לא-דטרמיניסטי; להשתמש בטקסט השמור (תואם [[feedback_no_reocr_retrofit]]) | RAG re-embed-on-edit (Medium); particula incremental update |
|
||||||
|
| detect→re-embed **רק שהשתנה** (לא rebuild מלא) + health-check staleness | incremental sync; ניטור recall כשהאינדקס מתיישן | apxml RAG updates; Pinecone/Weaviate (gap-audit) |
|
||||||
|
| backfill = hash בלבד (לא re-embed) — רשומות קיימות כבר מוטמעות | זול, הפיך, אפס עלות-API; re-embed רק כשתוכן באמת השתנה | — (נגזר מהמצב: 80 רשומות כבר embedded) |
|
||||||
|
|
||||||
|
## 3. הקבצים
|
||||||
|
|
||||||
|
- **Modify** `services/db.py`: V23 (`content_hash`, `indexed_hash` ב-case_law); `_content_hash(text)`;
|
||||||
|
כתיבת `content_hash` בכניסת `create_external_case_law`/`create_internal_committee_decision`/`create_case`;
|
||||||
|
`mark_indexed(case_law_id)` (מעתיק content_hash→indexed_hash); `recompute_content_hashes()` (backfill);
|
||||||
|
`list_stale_case_law()` (drift query).
|
||||||
|
- **Modify** `services/ingest.py`: אחרי `_chunk_embed_store` המוצלח → `mark_indexed(case_law_id)`; הוספת
|
||||||
|
`reindex_case_law(case_law_id)` — טוען row, chunk+embed+store מ-full_text שמור, ואז `mark_indexed`.
|
||||||
|
- **Modify** `services/metrics.py`: חשיפת `stale_embedding_case_law` count.
|
||||||
|
- **Add** MCP tool `precedent_reindex(case_law_id)` (wrapper דק ל-`ingest.reindex_case_law`) — מאפשר
|
||||||
|
הפעלה ידנית; voyage-API בלבד (אין CLI/LLM → בטוח גם בקונטיינר).
|
||||||
|
- **Test** `tests/test_reindex_on_change.py` (חדש).
|
||||||
|
|
||||||
|
**גבול:** אין שינוי לחתימות ציבוריות. `reindex_case_law` הוא **תוסף**; המסלול הקיים לא משתנה.
|
||||||
|
|
||||||
|
## 4. content_hash + indexed_hash
|
||||||
|
|
||||||
|
- `_content_hash(text) -> str`: `hashlib.sha256(text.encode()).hexdigest()`; על `""`/None → `""`.
|
||||||
|
- `content_hash` = hash של ה-full_text **הנוכחי**, נכתב בכל כתיבת-row (ב-create_*; גבול-הכתיבה כמו נרמול FU-2a).
|
||||||
|
- `indexed_hash` = ה-hash שעליו נבנו ה-chunks/embeddings **הנוכחיים**, נכתב ב-`mark_indexed` אחרי
|
||||||
|
store מוצלח (ב-ingest + ב-reindex).
|
||||||
|
- **טרי** ⇔ `content_hash = indexed_hash`. **stale** ⇔ `content_hash IS DISTINCT FROM indexed_hash`
|
||||||
|
(כולל indexed_hash=NULL = "מעולם לא הוטמע מהתוכן הזה").
|
||||||
|
|
||||||
|
## 5. `reindex_case_law(case_law_id)` (GAP-09 enforcement)
|
||||||
|
|
||||||
|
```
|
||||||
|
load case_law row → full_text (שמור)
|
||||||
|
→ _chunk_embed_store(case_law_id, full_text, page_offsets=None, ...) # אותו מסלול קנוני
|
||||||
|
→ mark_indexed(case_law_id) # indexed_hash = content_hash
|
||||||
|
return {chunks, reindexed: true}
|
||||||
|
```
|
||||||
|
- **לא** קורא ל-extractor/OCR ולא ל-LLM — רק chunk (טקסט שמור) + embed (voyage) + store. תואם
|
||||||
|
[[feedback_no_reocr_retrofit]] ו-claude_session (אין CLI).
|
||||||
|
- multimodal: מדלג (page-images דורשים PDF; רשומות-טקסט אין להן — ראה §7). אם בעתיד יש קובץ — המסלול
|
||||||
|
המלא של ingest מטפל.
|
||||||
|
- idempotent (store = DELETE-then-INSERT; mark_indexed דטרמיניסטי).
|
||||||
|
|
||||||
|
## 6. גילוי-drift + health-check
|
||||||
|
|
||||||
|
- `list_stale_case_law()` → רשומות עם full_text לא-ריק ו-`content_hash IS DISTINCT FROM indexed_hash`.
|
||||||
|
- health-check (metrics.py) חושף `stale_embedding_case_law` count (INV-G6 observability; אחות ל-
|
||||||
|
`non_searchable_case_law`/`cases_with_stale_blocks` מ-FU-2a/FU-7).
|
||||||
|
|
||||||
|
## 7. #61.2 (multimodal backfill) — נסגר כלא-ישים
|
||||||
|
|
||||||
|
בדיקת-DB (2026-05-30): 42 החלטות-ועדה ללא page-images — **כולן** `document_id=NULL` ו-full_text
|
||||||
|
קיים, ואין PDF מקור בדיסק (`data/internal-decisions/` מכיל קובץ אחד). page-images דורשים **רינדור
|
||||||
|
PDF**; לרשומות-טקסט אין PDF → **בלתי-אפשרי**. לכן #61.2 נסגר כ-not-applicable. (אם יועלה PDF לאחת —
|
||||||
|
מסלול-ה-ingest הרגיל יטפל ב-multimodal.) FU-3 core מטמיע-מחדש את ה**טקסט** של כל 42 במידת-הצורך.
|
||||||
|
|
||||||
|
## 8. שינויי-התנהגות וסיכון
|
||||||
|
|
||||||
|
| שינוי | השפעה | סיכון |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| content_hash בכתיבה | כל קליטה חדשה נושאת hash; טרי-מעצם-הקליטה | נמוך — additive |
|
||||||
|
| mark_indexed ב-ingest | רשומות חדשות = טרי (content=indexed) | נמוך |
|
||||||
|
| reindex_case_law | re-embed מתוכן שמור; עלות-API לפי-בקשה | נמוך — תוסף, ידני/מבוקר; לא רץ אוטומטית בהמוני |
|
||||||
|
| backfill hashes | content_hash לכולם; indexed_hash=content רק אם יש chunks, אחרת NULL | נמוך — הפיך, אפס re-embed |
|
||||||
|
| health-check stale count | חשיפת drift | נמוך — read-only |
|
||||||
|
|
||||||
|
## 9. אסטרטגיית בדיקה
|
||||||
|
|
||||||
|
`tests/test_reindex_on_change.py` — offline, monkeypatch. מקרים:
|
||||||
|
1. `_content_hash`: דטרמיניסטי; `""`/None→`""`; טקסט שונה→hash שונה.
|
||||||
|
2. stale-predicate: content≠indexed → stale; שווים → טרי; indexed=NULL → stale.
|
||||||
|
3. `mark_indexed` מריץ UPDATE שמעתיק content_hash→indexed_hash (monkeypatch conn).
|
||||||
|
4. `reindex_case_law`: טוען full_text, קורא _chunk_embed_store ו-mark_indexed (monkeypatch), לא קורא extractor/LLM.
|
||||||
|
5. create_* כותב content_hash (monkeypatch — assert ה-hash מועבר ל-INSERT/upsert).
|
||||||
|
|
||||||
|
> בדיקות-DB אמיתיות (V23, backfill, drift query) — smoke מול DB מקומי (5433) בסיום, כמו FU-2a/FU-7.
|
||||||
|
|
||||||
|
## 10. סדר-ביצוע
|
||||||
|
|
||||||
|
1. בדיקות אדומות.
|
||||||
|
2. V23 (`content_hash`,`indexed_hash`) + `_content_hash` + `mark_indexed` + כתיבת content_hash ב-create_*.
|
||||||
|
3. `reindex_case_law` ב-ingest.py + קריאת `mark_indexed` אחרי `_chunk_embed_store` בקליטה.
|
||||||
|
4. `list_stale_case_law` + health-check `stale_embedding_case_law`.
|
||||||
|
5. MCP tool `precedent_reindex`.
|
||||||
|
6. backfill (DB smoke): `recompute_content_hashes()` — content_hash לכולם, indexed_hash=content אם יש chunks.
|
||||||
|
7. בדיקות ירוקות + smoke מול DB + lint + סגירת #61.2 + TaskMaster #61.
|
||||||
Reference in New Issue
Block a user