2026-08-04 15:00:33 +02:00
|
|
|
|
---
|
|
|
|
|
|
okf: "0.1"
|
|
|
|
|
|
type: phase
|
|
|
|
|
|
visibility: private
|
|
|
|
|
|
status: active
|
|
|
|
|
|
updated: 2026-07-13
|
|
|
|
|
|
links: []
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-13 21:09:55 +02:00
|
|
|
|
# Modul 5, faza 2 — koperta dokumentow + embeddingi + cross-source (RECON + PLAN)
|
|
|
|
|
|
|
|
|
|
|
|
> Status: RECON ZAKONCZONY (2026-07-13), architektura DO ZATWIERDZENIA. Zaden kod nie
|
|
|
|
|
|
> zostal napisany, zadna migracja nie zostala odpalona, zaden model nie zostal pobrany.
|
|
|
|
|
|
> Uzupelnia `05-documents-ingest.md` (faza 1 — ekstrakcja PDF->Paperless, JUZ DZIALA).
|
|
|
|
|
|
>
|
|
|
|
|
|
> **v2 (2026-07-13, ta sama sesja):** korekta po decyzji Oskara — `correspondent` z
|
|
|
|
|
|
> Paperlessa PORZUCONY jako zrodlo cross-source (byl pusty, i tak by byl tylko zgadywany).
|
|
|
|
|
|
> Zamiast tego: deterministyczny link mail->zalacznik->dokument (sha256/nazwa pliku), juz
|
|
|
|
|
|
> obecny w danych faktury-1. Dodano tez krytyczne odkrycie: koperty mailowe NIE MAJA
|
|
|
|
|
|
> naglowkow (from/to/cc/subject) — tylko manifest zalacznikow. Plan rozszerzony o backfill.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. Stan faktyczny (zweryfikowany na zywo, read-only)
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 Filar maili — CZY MA EMBEDDINGI? **NIE.**
|
|
|
|
|
|
|
|
|
|
|
|
Sprawdzone bezposrednio w bazie i w kodzie:
|
|
|
|
|
|
|
|
|
|
|
|
- `envelope` na kb-postgres@PIHA ma **wylacznie 6 zamrozonych kolumn**
|
|
|
|
|
|
(`id, source, ts, geo, raw_ref, entities`) — zero kolumny wektorowej.
|
|
|
|
|
|
- `SELECT source, count(*) FROM envelope GROUP BY source` → **jeden wiersz: `gmail`, 225030**.
|
|
|
|
|
|
Zaden inny `source` nie istnieje jeszcze w bazie.
|
|
|
|
|
|
- `pg_extension` potwierdza `vector 0.8.3` **zainstalowane, ale NIEUZYWANE** — brak
|
|
|
|
|
|
jakiejkolwiek tabeli/kolumny typu `vector` w calej bazie.
|
|
|
|
|
|
- `grep -r "embed|vector|bge|chunk"` w `jobs/gmail-bulk-import/`, `packages/kb-mail/`,
|
|
|
|
|
|
`jobs/documents-ingest/` → **zero trafien**. Indexer opisany w `kb-01-email-design.md`
|
|
|
|
|
|
§5 (`parse -> chunk -> embed -> pgvector`) jest **wylacznie speca, nie kodem**.
|
|
|
|
|
|
|
|
|
|
|
|
**Wniosek:** to nie jest "reuzyj wzorzec maili" — to jest **pierwszy embedding w calym KB**.
|
|
|
|
|
|
Projektujemy schemat/chunking/pipeline od zera, dla dokumentow, ale w sposob ktory
|
|
|
|
|
|
mail-indexer (przyszly etap 6 z `kb-01-email-design.md`) bedzie mogl reuzyc bez zmian
|
|
|
|
|
|
(ta sama tabela chunkow, ten sam model, ten sam wzorzec joba).
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 Paperless — dane
|
|
|
|
|
|
|
|
|
|
|
|
- **189 dokumentow** (nie 186 — przybylo od czasu briefu; Paperless zyje, liczba rosnie).
|
|
|
|
|
|
- `SUM(length(content))` = 4 202 184 znakow. `AVG` = 22 234, `MIN` = 0, `MAX` = 360 466 (114 stron).
|
|
|
|
|
|
Co najmniej jeden dokument ma **pusty content** (OCR nie wyprodukowal tekstu — pewnie
|
|
|
|
|
|
czysty obraz bez warstwy tekstowej lub blad OCR) — trzeba to obslugiwac (pomijac embed,
|
|
|
|
|
|
nie crashowac).
|
|
|
|
|
|
- **`documents_correspondent` ma 0 wierszy. `documents_tag` ma 0 wierszy.** Wszystkie 189
|
|
|
|
|
|
dokumentow maja `correspondent_id = NULL`. Paperless auto-detekcja korespondenta/tagow
|
|
|
|
|
|
**nie jest skonfigurowana ani wytrenowana** — nie ma dzisiaj ZADNYCH danych do cross-source
|
|
|
|
|
|
linku poprzez pole `correspondent`. To odkrycie doprowadzilo do korekty cross-source w v2 —
|
|
|
|
|
|
patrz decyzja 4 i §1.9.
|
|
|
|
|
|
- Jakosc OCR/tekstu: **niejednolita**. Dokument #1 (polisa WARTA z 2020) ma mojibake w polskich
|
|
|
|
|
|
znakach (`integraln¹ czêœæ` zamiast `integralną część`) — najpewniej zle zdekodowana
|
|
|
|
|
|
warstwa tekstowa oryginalnego PDF, nie problem samego OCR. Dokument #191 (faktura proforma
|
|
|
|
|
|
2026) ma czyste polskie znaki (`Świdnica`, `wyłącznie`). Nie jest to systemowy problem calego
|
|
|
|
|
|
korpusu, ale chunking/embed pipeline musi to tolerowac (nie failowac na dziwnych bajtach) i
|
|
|
|
|
|
warto to logowac jako jakosciowy sygnal per-dokument.
|
|
|
|
|
|
- **Brak page-break markerow** w polu `content` (sprawdzone: `chr(12)` — form feed — nie
|
|
|
|
|
|
wystepuje w tekscie wielostronicowych dokumentow). Nie da sie dzielic po stronach z samego
|
|
|
|
|
|
tekstu; `page_count` istnieje jako osobna metadana, ale nie jest wyrownana do offsetow w tekscie.
|
|
|
|
|
|
- Losowa probka tytulow ujawnila **szum z fazy-1** (`documents-ingest` sample z zalacznikow
|
|
|
|
|
|
maili): `cryptocurrency_terms_conditions`, `crypto_exchange_terms_and_conditions`,
|
|
|
|
|
|
dokumenty konkursu robotycznego FLL dziecka. To nie sa dokumenty finansowe/tozsamosciowe —
|
|
|
|
|
|
to przypadkowe duze PDFy ktore przeszly filtr rozmiaru (>50KB) w probce fazy-1. Potwierdza
|
|
|
|
|
|
potrzebe filtra selektywnosci PRZED embedowaniem (zasada kb-00: archiwizuj/referuj wszystko,
|
|
|
|
|
|
**indeksuj selektywnie**).
|
|
|
|
|
|
- Paperless OCR: `PAPERLESS_OCR_LANGUAGE=pol+eng`, `PAPERLESS_OCR_LANGUAGES=pol` — polski
|
|
|
|
|
|
potwierdzony jako jezyk OCR.
|
|
|
|
|
|
|
|
|
|
|
|
### 1.3 Paperless API
|
|
|
|
|
|
|
|
|
|
|
|
- REST/DRF pod `http://192.168.31.5:8210/api/` (LAN, PIHA). Root bez auth zwraca mape
|
|
|
|
|
|
endpointow: `documents, correspondents, document_types, tags, storage_paths,
|
|
|
|
|
|
custom_fields, mail_accounts, mail_rules, workflows, tasks, users, groups, logs,
|
|
|
|
|
|
share_links, config, saved_views`.
|
|
|
|
|
|
- **Chronione endpointy wymagaja auth** — `GET /api/documents/` bez tokenu → `401`.
|
|
|
|
|
|
Standardowy mechanizm Paperless: `Authorization: Token <token>` (DRF TokenAuth),
|
|
|
|
|
|
token generowany w UI (My Profile) albo `manage.py drf_create_token <user>`.
|
|
|
|
|
|
- **Nie ma dzis zadnego tokenu API.** Haslo z `PAPERLESS_ADMIN_PASSWORD` (env, wartosc
|
|
|
|
|
|
bootstrapowa) **juz nie dziala** — wedlug `docs/sessions/2026-07-10-paperless-deploy.md`
|
|
|
|
|
|
haslo konta `oskar` zostalo zmienione recznie przez `manage.py shell` po skonfigurowaniu
|
|
|
|
|
|
OIDC. Nie probowalem zgadywac/resetowac hasla (poza zakresem read-only recon) —
|
|
|
|
|
|
**generowanie tokenu API to pierwszy krok implementacji**, nie recon (patrz decyzja #5).
|
|
|
|
|
|
- Paginacja: standardowy DRF pattern (`count/next/previous/results`, `?page_size=`) — wedlug
|
|
|
|
|
|
dokumentacji Paperless-ngx; nie zweryfikowane bezposrednio (401 zablokowal body), ale to
|
|
|
|
|
|
utrwalony, udokumentowany kontrakt API Paperless-ngx, nie zgadywanie.
|
|
|
|
|
|
|
|
|
|
|
|
### 1.4 Model embeddingowy — bge-m3
|
|
|
|
|
|
|
|
|
|
|
|
Decyzja "bge-m3" jest juz **zamknieta w `kb-00-overview.md`** ("multilingual, dlugi kontekst,
|
|
|
|
|
|
lepszy pod polski niz multilingual-e5"). Zweryfikowalem, ze to nadal trafny wybor (nie zgadywanie):
|
|
|
|
|
|
|
|
|
|
|
|
| Cecha | Wartosc | Zrodlo |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| Parametry | 568M (XLM-RoBERTa base) | ollama.com/library/bge-m3, HF BAAI/bge-m3 |
|
|
|
|
|
|
| Rozmiar na dysku (Ollama) | 1.2 GB | ollama.com/library/bge-m3 |
|
|
|
|
|
|
| Wymiar wektora (dense) | **1024** | HF BAAI/bge-m3 model card |
|
|
|
|
|
|
| Max kontekst | **8192 tokenow** | ollama.com/library/bge-m3, HF |
|
|
|
|
|
|
| Jezyki | 100+ working languages, w tym polski | ollama.com/library/bge-m3 |
|
|
|
|
|
|
| Tryby | dense + sparse (lexical) + multi-vector (ColBERT) w jednym modelu | HF BAAI/bge-m3 |
|
|
|
|
|
|
|
|
|
|
|
|
**Nie jest pobrany na SOLARII.** `curl http://solaria:11434/api/tags` (przez Tailscale z PIHA)
|
|
|
|
|
|
pokazuje wylacznie: `qwen2.5-coder:14b, qwen3-coder:30b, deepseek-coder:latest, deepcoder:14b` —
|
|
|
|
|
|
same modele coder, zero embeddingowych. `ollama pull bge-m3` (1.2GB) nie zostal wykonany
|
|
|
|
|
|
(poza zakresem recon — nic nie pobieram).
|
|
|
|
|
|
|
|
|
|
|
|
SOLARIA: RTX 4070, **12GB VRAM** (`hosts/solaria/capabilities.yaml`) — 1.2GB modelu mieści
|
|
|
|
|
|
się bez trudu obok/zamiast modeli coder; nie jest to wąskie gardło.
|
|
|
|
|
|
|
|
|
|
|
|
### 1.5 kb-postgres@PIHA — zasoby
|
|
|
|
|
|
|
|
|
|
|
|
- Kontener ograniczony do `mem_limit: 1g`, `shared_buffers=256MB`,
|
|
|
|
|
|
`maintenance_work_mem=64MB`, `max_connections=30` — celowo ciasno (Pi 5 dzieli RAM z HA/Immich).
|
|
|
|
|
|
- Dane obecnie: 202 MB (`du -sh` na wolumenie) dla 225k kopert maili. Zapas na PIHA:
|
|
|
|
|
|
`df -h /` → 29 GB wolne z 58 GB. Nie jest to problem dla pilota (189 dok × kilkanascie
|
|
|
|
|
|
chunkow ≈ kilka tysiecy wektorow × 4KB/wektor ≈ pojedyncze MB), ale
|
|
|
|
|
|
`maintenance_work_mem=64MB` (budowa indeksu HNSW) bedzie trzeba zrewidowac przy docelowej
|
|
|
|
|
|
skali (miliony chunkow po dolozeniu maili) — nie w tej fazie.
|
|
|
|
|
|
|
|
|
|
|
|
### 1.6 `packages/kb-mail` — czy da sie reuzyc?
|
|
|
|
|
|
|
|
|
|
|
|
Tak, **bezposrednio, bez nowego pakietu**:
|
|
|
|
|
|
|
|
|
|
|
|
- `envelope.py` (`Envelope` dataclass) i `db.py` (`insert_envelope`/`get_envelope`) sa juz
|
|
|
|
|
|
**w pelni zrodlo-agnostyczne** — zero logiki specyficznej dla maila. Dzialaja identycznie
|
|
|
|
|
|
dla `source='paperless'`.
|
|
|
|
|
|
- `archive.py` (`save_eml`) jest **specyficzny dla .eml** i **niepotrzebny** dla adaptera
|
|
|
|
|
|
Paperless — Paperless jest REFERENCJA (`raw_ref` = doc-id, nie kopiujemy bajtow), wiec nie
|
|
|
|
|
|
ma czego archiwizowac lokalnie.
|
|
|
|
|
|
- Wniosek: **nie tworzymy `packages/kb-documents`**. Adapter Paperless w
|
|
|
|
|
|
`jobs/documents-ingest/` robi `pip install /packages/kb-mail/` i importuje
|
|
|
|
|
|
`Envelope`/`insert_envelope` wprost. Gdy przyjdzie faza Nextcloud (KOPIA, nie referencja),
|
|
|
|
|
|
wtedy `archive.py` bedzie potrzebny do rozszerzenia — ale to inny adapter, nie ten.
|
|
|
|
|
|
|
|
|
|
|
|
### 1.7 Graf encji (warstwa 3) — stan
|
|
|
|
|
|
|
|
|
|
|
|
`grep -r "entity_link|entity_graph|warstwa.3|entity_resolution"` w calym repo → trafienia
|
|
|
|
|
|
wylacznie w dokumentacji (kb-00, kb-02, modul 5) i w kolumnie `entities` samej koperty.
|
|
|
|
|
|
**Zero kodu, zero tabeli.** Warstwa 3 zaczyna sie od zera w tej fazie — zgodnie z zasada
|
|
|
|
|
|
kb-00 #7 ("kontrakt encji definiujemy wczesnie, przy 2 zrodlach; cienka warstwa 4 po
|
|
|
|
|
|
drugim zrodle).
|
|
|
|
|
|
|
|
|
|
|
|
### 1.8 Koperty mailowe — brak naglowkow (potwierdzone)
|
|
|
|
|
|
|
|
|
|
|
|
Sprawdzone bezposrednio w `jobs/gmail-bulk-import/src/gmail_bulk_import/importer.py`:
|
|
|
|
|
|
`_parse_attachments()` (L84) buduje jedyne obiekty ktore trafiaja do `entities[]` —
|
|
|
|
|
|
wylacznie `{"type": "attachment", filename, content_type, size, sha256}`. Funkcja
|
|
|
|
|
|
`_parse_date()` (L70) parsuje `Date:` wylacznie po to, by wypelnic `envelope.ts` — wartosc
|
|
|
|
|
|
naglowka NIE trafia do `entities`. **Zero from/to/cc/delivered_to/subject gdziekolwiek w
|
|
|
|
|
|
bazie.** Te dane istnieja wylacznie wewnatrz zarchiwizowanych plikow `.eml` (27 GB,
|
|
|
|
|
|
225 030 plikow) — nigdy nie zostaly wyciagniete do struktury zapytywalnej.
|
|
|
|
|
|
|
|
|
|
|
|
To jest realny problem semantyczny, nie tylko brak wygody: Oskar ma wiele adresow/aliasow
|
|
|
|
|
|
na tym samym koncie Gmail. Bez `to`/`delivered_to` nie da sie odpowiedziec na pytania typu
|
|
|
|
|
|
"czy ta polisa jest na mnie czy na zone" ani odroznic poczty firmowej od prywatnej, gdy
|
|
|
|
|
|
oba konteksty ladowaly na rozne aliasy tej samej skrzynki. Naprawiane w §5 (backfill).
|
|
|
|
|
|
|
|
|
|
|
|
### 1.9 Cross-source: dowod juz istnieje w danych (odkrycie, nie projekt od zera)
|
|
|
|
|
|
|
|
|
|
|
|
`jobs/documents-ingest/` (faza 1, JUZ DZIALA) zostawia po sobie
|
|
|
|
|
|
`/opt/homelab/data/documents-ingest/registry.json`, klucz = **sha256 zalacznika** (z
|
|
|
|
|
|
manifestu mailowego `entities[]`), wartosc zawiera m.in. `envelope_id` (Message-ID maila
|
|
|
|
|
|
zrodlowego) i `consume_name` (nazwa pliku wrzuconego do `consume/`). Zweryfikowane na zywo:
|
|
|
|
|
|
|
|
|
|
|
|
- Registry ma dzis **185 wpisow**, wszystkie `consume_name` **unikalne** (185/185) —
|
|
|
|
|
|
bezpieczny klucz join.
|
|
|
|
|
|
- **KRYTYCZNE odkrycie:** `documents_document.checksum` / `.archive_checksum` w Paperlessie
|
|
|
|
|
|
to **MD5** (32 znaki hex — sprawdzone bezposrednio w bazie), a registry.json jest
|
|
|
|
|
|
kluczowany **SHA256** (64 znaki) tego samego pliku. Rozne algorytmy — **nie da sie ich
|
|
|
|
|
|
bezposrednio porownac**. To by unieważniło plan "dopasuj po checksumie", gdyby ktos
|
|
|
|
|
|
zaproponowal go bez sprawdzenia.
|
|
|
|
|
|
- Za to `documents_document.original_filename` (nazwa pliku jaki wszedl przez `consume/`)
|
|
|
|
|
|
**dokladnie odpowiada** `registry[sha256].consume_name` — zweryfikowane join na 8 losowych
|
|
|
|
|
|
przykladach (np. `2026-06-03_Regulamin_PL-ENG.pdf`). To jest **deterministyczny,
|
|
|
|
|
|
bezstratny klucz** miedzy Paperless a rejestrem faktury-1, bez zadnej heurystyki tekstowej.
|
|
|
|
|
|
- Dokumenty spoza tej sciezki (np. `polisa 920008969228.pdf`, `fll-challenge-...` — bez
|
|
|
|
|
|
prefiksu daty charakterystycznego dla `documents-ingest`) nie maja wpisu w registry —
|
|
|
|
|
|
to sa dokumenty dodane recznie/wczesniej, poprawnie NIE beda mialy cross-source linku.
|
|
|
|
|
|
|
|
|
|
|
|
**Wniosek:** cross-source link mail↔dokument nie wymaga zadnej nowej infrastruktury ani
|
|
|
|
|
|
entity-resolution — dane juz sa, trzeba je tylko odczytac i zapisac w kopercie dokumentu
|
|
|
|
|
|
przy jego tworzeniu (§4).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Otwarte decyzje dla Oskara
|
|
|
|
|
|
|
|
|
|
|
|
### Decyzja 1 — Gdzie wektory: kolumna w `envelope` czy osobna tabela?
|
|
|
|
|
|
|
|
|
|
|
|
**Rekomendacja: osobna tabela `document_chunk` (1:N do envelope).**
|
|
|
|
|
|
|
|
|
|
|
|
Uzasadnienie: sredni dokument (~22k znakow) jest blisko/ponad granice sensownego
|
|
|
|
|
|
pojedynczego embeda, a najwieksze (360k znakow / 114 stron) drastycznie ja przekraczaja —
|
|
|
|
|
|
**chunking jest obowiazkowy**, nie opcjonalny. Koperta jest zamrozona i ma byc addytywna
|
|
|
|
|
|
(kb-00 zasada #2) — N-wartosciowa relacja nie pasuje do plaskiej kolumny. Dodatkowo:
|
|
|
|
|
|
mail-indexer (przyszly etap) bedzie mial dokladnie ten sam problem (dlugie watki mailowe) —
|
|
|
|
|
|
projektujemy jedna tabele chunkow reuzywalna dla obu zrodel, zamiast osobnych rozwiazan.
|
|
|
|
|
|
|
|
|
|
|
|
### Decyzja 2 — Model embeddingowy
|
|
|
|
|
|
|
|
|
|
|
|
**bge-m3 pozostaje trafny wybor** (patrz §1.4) — nie ma potrzeby zmiany zamknietej decyzji
|
|
|
|
|
|
z kb-00. Jedyna otwarta czynnosc: `ollama pull bge-m3` na SOLARII — to akcja implementacyjna,
|
|
|
|
|
|
nie recon, wiec jej nie wykonalem.
|
|
|
|
|
|
|
|
|
|
|
|
### Decyzja 3 — Chunking
|
|
|
|
|
|
|
|
|
|
|
|
**Rekomendacja: ~500–800 tokenow/chunk (≈2000–3200 znakow polskiego tekstu), overlap
|
|
|
|
|
|
~100–150 tokenow, split preferujacy granice akapitow/pustych linii, twardy fallback
|
|
|
|
|
|
znakowy gdy akapit przekracza limit.**
|
|
|
|
|
|
|
|
|
|
|
|
Uzasadnienie:
|
|
|
|
|
|
- bge-m3 (limit 8192 tok) technicznie zmiesci wiekszosc dokumentow (sredni ~22k znakow ≈
|
|
|
|
|
|
5.5–6k tok) w JEDNYM embedzie — ale maly chunk daje lepsza precyzje retrievalu dla pytan
|
|
|
|
|
|
punktowych ("jaka suma ubezpieczenia w polisie GA431JA") niz jeden wektor uśredniajacy
|
|
|
|
|
|
caly dokument.
|
|
|
|
|
|
- Najwieksze dokumenty (360k znakow / 114 stron) i tak **wymagaja** twardego chunkingu — nie
|
|
|
|
|
|
ma opcji embedowania calosci.
|
|
|
|
|
|
- Brak page-break markerow w tekscie (§1.2) wyklucza czysty split po stronach — dzielimy po
|
|
|
|
|
|
strukturze tekstu (akapity/puste linie), nie po metadanej `page_count`.
|
|
|
|
|
|
|
|
|
|
|
|
### Decyzja 4 — Cross-source link — **ROZSTRZYGNIETA przez Oskara (v2)**
|
|
|
|
|
|
|
|
|
|
|
|
~~Pierwotna rekomendacja (v1) opierala sie o `correspondent` z Paperlessa — Oskar to
|
|
|
|
|
|
odrzucil, slusznie: pole jest puste (recon to wykazalo), a nawet gdyby nie bylo, to tylko
|
|
|
|
|
|
ZGADYWANIE Paperlessa "kto wystawil dokument", nie fakt.~~
|
|
|
|
|
|
|
|
|
|
|
|
**Decyzja: link mail↔dokument idzie po deterministycznej sciezce danych, nie po polu
|
|
|
|
|
|
`correspondent`.** Sciezka (opisana w §1.9): `envelope(gmail).entities[attachment].sha256`
|
|
|
|
|
|
→ `registry.json[sha256]` → `{envelope_id, consume_name}` → JOIN
|
|
|
|
|
|
`documents_document.original_filename = consume_name` → `paperless doc_id`. Wynik zapisany
|
|
|
|
|
|
w kopercie DOKUMENTU jako `entities[type=source_mail]` (ksztalt w §4.2).
|
|
|
|
|
|
|
|
|
|
|
|
`correspondent`/`tag` z Paperlessa **zostaja w entities jako metadane informacyjne**
|
|
|
|
|
|
(moga sie kiedys wypelnic — auto-matcher albo reczne tagowanie), ale **przestaja byc
|
|
|
|
|
|
mechanizmem cross-source** — nic w pipeline nie polega juz na tym, ze sa niepuste.
|
|
|
|
|
|
|
|
|
|
|
|
**Konsekwencja dla schematu (§3 zaktualizowane):** tabele `entity`/`entity_link` (graf
|
|
|
|
|
|
encji person/organizacja) **nie sa wymagane do domkniecia kryterium ukonczenia modulu 5**
|
|
|
|
|
|
("min. jeden cross-source link zademonstrowany") — ten dowod daje wprost pole
|
|
|
|
|
|
`source_mail` w kopercie dokumentu, bez potrzeby modelowania tozsamosci. Tabele zostaja w
|
|
|
|
|
|
planie jako **przyszla praca, odlozona** (prawdziwa identity-resolution: gdy `correspondent`
|
|
|
|
|
|
kiedys sie wypelni, gdy dojda nadawcy mailowi jako encje, gdy dojda twarze ze zdjec z
|
|
|
|
|
|
Immich) — nie blokuja pilota. To realne uproszczenie zakresu tej fazy, nie kompromis.
|
|
|
|
|
|
|
|
|
|
|
|
### Decyzja 5 — Konto serwisowe dla API Paperless
|
|
|
|
|
|
|
|
|
|
|
|
Trzeba wygenerowac token API — **to mutuje baze Paperless (jeden rekord `authtoken_token`)**,
|
|
|
|
|
|
wiec nie zrobilem tego w ramach read-only recon. Do decyzji: token na istniejacym koncie
|
|
|
|
|
|
`oskar` (prosciej) czy dedykowany user tylko-do-odczytu `kb-ingest` (czystsze rozdzielenie
|
|
|
|
|
|
uprawnien, ale trzeba sprawdzic czy Paperless ma granularne read-only role). **Rekomendacja:
|
|
|
|
|
|
dedykowany user readonly jesli Paperless to latwo wspiera** — do zweryfikowania w
|
|
|
|
|
|
pierwszym kroku implementacji, nie blokuje planu.
|
|
|
|
|
|
|
|
|
|
|
|
### Decyzja 6 — Filtr selektywnosci przed embedowaniem
|
|
|
|
|
|
|
|
|
|
|
|
§1.2 pokazal, ze aktualna zawartosc Paperless zawiera szum niezwiazany z celem KB (crypto
|
|
|
|
|
|
T&C, PDFy z konkursu robotycznego) — pozostalosc po size-only filtrze fazy-1. **Rekomendacja:
|
|
|
|
|
|
na pilot embedowac WSZYSTKO** (189 dok to tani eksperyment, latwo przebudowac indeks —
|
|
|
|
|
|
kb-00 zasada #1: indeks jest odtwarzalny/wyrzucalny), **ale zaprojektowac filtr jako
|
|
|
|
|
|
parametr adaptera od razu** (np. blacklist po `document_type`/tagach gdy beda ustawione,
|
|
|
|
|
|
lub po korespondencie), zeby nie trzeba bylo tego retrofitowac przy skalowaniu do tysiecy
|
|
|
|
|
|
dokumentow z faktycznego batcha 70k zalacznikow.
|
|
|
|
|
|
|
|
|
|
|
|
### Decyzja 7 — Backfill naglowkow: osobny job czy rozszerzenie `gmail-bulk-import`? (NOWA, v2)
|
|
|
|
|
|
|
|
|
|
|
|
**Rekomendacja: osobny jednorazowy job**, np. `jobs/gmail-header-backfill/`, nie
|
|
|
|
|
|
rozszerzenie `gmail-bulk-import`.
|
|
|
|
|
|
|
|
|
|
|
|
Uzasadnienie:
|
|
|
|
|
|
- `gmail-bulk-import` ma semantyke **INSERT** (nowe koperty z mbox/Takeout) — juz
|
|
|
|
|
|
wykonany, historyczny, uzasadniony przez wlasny modul/etap. Backfill ma semantyke
|
|
|
|
|
|
**UPDATE** istniejacych 225 030 wierszy — inna operacja, inne ryzyko (dotyka danych juz
|
|
|
|
|
|
w produkcyjnej bazie), inny cykl zycia.
|
|
|
|
|
|
- Precedens w repo juz istnieje: `jobs/documents-ingest/` zostal swiadomie wydzielony jako
|
|
|
|
|
|
NOWY job mimo ze czyta ten sam `.eml`-archiwum co `gmail-bulk-import` — bo robi cos
|
|
|
|
|
|
innego (ekstrakcja, nie import). Ta sama logika stosuje sie tu.
|
|
|
|
|
|
- Osobny job = latwiej o dry-run/--apply, `--limit`, wznawialnosc i testy jednostkowe
|
|
|
|
|
|
bez ryzyka regresji w kodzie ktory juz odpowiada za 225k-wierszowy bulk import.
|
|
|
|
|
|
|
|
|
|
|
|
Alternatywa (odrzucona): dopisanie `--backfill-headers` do `gmail-bulk-import` — mniej
|
|
|
|
|
|
kodu, ale miesza dwie odrebne operacje w jednym CLI i utrudnia code review/testy. Do
|
|
|
|
|
|
przyklepania przez Oskara, ale rekomendacja jest jasna.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Proponowany schemat SQL (addytywny, nowe migracje)
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- services/kb-postgres/init/002_chunks.sql
|
|
|
|
|
|
-- Chunk-level embeddings, 1:N do envelope. Addytywne — envelope (001) nietkniete.
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS document_chunk (
|
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
|
envelope_id TEXT NOT NULL REFERENCES envelope(id) ON DELETE CASCADE,
|
|
|
|
|
|
chunk_index INT NOT NULL, -- kolejnosc w obrebie envelope, 0-based
|
|
|
|
|
|
text TEXT NOT NULL,
|
|
|
|
|
|
embedding VECTOR(1024), -- bge-m3 dense; NULL dopoki nie zembedowany
|
|
|
|
|
|
model TEXT NOT NULL, -- np. 'bge-m3' — sledzenie re-indexu przy zmianie modelu
|
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
|
|
UNIQUE (envelope_id, chunk_index)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS document_chunk_envelope_idx ON document_chunk (envelope_id);
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS document_chunk_embedding_hnsw_idx
|
|
|
|
|
|
ON document_chunk USING hnsw (embedding vector_cosine_ops);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`model` na `document_chunk` istnieje wylacznie po to, by re-indeks przy lepszym modelu
|
|
|
|
|
|
(kb-00 zasada #1) byl trywialny: nowy embed = nowy wiersz z innym `model`, stary do
|
|
|
|
|
|
skasowania po weryfikacji, bez migracji schematu.
|
|
|
|
|
|
|
|
|
|
|
|
### ODLOZONE (v2): `entity` / `entity_link`
|
|
|
|
|
|
|
|
|
|
|
|
W v1 tego planu byla tu tabela grafu encji (`entity` + `entity_link`) jako mechanizm
|
|
|
|
|
|
cross-source. **Po decyzji 4 (v2) nie jest juz potrzebna do domkniecia modulu 5** — dowod
|
|
|
|
|
|
cross-source daje bezposrednio `entities[type=source_mail]` w kopercie dokumentu (§4.2), bez
|
|
|
|
|
|
modelowania tozsamosci osoby/firmy. Schemat zostaje udokumentowany tutaj jako **przyszla
|
|
|
|
|
|
praca** (prawdziwa identity-resolution — gdy `correspondent` w Paperless kiedys sie wypelni,
|
|
|
|
|
|
gdy dojda nadawcy mailowi jako encje, gdy dojda twarze z Immich), nie jest czescia planu
|
|
|
|
|
|
implementacji tej fazy:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- ODLOZONE — nie czesc migracji fazy 2, tylko szkic na przyszlosc
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS entity (
|
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
|
type TEXT NOT NULL, -- 'person' | 'organization' | ...
|
|
|
|
|
|
canonical_name TEXT NOT NULL
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS entity_link (
|
|
|
|
|
|
id BIGSERIAL PRIMARY KEY,
|
|
|
|
|
|
entity_id BIGINT NOT NULL REFERENCES entity(id) ON DELETE CASCADE,
|
|
|
|
|
|
envelope_id TEXT NOT NULL REFERENCES envelope(id) ON DELETE CASCADE,
|
|
|
|
|
|
role TEXT NOT NULL, -- 'correspondent' | 'sender' | ...
|
|
|
|
|
|
raw_value TEXT, -- surowa wartosc jak wystapila w zrodle
|
|
|
|
|
|
UNIQUE (entity_id, envelope_id, role)
|
|
|
|
|
|
);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. Ksztalt `entities[]` — ujednolicony miedzy zrodlami
|
|
|
|
|
|
|
|
|
|
|
|
Oba zrodla uzywaja **tej samej konwencji**: `entities` to plaska lista obiektow
|
|
|
|
|
|
roznotypowych, kazdy z kluczem `"type"` (wzorzec juz ustalony przez `gmail-bulk-import`'s
|
|
|
|
|
|
`{"type": "attachment", ...}`). Nowosc w v2: wspolne slownictwo typow rozszerzone o
|
|
|
|
|
|
naglowki (mail) i link zrodlowy (dokument), oba dodane **addytywnie obok** istniejacych
|
|
|
|
|
|
wpisow — zaden istniejacy element listy nie jest ruszany ani restrukturyzowany.
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 Koperta MAILOWA (`source='gmail'`) — po backfillu
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
[
|
|
|
|
|
|
{"type": "attachment", "filename": "...", "content_type": "application/pdf",
|
|
|
|
|
|
"size": 143514, "sha256": "00866aa3..."},
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "headers",
|
|
|
|
|
|
"from": {"name": "WARTA", "address": "no-reply@warta.pl"},
|
|
|
|
|
|
"to": [{"name": "...", "address": "oskar@gmail.com"}],
|
|
|
|
|
|
"cc": [],
|
|
|
|
|
|
"delivered_to": ["oskar+alias@gmail.com"],
|
|
|
|
|
|
"subject": "Twoja polisa OC/AC",
|
|
|
|
|
|
"date_raw": "Mon, 9 Jun 2026 12:34:56 +0200"
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Uwagi projektowe:
|
|
|
|
|
|
|
|
|
|
|
|
- `from`/`to`/`cc` **parsowane** (`email.utils.getaddresses`) do `{name, address}`, nie
|
|
|
|
|
|
surowy string — ulatwia przyszla entity-resolution (§ decyzja 4) bez re-parsowania.
|
|
|
|
|
|
`to`/`cc` to listy (wielu adresatow mozliwych); `from` pojedynczy obiekt (RFC dopuszcza
|
|
|
|
|
|
wiele, w praktyce prawie zawsze jeden — jesli wielu, bierzemy pierwszy i logujemy).
|
|
|
|
|
|
- `delivered_to` to **lista surowych stringow**, NIE parsowana — naglowek `Delivered-To`
|
|
|
|
|
|
moze wystapic wielokrotnie (hop-by-hop), a to wlasnie te wartosci mowia "ktory alias
|
|
|
|
|
|
Oskara odebral wiadomosc" (motywacja z §1.8). Zachowujemy wszystkie wystapienia.
|
|
|
|
|
|
- `date_raw` to surowy string naglowka `Date:` (dla porownania/debug) — `envelope.ts`
|
|
|
|
|
|
juz ma sparsowana wersje UTC z importu (`_parse_date`), wiec to nie jest duplikacja
|
|
|
|
|
|
zrodla prawdy, tylko provenance.
|
|
|
|
|
|
- Parsowanie przez `email.policy.default` (dekoduje RFC 2047 encoded-words do realnego
|
|
|
|
|
|
Unicode) — ta sama decyzja co `documents-ingest` juz podjal dla MIME-parts (patrz jego
|
|
|
|
|
|
README, sekcja o rozbieznosci compat32 vs default).
|
|
|
|
|
|
|
|
|
|
|
|
### 4.2 Koperta DOKUMENTU (`source='paperless'`)
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
[
|
|
|
|
|
|
{"type": "content", "text": "<pelny OCR-text>"},
|
|
|
|
|
|
{"type": "correspondent", "name": null},
|
|
|
|
|
|
{"type": "tag", "name": "..."},
|
|
|
|
|
|
{"type": "filename", "value": "..."},
|
|
|
|
|
|
{"type": "content_type", "value": "application/pdf"},
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "source_mail",
|
|
|
|
|
|
"envelope_id": "CABtrY-KrCCZ0EgSDkg8xf-2pvkaEyxo1Y12Pfi=p=A3R9NFQKw@mail.gmail.com",
|
|
|
|
|
|
"attachment_sha256": "00866aa304e65f90ef3538fd9994a6e9d8af6a76480543698c42dd7ea17e016d",
|
|
|
|
|
|
"attachment_filename": "31657539_13049.pdf"
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`source_mail` — **obecny tylko gdy jest deterministyczny match** (§1.9: join
|
|
|
|
|
|
`documents_document.original_filename` = `registry.json[sha256].consume_name`). Dla
|
|
|
|
|
|
dokumentow dodanych recznie/spoza pipeline'u faktury-1 (np. `polisa 920008969228.pdf`,
|
|
|
|
|
|
`fll-challenge-...`) ten wpis **nie wystepuje** — brak linku jest poprawnym stanem, nie
|
|
|
|
|
|
bledem. `correspondent`/`tag` zostaja jako metadane informacyjne (§ decyzja 4) — nie sa juz
|
|
|
|
|
|
mechanizmem cross-source, ale nie ma powodu ich wyrzucac (tanie, Paperless i tak je daje).
|
|
|
|
|
|
|
|
|
|
|
|
Przykladowe zapytanie po dowod cross-source (mail -> dokument):
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
SELECT e_mail.id AS mail_id, e_mail.entities, e_doc.id AS doc_id
|
|
|
|
|
|
FROM envelope e_doc
|
|
|
|
|
|
JOIN LATERAL jsonb_array_elements(e_doc.entities) AS link ON link->>'type' = 'source_mail'
|
|
|
|
|
|
JOIN envelope e_mail ON e_mail.id = link->>'envelope_id'
|
|
|
|
|
|
WHERE e_doc.source = 'paperless';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.3 Adapter Paperless -> koperta (pelny mapping)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
source = 'paperless'
|
|
|
|
|
|
id = f"paperless:{document_id}" -- PREFIX wymagany: PK envelope.id jest globalny
|
|
|
|
|
|
-- plaski TEXT bez kolumny source w kluczu; Paperless
|
|
|
|
|
|
-- doc-id to male sekwencyjne inty (1,2,3...) —
|
|
|
|
|
|
-- bez prefiksu koliduja z kazdym przyszlym zrodlem
|
|
|
|
|
|
-- uzywajacym prostych ID (np. Nextcloud fileid).
|
|
|
|
|
|
ts = documents_document.created -- Paperless wykrywa date z tresci/nazwy pliku,
|
|
|
|
|
|
-- lepsze niz plikowy mtime (potwierdzone: dok #1
|
|
|
|
|
|
-- created=2020-04-21 zgadza sie z data polisy w OCR)
|
|
|
|
|
|
geo = NULL -- jak w specyfikacji modulu 5
|
|
|
|
|
|
raw_ref = str(document_id) -- REFERENCJA, zrodlem prawdy jest Paperless
|
|
|
|
|
|
entities = jak w §4.2, w tym `source_mail` gdy join z registry.json sie powiedzie
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Rozwiazanie `source_mail`: adapter wczytuje `registry.json` raz do pamieci, buduje indeks
|
|
|
|
|
|
`consume_name -> {envelope_id, sha256, filename}`, dla kazdego dokumentu z Paperless API
|
|
|
|
|
|
sprawdza `original_filename` w tym indeksie. O(1) per dokument, zero dodatkowych zapytan do
|
|
|
|
|
|
Paperless czy do bazy maili.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Backfill naglowkow mailowych (225 030 kopert)
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 Zakres i podejscie
|
|
|
|
|
|
|
|
|
|
|
|
Cel: dopisac `{"type": "headers", ...}` (§4.1) do kazdej z 225 030 istniejacych kopert
|
|
|
|
|
|
`source='gmail'`, bez ruszania istniejacych elementow `entities` (manifesty zalacznikow).
|
|
|
|
|
|
|
|
|
|
|
|
**Zrodlo danych: zarchiwizowane pliki `.eml`**, nie oryginalny plik Takeout/mbox.
|
|
|
|
|
|
Uzasadnienie: archiwum `.eml` (`/home/oskar/kb/mail/archive` na PIHA, 27 GB, 225 030
|
|
|
|
|
|
plikow) jest juz warstwa "surowa/niezmienna" (kb-00 zasada #1) i kazdy `.eml` niesie pelne
|
|
|
|
|
|
oryginalne naglowki — nie trzeba wracac do zrodlowego mboxa (ktory moze juz nie byc
|
|
|
|
|
|
dostepny/aktualny na dysku roboczym). To tez dokladnie ten sam wzorzec dostepu co
|
|
|
|
|
|
`documents-ingest` juz uzywa (`archive_root / row["raw_ref"]` -> `read_bytes()` ->
|
|
|
|
|
|
`email.message_from_bytes(raw, policy=email.policy.default)`).
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 Idempotencja i wznawialnosc
|
|
|
|
|
|
|
|
|
|
|
|
`entities` jest JSONB **tablica** — dopisanie nowego elementu to konkatenacja, nie
|
|
|
|
|
|
nadpisanie:
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
UPDATE envelope
|
|
|
|
|
|
SET entities = entities || $2::jsonb -- $2 = '[{"type": "headers", ...}]'::jsonb
|
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
|
AND NOT EXISTS (
|
|
|
|
|
|
SELECT 1 FROM jsonb_array_elements(entities) e WHERE e->>'type' = 'headers'
|
|
|
|
|
|
);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Warunek `NOT EXISTS` w `WHERE` czyni operacje **bezpiecznie powtarzalna**: przerwany
|
|
|
|
|
|
backfill (crash, restart) po prostu pomija wiersze juz zaktualizowane przy ponownym
|
|
|
|
|
|
uruchomieniu — dokladnie ten sam wzorzec `ON CONFLICT DO NOTHING` co reszta pipeline'u, tylko
|
|
|
|
|
|
wyrazony przez `WHERE NOT EXISTS` (bo to `UPDATE`, nie `INSERT`).
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3 Wydajnosc (szacunek, nie pomiar)
|
|
|
|
|
|
|
|
|
|
|
|
225 030 plikow, srednio ~124 KB kazdy (27 GB / 225 030 — z faktycznych liczb importu w
|
|
|
|
|
|
`kb-00-overview.md`). Operacja per wiersz: `open + read + parse headers (tanie, tylko
|
|
|
|
|
|
naglowki, nie cale MIME-walk po zalacznikach) + UPDATE`. Pelny bulk-import (parsowanie
|
|
|
|
|
|
calego mboxa **wlacznie z** MIME-walk zalacznikow i insertami) zajal ~29 min
|
|
|
|
|
|
(`kb-00-overview.md`, zmierzone). Backfill robi mniej pracy per-wiadomosc (tylko naglowki,
|
|
|
|
|
|
nie MIME-walk zalacznikow) ale placi za **osobne otwarcie 225k malych plikow** zamiast
|
|
|
|
|
|
strumieniowego czytania jednego mboxa — te dwa efekty czesciowo sie znosza. **Szacunek:
|
|
|
|
|
|
tego samego rzedu wielkosci, prawdopodobnie kilkadziesiat minut**, nie pomierzone
|
|
|
|
|
|
bezposrednio (nie odpalilem tego joba — nie istnieje jeszcze). Rekomendacja: batch UPDATE
|
|
|
|
|
|
(multi-row, nie jeden `UPDATE` na transakcje), `--limit`/`--offset` jak inne joby w repo,
|
|
|
|
|
|
zeby dalo sie uruchomic partiami i zweryfikowac progres bez czekania na cale 225k na raz.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Plan implementacji (kolejnosc)
|
|
|
|
|
|
|
|
|
|
|
|
1. **Migracja** `002_chunks.sql` na kb-postgres@PIHA (addytywna, nie rusza
|
|
|
|
|
|
`001_envelope.sql`). `003_entities.sql` **odlozona** (§3) — nie jest czescia tego kroku.
|
|
|
|
|
|
2. **`ollama pull bge-m3`** na SOLARII — zweryfikowac czas pobrania i realne zuzycie VRAM
|
|
|
|
|
|
przy pierwszym `ollama run`/embed call.
|
|
|
|
|
|
3. **Token API Paperless** — decyzja #5 wyzej (konto dedykowane vs `oskar`), wygenerowac
|
|
|
|
|
|
przez `manage.py drf_create_token` lub UI.
|
|
|
|
|
|
4. **`jobs/gmail-header-backfill/`** (decyzja #7) — nowy one-shot job: parsuje naglowki z
|
|
|
|
|
|
225 030 zarchiwizowanych `.eml`, dopisuje `{"type": "headers", ...}` (§4.1) do
|
|
|
|
|
|
`envelope.entities` przez idempotentny `UPDATE ... WHERE NOT EXISTS` (§5.2). Wykonac
|
|
|
|
|
|
PRZED adapterem Paperless, bo krok 6 (dowod cross-source) czyta te naglowki.
|
|
|
|
|
|
5. **Rozbudowa `jobs/documents-ingest/`** o adapter Paperless→koperta: fetch (paginowany
|
|
|
|
|
|
`GET /api/documents/`), zaladowanie `registry.json` do indeksu `consume_name -> wpis`,
|
|
|
|
|
|
mapping wg §4.2–4.3 (w tym `source_mail` gdy join sie powiedzie),
|
|
|
|
|
|
`insert_envelope` (reuzyte z `packages/kb-mail`). Idempotentnie —
|
|
|
|
|
|
`ON CONFLICT (id) DO NOTHING` juz jest w `kb_mail.db.insert_envelope`.
|
|
|
|
|
|
6. **Chunking + embed job** — nowy modul (rozwazyc czy zyje w `jobs/documents-ingest/` czy
|
|
|
|
|
|
jako osobny `jobs/kb-indexer/` reuzywalny tez dla maili pozniej — rekomendacja: zaczac w
|
|
|
|
|
|
`documents-ingest`, wydzielic gdy mail-indexer bedzie realny, nie przed czasem).
|
|
|
|
|
|
Pipeline: `entities[content] -> chunk (§ decyzja 3) -> POST /api/embeddings (ollama,
|
|
|
|
|
|
model=bge-m3) -> INSERT document_chunk`.
|
|
|
|
|
|
7. **Pilot na 189 dok** — uruchomic, zweryfikowac jakosciowo retrieval (recznie, kilka
|
|
|
|
|
|
zapytan przez SQL `ORDER BY embedding <=> query_vector`).
|
|
|
|
|
|
8. **Weryfikacja dowodu cross-source** — po kroku 5, policzyc ile dokumentow dostalo
|
|
|
|
|
|
`source_mail` (oczekiwane: ~185, wedlug dzisiejszego stanu registry.json), zapytaniem
|
|
|
|
|
|
z §4.2 pokazac jeden konkretny przyklad mail↔dokument koniec-do-konca. To jest dowod
|
|
|
|
|
|
wymagany przez kryterium ukonczenia modulu 5 — bez recznego tagowania w UI.
|
|
|
|
|
|
9. **Testy jednostkowe** (wzorzec `jobs/gmail-bulk-import/tests/`) — dla kazdego nowego/
|
|
|
|
|
|
zmienionego joba: mapping Paperless→koperta, parsowanie naglowkow (w tym multi-`Delivered-To`,
|
|
|
|
|
|
RFC 2047 encoded-words), join `source_mail`, chunking (granice, overlap, dokument
|
|
|
|
|
|
pusty/za dlugi), idempotencja UPDATE-backfillu.
|
|
|
|
|
|
10. **Definition of Done** (CLAUDE.md): `docker build` + smoke run + `pytest` przed
|
|
|
|
|
|
jakimkolwiek deployem/commitem.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. Wydajnosc
|
|
|
|
|
|
|
|
|
|
|
|
- **189 dokumentow, ~22k znakow sredniej** → przy chunkach ~600 tok / ~2400 znakow +
|
|
|
|
|
|
overlap 150 tok: srednio ~10 chunkow/dok dla typowych dokumentow, ~150 chunkow dla
|
|
|
|
|
|
najwiekszego (360k znakow/114 stron). Szacunkowo **~2000–3000 chunkow lacznie** dla
|
|
|
|
|
|
calego pilota.
|
|
|
|
|
|
- **bge-m3 na RTX 4070**: maly model (568M, 1.2GB) — inferencja embeddingu w batchu to
|
|
|
|
|
|
rzedu dziesiatek-set chunkow/s. Caly pilot (2–3k chunkow) → **rzedu minut**, nie wymaga
|
|
|
|
|
|
specjalnego batchowania/partii.
|
|
|
|
|
|
- **Skala docelowa (70k zalacznikow z maili)**: modul 5 faza-1 to swiadomie **probka, nie
|
|
|
|
|
|
bulk** (`jobs/documents-ingest/README.md` — decyzja architektoniczna). Realny wolumen
|
|
|
|
|
|
ktory trafi do embeddingu zalezy od (a) throughput OCR-workera na SOLARII (modul 3) —
|
|
|
|
|
|
**to jest waskie gardlo skalowania, nie embedding** — oraz (b) filtra selektywnosci
|
|
|
|
|
|
(decyzja #6). Sam embedding bge-m3 nie bedzie bottleneckiem nawet przy tysiacach
|
|
|
|
|
|
dokumentow.
|
|
|
|
|
|
- **kb-postgres@PIHA**: budzet 1GB / `maintenance_work_mem=64MB` wystarcza na budowe HNSW
|
|
|
|
|
|
dla pilotowych 2–3k wektorow. Przy docelowej skali (miliony chunkow po dolozeniu maili)
|
|
|
|
|
|
ten limit trzeba bedzie zrewidowac — **poza zakresem tej fazy**, ale warto zanotowac w
|
|
|
|
|
|
backlogu jako przyszly checkpoint operacyjny.
|
|
|
|
|
|
- **Backfill naglowkow (225 030 kopert)** — szacunek jest osobny, patrz §5.3: rzedu
|
|
|
|
|
|
dziesiatek minut, dominowane przez I/O otwarcia 225k malych plikow, nie przez sam
|
|
|
|
|
|
parsing naglowkow (tani). Nie mierzone bezposrednio (job nie istnieje jeszcze).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. Podsumowanie dla Oskara
|
|
|
|
|
|
|
|
|
|
|
|
**Co jest:** Paperless dziala (189 dok, 4.2M znakow OCR), koperta+pgvector-extension stoi,
|
|
|
|
|
|
`packages/kb-mail` reuzywalny bez zmian dla adaptera Paperless, **dowod cross-source juz
|
|
|
|
|
|
lezy w danych** (registry.json z faktury-1 + `original_filename` w Paperless) — nie trzeba
|
|
|
|
|
|
go budowac, tylko odczytac i zapisac.
|
|
|
|
|
|
|
|
|
|
|
|
**Czego brakuje (do zrobienia w tej fazie):** tabela `document_chunk` (nie istnieje nigdzie
|
|
|
|
|
|
w KB — mail pillar tez jej nie ma), model bge-m3 (nie pobrany), token API Paperless (nie
|
|
|
|
|
|
wygenerowany), naglowki w 225 030 kopertach mailowych (dzis tylko manifest zalacznikow —
|
|
|
|
|
|
**krytyczna luka odkryta przez Oskara**, naprawiana backfillem w §5), jakikolwiek kod
|
|
|
|
|
|
chunking/embed/backfill (nie istnieje).
|
|
|
|
|
|
|
|
|
|
|
|
**7 otwartych decyzji** wypisanych w §2 (byla 6, doszla #7 — backfill: osobny job czy
|
|
|
|
|
|
rozszerzenie `gmail-bulk-import`). Decyzja #4 (cross-source) jest juz **rozstrzygnieta**
|
|
|
|
|
|
przez Oskara w tej korekcie — mechanizm to deterministyczny sha256/nazwa-pliku join, nie
|
|
|
|
|
|
`correspondent`. To jednoczesnie **upraszcza zakres**: tabele `entity`/`entity_link`
|
|
|
|
|
|
(graf encji) sa teraz odlozone, nie blokuja domkniecia modulu 5.
|
|
|
|
|
|
|
|
|
|
|
|
Nic nie zostalo zaimplementowane, zdeployowane, pobrane ani zacommitowane w ramach tego
|
|
|
|
|
|
recon — wlacznie z ta korekta (v2).
|