2026-07-22 19:00:58 +02:00
|
|
|
"""Unit tests for the chunk + embed job — no DB, no real HTTP, no real Ollama.
|
|
|
|
|
|
|
|
|
|
`chunk_text`/`hard_split`/`split_paragraphs` tests moved to
|
|
|
|
|
packages/kb-mail/tests/test_chunking.py (module 5, faza mailowa, Krok 0) alongside the code."""
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from documents_ingest.chunk_embed import (
|
|
|
|
|
EmbeddingDimensionError,
|
|
|
|
|
embed_chunk,
|
|
|
|
|
extract_content,
|
|
|
|
|
fetch_documents,
|
|
|
|
|
fetch_existing_chunk_keys,
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
insert_chunk,
|
|
|
|
|
is_ocr_junk,
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
run,
|
|
|
|
|
_vector_literal,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _paragraph(char: str, length: int) -> str:
|
|
|
|
|
return char * length
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExtractContent:
|
|
|
|
|
def test_finds_content_entity(self):
|
|
|
|
|
entities = [{"type": "filename", "value": "a.pdf"}, {"type": "content", "text": "hello"}]
|
|
|
|
|
assert extract_content(entities) == "hello"
|
|
|
|
|
|
|
|
|
|
def test_missing_content_entity_returns_empty_string(self):
|
|
|
|
|
entities = [{"type": "filename", "value": "a.pdf"}]
|
|
|
|
|
assert extract_content(entities) == ""
|
|
|
|
|
|
|
|
|
|
def test_none_text_returns_empty_string(self):
|
|
|
|
|
entities = [{"type": "content", "text": None}]
|
|
|
|
|
assert extract_content(entities) == ""
|
|
|
|
|
|
|
|
|
|
def test_empty_entities_list(self):
|
|
|
|
|
assert extract_content([]) == ""
|
|
|
|
|
assert extract_content(None) == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestVectorLiteral:
|
|
|
|
|
def test_formats_as_bracketed_csv(self):
|
|
|
|
|
assert _vector_literal([0.1, 0.2, -0.3]) == "[0.1,0.2,-0.3]"
|
|
|
|
|
|
|
|
|
|
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
class TestIsOcrJunk:
|
fix(kb): przepiecie wszystkich odwolan wewnetrznych po migracji
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:12:24 +02:00
|
|
|
"""Pilot cases from kb/phases/kb-m5-faza3.md §3.1 and the 2026-07-16 calibration
|
|
|
|
|
session (kb/phases/kb-m5-eval-retrieval-pilot.md)."""
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
|
|
|
|
|
def test_clean_text_is_not_junk(self):
|
|
|
|
|
text = (
|
|
|
|
|
"Ogólne Warunki Ubezpieczenia PZU Auto ustalają zasady odpowiedzialności "
|
|
|
|
|
"ubezpieczyciela za szkody powstałe w związku z ruchem pojazdów mechanicznych."
|
|
|
|
|
)
|
|
|
|
|
assert is_ocr_junk(text) is False
|
|
|
|
|
|
|
|
|
|
def test_ground_barcode_binary_noise_is_junk(self):
|
|
|
|
|
# paperless:39/96/128/186/192 pattern: dense run of C0 control chars (scrambled
|
|
|
|
|
# barcode), well above the calibrated raw-count threshold of 5.
|
|
|
|
|
text = "\x01\x02\x03\x05\x06\x07\x0f\x10\x11\x12\x14\x15\x16\x17\x18\x19qQQQ\x11q\x01\x11!!AA"
|
|
|
|
|
assert is_ocr_junk(text) is True
|
|
|
|
|
|
|
|
|
|
def test_partial_mojibake_is_not_junk(self):
|
|
|
|
|
# paperless:119: a confirmed retrieval hit (query 4, eval-set) despite partial
|
|
|
|
|
# mojibake and a couple of stray control bytes — mojibake alone must not exclude it.
|
|
|
|
|
text = (
|
|
|
|
|
"Wygenerowano z systemu wydarzeń FIRST Israel | Turniej Regionalny FIRST LEGO "
|
|
|
|
|
"League\n\x01integraln¹ czêœæ dokumentacji zawodów\nARTEFAKTY Z WĘDKI"
|
|
|
|
|
)
|
|
|
|
|
assert is_ocr_junk(text) is False
|
|
|
|
|
|
|
|
|
|
def test_stray_single_control_char_in_legible_text_is_not_junk(self):
|
|
|
|
|
# paperless:141 pattern: one stray control byte in an otherwise clean English
|
|
|
|
|
# paragraph (a PDF-extraction artifact, not OCR noise).
|
|
|
|
|
text = "separable\x0fconvolution is equal to the combination of a self-attention layer"
|
|
|
|
|
assert is_ocr_junk(text) is False
|
|
|
|
|
|
|
|
|
|
def test_dot_leader_toc_is_junk(self):
|
|
|
|
|
text = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ."
|
|
|
|
|
assert is_ocr_junk(text) is True
|
|
|
|
|
|
|
|
|
|
def test_empty_text_is_not_junk(self):
|
|
|
|
|
assert is_ocr_junk("") is False
|
|
|
|
|
|
|
|
|
|
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
class _FakeEmbedResponse:
|
|
|
|
|
def __init__(self, payload, status=200):
|
|
|
|
|
self._payload = payload
|
|
|
|
|
self._status = status
|
|
|
|
|
|
|
|
|
|
async def __aenter__(self):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
async def __aexit__(self, *exc):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def json(self):
|
|
|
|
|
return self._payload
|
|
|
|
|
|
|
|
|
|
def raise_for_status(self):
|
|
|
|
|
if self._status >= 400:
|
|
|
|
|
raise RuntimeError(f"HTTP {self._status}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeOllamaSession:
|
|
|
|
|
"""Serves a fixed embedding vector for every /api/embeddings POST, or errors by filename."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, dim=1024, fail_for=None):
|
|
|
|
|
self._dim = dim
|
|
|
|
|
self._fail_for = fail_for or set()
|
|
|
|
|
self.requests: list[dict] = []
|
|
|
|
|
|
|
|
|
|
def post(self, url, json):
|
|
|
|
|
self.requests.append({"url": url, "json": json})
|
|
|
|
|
if json["prompt"] in self._fail_for:
|
|
|
|
|
return _FakeEmbedResponse({}, status=500)
|
|
|
|
|
return _FakeEmbedResponse({"embedding": [0.01] * self._dim})
|
|
|
|
|
|
|
|
|
|
async def close(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEmbedChunk:
|
|
|
|
|
async def test_returns_embedding_and_elapsed(self):
|
|
|
|
|
session = _FakeOllamaSession(dim=1024)
|
|
|
|
|
embedding, elapsed = await embed_chunk(session, "http://fake-ollama", "bge-m3", "hello world")
|
|
|
|
|
assert len(embedding) == 1024
|
|
|
|
|
assert elapsed >= 0
|
|
|
|
|
assert session.requests == [
|
|
|
|
|
{"url": "http://fake-ollama/api/embeddings", "json": {"model": "bge-m3", "prompt": "hello world"}}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
async def test_missing_embedding_key_raises(self):
|
|
|
|
|
class _EmptyResponse(_FakeEmbedResponse):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
class _Session:
|
|
|
|
|
def post(self, url, json):
|
|
|
|
|
return _EmptyResponse({})
|
|
|
|
|
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
await embed_chunk(_Session(), "http://fake-ollama", "bge-m3", "text")
|
|
|
|
|
|
|
|
|
|
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
class TestInsertSql:
|
|
|
|
|
def test_on_conflict_target_includes_model(self):
|
|
|
|
|
# Plan §3.3 fix: the old UNIQUE(envelope_id, chunk_index) silently no-opped a
|
|
|
|
|
# second embedding model via ON CONFLICT DO NOTHING (review finding, phase 2 step
|
|
|
|
|
# 6). The target must now include `model`.
|
|
|
|
|
from documents_ingest.chunk_embed import _INSERT_SQL
|
|
|
|
|
assert "ON CONFLICT (envelope_id, chunk_index, model)" in _INSERT_SQL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestInsertChunk:
|
|
|
|
|
async def test_junk_chunk_inserts_null_embedding_with_reason(self):
|
|
|
|
|
conn = _FakeConn()
|
|
|
|
|
await insert_chunk(conn, "paperless:1", 0, "junk text", None, "bge-m3", excluded_reason="ocr_junk")
|
|
|
|
|
|
|
|
|
|
assert conn.execute_calls == [("paperless:1", 0, "junk text", None, "bge-m3", "ocr_junk")]
|
|
|
|
|
|
|
|
|
|
async def test_normal_chunk_inserts_vector_with_no_reason(self):
|
|
|
|
|
conn = _FakeConn()
|
|
|
|
|
await insert_chunk(conn, "paperless:1", 0, "real text", [0.1, 0.2], "bge-m3")
|
|
|
|
|
|
|
|
|
|
assert conn.execute_calls == [("paperless:1", 0, "real text", "[0.1,0.2]", "bge-m3", None)]
|
|
|
|
|
|
|
|
|
|
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
class _FakeConn:
|
|
|
|
|
def __init__(self, docs=None, existing_keys=None, existing_model="bge-m3", execute_results=None):
|
|
|
|
|
self._docs = docs or []
|
|
|
|
|
self._existing_keys = list(existing_keys or [])
|
|
|
|
|
self._existing_model = existing_model
|
|
|
|
|
# Optional queue of command tags returned by successive execute() calls, in order
|
|
|
|
|
# (e.g. ["INSERT 0 0"] to simulate an ON CONFLICT DO NOTHING no-op). Defaults to a
|
|
|
|
|
# real insert every time.
|
|
|
|
|
self._execute_results = list(execute_results) if execute_results is not None else None
|
|
|
|
|
self.execute_calls: list[tuple] = []
|
|
|
|
|
|
|
|
|
|
async def fetch(self, query, *params):
|
|
|
|
|
if "FROM document_chunk" in query:
|
|
|
|
|
# Mirrors the real `WHERE model = $1` filter: existing_keys were "written" under
|
|
|
|
|
# existing_model, so a query for a different model must not see them.
|
|
|
|
|
if params and params[0] != self._existing_model:
|
|
|
|
|
return []
|
|
|
|
|
return [{"envelope_id": eid, "chunk_index": idx} for eid, idx in self._existing_keys]
|
|
|
|
|
return self._docs
|
|
|
|
|
|
|
|
|
|
async def execute(self, query, *params):
|
|
|
|
|
self.execute_calls.append(params)
|
|
|
|
|
if self._execute_results is not None:
|
|
|
|
|
return self._execute_results.pop(0)
|
|
|
|
|
return "INSERT 0 1"
|
|
|
|
|
|
|
|
|
|
async def close(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _row(envelope_id, content):
|
|
|
|
|
return {"id": envelope_id, "entities": json.dumps([{"type": "content", "text": content}])}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFetchHelpers:
|
|
|
|
|
async def test_fetch_documents_no_limit_offset(self):
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", "hi")])
|
|
|
|
|
docs = await fetch_documents(conn, limit=None, offset=None)
|
|
|
|
|
assert docs == [_row("paperless:1", "hi")]
|
|
|
|
|
|
|
|
|
|
async def test_fetch_existing_chunk_keys(self):
|
|
|
|
|
conn = _FakeConn(existing_keys=[("paperless:1", 0), ("paperless:1", 1)])
|
|
|
|
|
keys = await fetch_existing_chunk_keys(conn, model="bge-m3")
|
|
|
|
|
assert keys == {("paperless:1", 0), ("paperless:1", 1)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRun:
|
|
|
|
|
def _patch(self, monkeypatch, conn, ollama_session):
|
|
|
|
|
async def _fake_connect(dsn):
|
|
|
|
|
return conn
|
|
|
|
|
monkeypatch.setattr("documents_ingest.chunk_embed.asyncpg.connect", _fake_connect)
|
|
|
|
|
|
|
|
|
|
def _fake_session_factory(*args, **kwargs):
|
|
|
|
|
return ollama_session
|
|
|
|
|
monkeypatch.setattr("documents_ingest.chunk_embed.aiohttp.ClientSession", _fake_session_factory)
|
|
|
|
|
|
|
|
|
|
async def test_dry_run_counts_without_calling_ollama_or_db(self, monkeypatch):
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", "short doc")])
|
|
|
|
|
ollama = _FakeOllamaSession()
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=False)
|
|
|
|
|
|
|
|
|
|
assert stats["documents_fetched"] == 1
|
|
|
|
|
assert stats["documents_chunked"] == 1
|
|
|
|
|
assert stats["chunks_total"] == 1
|
|
|
|
|
assert stats["chunks_inserted"] == 1
|
|
|
|
|
assert ollama.requests == []
|
|
|
|
|
assert conn.execute_calls == []
|
|
|
|
|
|
|
|
|
|
async def test_empty_content_counted_separately_not_as_error(self, monkeypatch):
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", ""), _row("paperless:2", "some text")])
|
|
|
|
|
ollama = _FakeOllamaSession()
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=False)
|
|
|
|
|
|
|
|
|
|
assert stats["empty_content"] == 1
|
|
|
|
|
assert stats["documents_chunked"] == 1
|
|
|
|
|
assert stats["documents_fetched"] == 2
|
|
|
|
|
assert stats["chunks_errors"] == 0
|
|
|
|
|
|
|
|
|
|
async def test_apply_embeds_and_inserts(self, monkeypatch):
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", "some real content")])
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_inserted"] == 1
|
|
|
|
|
assert stats["embed_calls"] == 1
|
|
|
|
|
assert len(conn.execute_calls) == 1
|
|
|
|
|
assert len(ollama.requests) == 1
|
|
|
|
|
|
|
|
|
|
async def test_idempotent_skips_already_embedded_chunks(self, monkeypatch):
|
|
|
|
|
conn = _FakeConn(
|
|
|
|
|
docs=[_row("paperless:1", "some real content")],
|
|
|
|
|
existing_keys=[("paperless:1", 0)],
|
|
|
|
|
)
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_already_embedded"] == 1
|
|
|
|
|
assert stats["chunks_inserted"] == 0
|
|
|
|
|
assert ollama.requests == []
|
|
|
|
|
assert conn.execute_calls == []
|
|
|
|
|
|
|
|
|
|
async def test_rerun_after_apply_inserts_nothing_new(self, monkeypatch):
|
|
|
|
|
doc = _row("paperless:1", "some real content")
|
|
|
|
|
|
|
|
|
|
conn1 = _FakeConn(docs=[doc])
|
|
|
|
|
ollama1 = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn1, ollama1)
|
|
|
|
|
first = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
assert first["chunks_inserted"] == 1
|
|
|
|
|
|
|
|
|
|
conn2 = _FakeConn(docs=[doc], existing_keys=[("paperless:1", 0)])
|
|
|
|
|
ollama2 = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn2, ollama2)
|
|
|
|
|
second = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert second["chunks_inserted"] == 0
|
|
|
|
|
assert second["chunks_already_embedded"] == 1
|
|
|
|
|
assert ollama2.requests == []
|
|
|
|
|
|
|
|
|
|
async def test_dimension_mismatch_aborts(self, monkeypatch):
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", "some real content")])
|
|
|
|
|
ollama = _FakeOllamaSession(dim=768) # wrong dim
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(EmbeddingDimensionError):
|
|
|
|
|
await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
async def test_embed_error_is_isolated_and_counted(self, monkeypatch):
|
|
|
|
|
# Two chunks: force one to fail via a document long enough to produce 2 chunks
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
# (chunk_size/overlap kept small to make the test fast and explicit). Real
|
|
|
|
|
# word-like tokens (not a single 100-char run) so neither chunk trips is_ocr_junk.
|
|
|
|
|
para1 = " ".join(["word"] * 20)
|
|
|
|
|
para2 = " ".join(["term"] * 20)
|
|
|
|
|
big = para1 + "\n\n" + para2
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", big)])
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
ollama = _FakeOllamaSession(dim=1024, fail_for={para1})
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True, chunk_size=100, chunk_overlap=20)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_errors"] == 1
|
|
|
|
|
assert stats["chunks_inserted"] == 1
|
|
|
|
|
assert stats["chunks_total"] == (
|
|
|
|
|
stats["chunks_already_embedded"] + stats["chunks_inserted"]
|
|
|
|
|
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def test_insert_error_is_isolated_and_counted(self, monkeypatch):
|
|
|
|
|
# Two chunks; the first chunk's INSERT raises (simulated DB blip) but the run
|
|
|
|
|
# continues and the second chunk still gets embedded and inserted normally.
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
# Real word-like tokens so neither chunk trips is_ocr_junk.
|
|
|
|
|
big = " ".join(["word"] * 20) + "\n\n" + " ".join(["term"] * 20)
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", big)])
|
|
|
|
|
|
|
|
|
|
real_execute = conn.execute
|
|
|
|
|
|
|
|
|
|
async def _flaky_execute(query, *params):
|
|
|
|
|
if params[1] == 0: # chunk_index 0
|
|
|
|
|
conn.execute_calls.append(params)
|
|
|
|
|
raise RuntimeError("simulated db error")
|
|
|
|
|
return await real_execute(query, *params)
|
|
|
|
|
|
|
|
|
|
conn.execute = _flaky_execute
|
|
|
|
|
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True, chunk_size=100, chunk_overlap=20)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_errors"] == 1
|
|
|
|
|
assert stats["chunks_inserted"] == 1
|
|
|
|
|
assert stats["chunks_total"] == (
|
|
|
|
|
stats["chunks_already_embedded"] + stats["chunks_inserted"]
|
|
|
|
|
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def test_conflict_skipped_counted_separately_from_inserted(self, monkeypatch):
|
|
|
|
|
# ON CONFLICT DO NOTHING no-op: command tag reports 0 rows affected.
|
|
|
|
|
conn = _FakeConn(
|
|
|
|
|
docs=[_row("paperless:1", "some real content")], execute_results=["INSERT 0 0"]
|
|
|
|
|
)
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_conflict_skipped"] == 1
|
|
|
|
|
assert stats["chunks_inserted"] == 0
|
|
|
|
|
assert stats["chunks_total"] == (
|
|
|
|
|
stats["chunks_already_embedded"] + stats["chunks_inserted"]
|
|
|
|
|
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def test_existing_keys_scoped_to_model(self, monkeypatch):
|
|
|
|
|
# existing_keys were "written" under a different model — the WHERE model = $1
|
|
|
|
|
# filter must not treat them as already-embedded for the current --model.
|
|
|
|
|
conn = _FakeConn(
|
|
|
|
|
docs=[_row("paperless:1", "some real content")],
|
|
|
|
|
existing_keys=[("paperless:1", 0)],
|
|
|
|
|
existing_model="some-other-model",
|
|
|
|
|
)
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True, model="bge-m3")
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_already_embedded"] == 0
|
|
|
|
|
assert stats["chunks_inserted"] == 1
|
|
|
|
|
|
feat(kb): faza 3 krok 1 — porządki po pilocie retrieval (śmieć + dedup + klucz modelu)
Migracja 003 (UNIQUE+model, excluded_reason) zastosowana na żywej bazie kb-postgres@PIHA
(2683 chunki, bez DELETE). Kalibracja heurystyki ocr_junk (3 sygnały z planu §3.1) ujawniła
realną sprzeczność z planem: sygnał 1 (dowolny znak kontrolny) fałszywie łapał paperless:119
(wymagany aktywny) i legalny angielski tekst — próg doprecyzowany do >=5 wystąpień na
podstawie rozkładu na korpusie. 8 chunków oflagowanych ocr_junk po odjęciu potwierdzonych
fałszywych alarmów (kalendarz, mikro-fragmenty referencyjne).
Dedup: SQL z planu (exact content hash) znalazł 2 pary, ale nie wykrył znanego z pilota
duplikatu paperless:14≡74 (różne OCR, 99.2% chunków identycznych treściowo) — dodany fuzzy
check na potwierdzenie. Odrzucono 3 kandydatury o wysokim nakładaniu jako różne
wersje/typy dokumentów dzielące boilerplate PZU, nie duplikaty. 130 chunków oflagowanych
duplicate + entities[duplicate_of] na 3 kopertach.
chunk_embed.py: heurystyka is_ocr_junk() przed embedem (junk -> insert bez wywołania Ollamy,
embedding=NULL), ON CONFLICT rozszerzony o model, nowy licznik chunks_junk_flagged w bilansie,
testy (kody kreskowe, mojibake nie-junk, dot-leader, idempotencja, model w kluczu konfliktu).
Weryfikacja: 7 zapytań eval-setu z WHERE excluded_reason IS NULL — żadne trafienie nie
degraduje, kontrole negatywne bez zmian (>0.55), śmieć zniknął z top-5 zapytania 2,
paperless:119 pozostał aktywnym trafieniem. Bilans: 2545 aktywne / 130 duplicate / 8 ocr_junk.
Co najmniej 3 decyzje wymagały zatrzymania i potwierdzenia z Oskarem w sesji (kalibracja
progu sygnału 1, dołączenie 14≡74 mimo braku exact-hash matcha, odrzucenie 3 fałszywych
kandydatur dedup) — plan przewidywał, że heurystyka będzie się mylić; wszystkie decyzje
udokumentowane w transkrypcie sesji.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:17:18 +02:00
|
|
|
async def test_junk_chunk_flagged_without_calling_ollama(self, monkeypatch):
|
|
|
|
|
junk = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ."
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", junk)])
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_junk_flagged"] == 1
|
|
|
|
|
assert stats["chunks_inserted"] == 0
|
|
|
|
|
assert ollama.requests == []
|
|
|
|
|
assert conn.execute_calls == [("paperless:1", 0, junk, None, "bge-m3", "ocr_junk")]
|
|
|
|
|
assert stats["chunks_total"] == (
|
|
|
|
|
stats["chunks_already_embedded"] + stats["chunks_inserted"] + stats["chunks_junk_flagged"]
|
|
|
|
|
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def test_junk_chunk_dry_run_counted_without_db_or_ollama(self, monkeypatch):
|
|
|
|
|
junk = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ."
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", junk)])
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=False)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_junk_flagged"] == 1
|
|
|
|
|
assert stats["chunks_inserted"] == 0
|
|
|
|
|
assert ollama.requests == []
|
|
|
|
|
assert conn.execute_calls == []
|
|
|
|
|
|
|
|
|
|
async def test_junk_chunk_rerun_is_idempotent(self, monkeypatch):
|
|
|
|
|
# Same (envelope_id, chunk_index) already flagged ocr_junk under this model — a
|
|
|
|
|
# rerun must skip it, same as a normal embedded chunk (plan §3.1: backfill is a
|
|
|
|
|
# one-shot, but chunk_embed.py itself must stay idempotent for new junk too).
|
|
|
|
|
junk = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ."
|
|
|
|
|
conn = _FakeConn(
|
|
|
|
|
docs=[_row("paperless:1", junk)],
|
|
|
|
|
existing_keys=[("paperless:1", 0)],
|
|
|
|
|
)
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_already_embedded"] == 1
|
|
|
|
|
assert stats["chunks_junk_flagged"] == 0
|
|
|
|
|
assert conn.execute_calls == []
|
|
|
|
|
|
|
|
|
|
async def test_junk_conflict_skip_not_double_counted(self, monkeypatch):
|
|
|
|
|
junk = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ."
|
|
|
|
|
conn = _FakeConn(docs=[_row("paperless:1", junk)], execute_results=["INSERT 0 0"])
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert stats["chunks_conflict_skipped"] == 1
|
|
|
|
|
assert stats["chunks_junk_flagged"] == 0
|
|
|
|
|
assert ollama.requests == []
|
|
|
|
|
|
feat(documents-ingest): chunk + embed job (module 5 phase 2 step 6)
Adds documents-ingest-embed: chunks source='paperless' envelope content
(paragraph-preferring, ~600 tok/chunk, ~150 tok overlap, hard char-fallback
for oversized paragraphs per plan §2 decision 3), embeds each chunk via
Ollama (bge-m3, dim validated against document_chunk's VECTOR(1024) on
every response) and inserts into document_chunk. Lives in documents-ingest
per the plan's own recommendation (§6 step 6) rather than a new package —
reuses the job family's existing idempotency/stats-balance/dry-run
conventions (paperless_adapter.py, gmail-header-backfill).
A 5-angle multi-agent code review of the initial implementation surfaced
three real bugs, fixed here: hard_split() could infinite-loop if
--chunk-overlap >= --chunk-size (now guarded in both hard_split() and
main()); insert_chunk() wasn't error-isolated like embed_chunk(), so a DB
write failure would crash the whole run instead of being counted and
skipped; and ON CONFLICT DO NOTHING's outcome was discarded, so a silently
skipped row (the known gap where document_chunk's UNIQUE constraint
doesn't include `model`) would have been miscounted as a successful insert
- now tracked separately as chunks_conflict_skipped and treated as a
run failure.
Smoke-tested and run to completion live on SOLARIA against the real Ollama
instance and kb-postgres@PIHA: dry-run matched the known phase-2-step-5
figures exactly (186 fetched, 26 empty_content, 2684 chunks planned), a
--limit 10 apply + idempotent re-run + DB/distance sanity checks all
passed, and the full 186-document run inserted 2683/2684 chunks (1 isolated
error - Ollama's runtime context window rejected one pathological
dot-leader table-of-contents chunk that tokenized far more densely than
estimated; documented as a known limitation, not fixed here given it's a
single-chunk edge case). Timing: ~0.79s/chunk average on CPU (SOLARIA's
Ollama runs GPU-less per the recent GPU-reservation-disabled fix), ~35 min
wall-clock for the full pilot - the real input for scoping the later
mail-corpus embedding phase (plan §7's GPU-based estimate doesn't hold
here).
pytest: 101 passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 20:41:00 +02:00
|
|
|
async def test_stats_balance_documents_and_chunks(self, monkeypatch):
|
|
|
|
|
conn = _FakeConn(docs=[
|
|
|
|
|
_row("paperless:1", "content one"),
|
|
|
|
|
_row("paperless:2", ""),
|
|
|
|
|
_row("paperless:3", "content three"),
|
|
|
|
|
])
|
|
|
|
|
ollama = _FakeOllamaSession(dim=1024)
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
stats = await run(dsn="postgresql://fake", apply=True)
|
|
|
|
|
|
|
|
|
|
assert stats["documents_fetched"] == stats["empty_content"] + stats["documents_chunked"]
|
|
|
|
|
assert stats["chunks_total"] == (
|
|
|
|
|
stats["chunks_already_embedded"] + stats["chunks_inserted"]
|
|
|
|
|
+ stats["chunks_conflict_skipped"] + stats["chunks_errors"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def test_limit_and_offset_passed_through_to_query(self, monkeypatch):
|
|
|
|
|
captured = {}
|
|
|
|
|
|
|
|
|
|
class _Conn(_FakeConn):
|
|
|
|
|
async def fetch(self, query, *params):
|
|
|
|
|
if "FROM document_chunk" in query:
|
|
|
|
|
return []
|
|
|
|
|
captured["query"] = query
|
|
|
|
|
captured["params"] = params
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
conn = _Conn()
|
|
|
|
|
ollama = _FakeOllamaSession()
|
|
|
|
|
self._patch(monkeypatch, conn, ollama)
|
|
|
|
|
|
|
|
|
|
await run(dsn="postgresql://fake", apply=False, limit=10, offset=5)
|
|
|
|
|
|
|
|
|
|
assert "LIMIT $1" in captured["query"]
|
|
|
|
|
assert "OFFSET $2" in captured["query"]
|
|
|
|
|
assert captured["params"] == (10, 5)
|