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>
8.3 KiB
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_lawcount. - 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_lawcount (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. מקרים:
_content_hash: דטרמיניסטי;""/None→""; טקסט שונה→hash שונה.- stale-predicate: content≠indexed → stale; שווים → טרי; indexed=NULL → stale.
mark_indexedמריץ UPDATE שמעתיק content_hash→indexed_hash (monkeypatch conn).reindex_case_law: טוען full_text, קורא _chunk_embed_store ו-mark_indexed (monkeypatch), לא קורא extractor/LLM.- create_* כותב content_hash (monkeypatch — assert ה-hash מועבר ל-INSERT/upsert).
בדיקות-DB אמיתיות (V23, backfill, drift query) — smoke מול DB מקומי (5433) בסיום, כמו FU-2a/FU-7.
10. סדר-ביצוע
- בדיקות אדומות.
- V23 (
content_hash,indexed_hash) +_content_hash+mark_indexed+ כתיבת content_hash ב-create_*. reindex_case_lawב-ingest.py + קריאתmark_indexedאחרי_chunk_embed_storeבקליטה.list_stale_case_law+ health-checkstale_embedding_case_law.- MCP tool
precedent_reindex. - backfill (DB smoke):
recompute_content_hashes()— content_hash לכולם, indexed_hash=content אם יש chunks. - בדיקות ירוקות + smoke מול DB + lint + סגירת #61.2 + TaskMaster #61.