608 lines
32 KiB
Markdown
608 lines
32 KiB
Markdown
|
|
# Moduł 5, faza mailowa — treść maili w retrievalu (RECON + PLAN)
|
|||
|
|
|
|||
|
|
> Status: RECON ZAKOŃCZONY (2026-07-22), plan DO ZATWIERDZENIA. Zero kodu, zero
|
|||
|
|
> migracji, zero pełnych runów w ramach tego zadania — wyłącznie ten dokument
|
|||
|
|
> (recon obejmował pomiary read-only: próbka 600 .eml na PIHA, benchmark
|
|||
|
|
> `/api/embed` na SOLARII, `\d` + SELECT count na żywej bazie).
|
|||
|
|
>
|
|||
|
|
> Kontynuacja `05-faza4-plan.md` (faza 4: `packages/kb-retrieval` wydzielone,
|
|||
|
|
> serwis `kb-query` z UI działa na PIHA — „KB po raz pierwszy odpowiada przez
|
|||
|
|
> HTTP", 2026-07-22, `docs/sessions/2026-07-22.md`). Faza mailowa = odpowiedź na
|
|||
|
|
> feedback operatora z POC wyszukiwarki: **„mało danych, brak połączeń"**.
|
|||
|
|
> 225 030 kopert gmail ma dziś w bazie tylko nagłówki — treści leżą wyłącznie
|
|||
|
|
> w archiwum .eml na PIHA. Ta faza wprowadza treści maili do `document_chunk`
|
|||
|
|
> i udostępnia je w retrievalu. **To nadal wyszukiwarka, nie chat** — synteza,
|
|||
|
|
> Drive Takeout i backfill 70k załączników PDF pozostają poza zakresem (§11).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Stan faktyczny (recon, zweryfikowany w repo i na żywo 2026-07-22)
|
|||
|
|
|
|||
|
|
### 1.1 Baza — co jest, czego nie ma
|
|||
|
|
|
|||
|
|
Live (`docker exec kb-postgres psql` na PIHA):
|
|||
|
|
|
|||
|
|
| Miara | Wartość |
|
|||
|
|
|---|---|
|
|||
|
|
| `envelope` source='gmail' | **225 030** (100% z `entities[type=headers]` po backfillu) |
|
|||
|
|
| `envelope` source='paperless' | 191 |
|
|||
|
|
| `document_chunk` | 2 767 (2 627 aktywnych, 130 `duplicate`, 10 `ocr_junk`; 2 765 z embeddingiem) — **wszystkie paperless** |
|
|||
|
|
| `document_summary` | 317 (162 `claude-haiku-4-5` + 155 `gemma3:12b`) — **wszystkie paperless** |
|
|||
|
|
| Rozmiar bazy `kb` | **250 MB** |
|
|||
|
|
| Dysk PIHA `/home` | 410 GB, wolne **144 GB** |
|
|||
|
|
| RAM PIHA | 8 GB (dostępne ~3,8 GB; obok HA, Paperless, kb-postgres `mem_limit: 1g`) |
|
|||
|
|
|
|||
|
|
**Treści maili NIE ma w bazie.** `gmail-bulk-import` celowo pomijał inline
|
|||
|
|
`text/plain`/`text/html` (`_parse_attachments` robi `continue` na częściach
|
|||
|
|
tekstowych) — do DB trafił tylko manifest załączników, potem backfill dopisał
|
|||
|
|
nagłówki. Ta luka jest dokładnie tym, co faza mailowa wypełnia.
|
|||
|
|
|
|||
|
|
### 1.2 Archiwum .eml — jedyne źródło treści
|
|||
|
|
|
|||
|
|
- `/home/oskar/kb/mail/archive/gmail/YYYY/MM/<sanitized_message_id>.eml` —
|
|||
|
|
**225 057 plików, 27 GB** (append-only; 27 plików to duplikaty Message-ID,
|
|||
|
|
które w DB skleiły się przez `ON CONFLICT (id) DO NOTHING`).
|
|||
|
|
- Mapowanie koperta→plik: `envelope.raw_ref` (ścieżka względna) + archive root.
|
|||
|
|
Rejestrem jest sama kolumna — osobnego manifestu nie ma i nie potrzeba.
|
|||
|
|
- Rozkład rozmiarów plików (pełny skan `find -printf '%s'`): p50 = 24 KB,
|
|||
|
|
p90 = 116 KB, p99 = 1,9 MB, średnia 125 KB — ale to rozmiary Z załącznikami
|
|||
|
|
(base64), więc NIE nadają się do szacowania chunków. Stąd pomiar §1.3.
|
|||
|
|
|
|||
|
|
### 1.3 Zmierzony rozkład długości TREŚCI (próbka 600 losowych .eml, parse na PIHA)
|
|||
|
|
|
|||
|
|
Deterministyczna próbka 600 plików, parser stdlib `email` (policy.default z
|
|||
|
|
fallbackiem compat32 — wzorzec z backfillu), HTML→tekst własnym `HTMLParser`,
|
|||
|
|
0 błędów parsowania:
|
|||
|
|
|
|||
|
|
| Miara | Wartość |
|
|||
|
|
|---|---|
|
|||
|
|
| Długość body (znaki): p25 / p50 / p75 / p90 / p99 | 656 / **1 616** / 3 752 / 8 894 / 24 796 |
|
|||
|
|
| Średnia | 3 335 znaków |
|
|||
|
|
| Puste body (attachment-only itp.) | 0,3% |
|
|||
|
|
| HTML-only (bez części text/plain) | **15%** |
|
|||
|
|
| Sygnał newslettera (`List-Unsubscribe` OR `List-Id` OR `Precedence: bulk/list`) | **40% maili, 48,5% wolumenu tekstu** (w dekadzie 202x: 64% maili!) |
|
|||
|
|
| Maile z >30% linii cytowanych (`>`) | **31%** |
|
|||
|
|
|
|||
|
|
**Ekstrapolacja chunków** (600 tok / 150 overlap = 2400/600 znaków, jak
|
|||
|
|
paperless): ~**496k chunków** dla całości, ~**271k** bez newsletterów
|
|||
|
|
(2,0–2,5 chunka/mail). Bez obcinania cytowań — po obcięciu (Decyzja 2)
|
|||
|
|
realnie mniej. Mediana maila (1,6k znaków) = **1 chunk**.
|
|||
|
|
|
|||
|
|
Rozkład kopert po latach (SQL, `envelope.ts`): 2011–2012 szczyt (~23k/rok),
|
|||
|
|
ostatnie 12 miesięcy = **13 712 maili**, `epoch_fallback` (1970) = 2 559.
|
|||
|
|
|
|||
|
|
### 1.4 Ollama SOLARIA — batch embed ZMIERZONY, działa
|
|||
|
|
|
|||
|
|
- Ollama **0.32.0**, RTX 4070 Ti SUPER 16 GB (driver 595.71.05, GPU żywe).
|
|||
|
|
- `/api/embed` z `input` jako **listą** działa. Benchmark (bge-m3, na żywo):
|
|||
|
|
|
|||
|
|
| Długość tekstu | batch=1 | batch=32 | batch=64 | batch=128 |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| ~900 znaków | 148 ms | 9,8 ms/szt | 8,5 ms/szt | 6,6 ms/szt |
|
|||
|
|
| ~3500 znaków | 160 ms | 26,8 ms/szt | 18,1 ms/szt | — |
|
|||
|
|
|
|||
|
|
Wniosek: batch 64 daje **~8–18 ms/chunk** zamiast 207 ms sekwencyjnie —
|
|||
|
|
**~271k aktywnych chunków ≈ 1–1,5 h GPU** (vs ~16 h sekwencyjnie). Batching
|
|||
|
|
przestaje być ryzykiem, jest zmierzonym faktem.
|
|||
|
|
|
|||
|
|
- Obecny klient (`packages/kb-retrieval/src/kb_retrieval/embed.py::embed_chunk`)
|
|||
|
|
używa starego endpointu `/api/embeddings` z pojedynczym `prompt` — wymaga
|
|||
|
|
rozszerzenia o `embed_batch` (Krok 1).
|
|||
|
|
- Kontekst: bge-m3 ma limit 8192 tok; chunk 600 tok — bez ryzyka obcięcia.
|
|||
|
|
- Ollama@SOLARIA ma udokumentowaną niestabilność (3 incydenty w tydzień:
|
|||
|
|
zniknięcie kontenera, network-detach, bind-race przy starcie) — run musi być
|
|||
|
|
wznawialny i logowany do pliku (inwarianty §1.5).
|
|||
|
|
|
|||
|
|
### 1.5 Wzorce jobów — dziedzictwo obowiązkowe
|
|||
|
|
|
|||
|
|
Z `gmail-bulk-import` / `gmail-header-backfill` / `chunk_embed` (rodzina jobów,
|
|||
|
|
faza 3 §1.4):
|
|||
|
|
|
|||
|
|
- dry-run domyślny, `--apply` jawnie; `--limit`/`--offset` po stabilnym
|
|||
|
|
`ORDER BY id`; idempotencja = pre-fetch istniejących kluczy +
|
|||
|
|
`ON CONFLICT DO NOTHING` (z parsowaniem command taga); **bilans statystyk jako
|
|||
|
|
inwariant** (suma wyników = scanned, `stats_mismatch` → niezerowy exit);
|
|||
|
|
izolacja błędów per wiersz (lekcja −4999 wierszy: `json.dumps` i wszystko
|
|||
|
|
per-row W try); zawsze `> run.log 2>&1`, nigdy goły tmux.
|
|||
|
|
- Hardening 8-bitowych nagłówków: typed parse (`policy.default`) z fallbackiem
|
|||
|
|
compat32 + `kb_mail.text.sanitize_surrogates` — korpus **udowodnił**, że
|
|||
|
|
zawiera surowe bajty 8-bit w nagłówkach (9 maili wymaga fallbacku).
|
|||
|
|
- Znany wart do naprawy w nowym jobie: pre-fetch kluczy w `chunk_embed` pomija
|
|||
|
|
`model` (README to dokumentuje) — job mailowy od początku kluczuje
|
|||
|
|
`(envelope_id, chunk_index, model)`.
|
|||
|
|
|
|||
|
|
### 1.6 Schema `document_chunk` — GOTOWA, migracje NIEPOTRZEBNE
|
|||
|
|
|
|||
|
|
Zweryfikowane `\d` na żywej bazie: `UNIQUE (envelope_id, chunk_index, model)`
|
|||
|
|
(migracja 003 zaaplikowana), `excluded_reason TEXT` obecne, FK
|
|||
|
|
`envelope(id) ON DELETE CASCADE`, HNSW `vector_cosine_ops` (domyślne m=16,
|
|||
|
|
ef_construction=64). `excluded_reason` nie ma CHECK-a — nowa wartość
|
|||
|
|
`'newsletter'` (Decyzja 4) nie wymaga ALTER-a. **Zero migracji w tej fazie.**
|
|||
|
|
|
|||
|
|
### 1.7 Retrieval — koperty bez summary są dziś NIEWIDOCZNE w kaskadzie
|
|||
|
|
|
|||
|
|
`cascade_retrieve` (`packages/kb-retrieval/src/kb_retrieval/retrieval.py:57-100`):
|
|||
|
|
stage 2 rankuje wyłącznie chunki kopert wyłonionych w stage 1 z
|
|||
|
|
`document_summary WHERE model='claude-haiku-4-5'`. Koperta bez streszczenia
|
|||
|
|
**nigdy nie wejdzie do wyniku kaskady** (docstring mówi to wprost). `flat`
|
|||
|
|
je widzi, ale w UI to tryb debug. `kb-query` domyślnie `mode=cascade`.
|
|||
|
|
Inwariant startowy `startup.py` (bge-m3 w `document_chunk.model` I
|
|||
|
|
`document_summary.embedding_model`) pozostaje spełniony przez paperless —
|
|||
|
|
dolanie chunków mailowych bez summaries go nie łamie.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Otwarte decyzje dla Oskara (z rekomendacjami)
|
|||
|
|
|
|||
|
|
### Decyzja 1 — Skąd treść: ponowny MIME-walk archiwum .eml, ekstrakcja stdlib
|
|||
|
|
|
|||
|
|
**Rekomendacja: nowy przebieg po archiwum .eml (nie po mbox), parser stdlib
|
|||
|
|
z dziedziczonym hardeningiem, HTML→tekst własnym `html.parser.HTMLParser`.**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- Archiwum jest źródłem kanonicznym z gotowym mapowaniem `raw_ref`; re-run
|
|||
|
|
mboxa to pełny rebuild indeksu 27 GB przy każdym otwarciu (lekcja z sesji
|
|||
|
|
importu) i brak związku z id kopert.
|
|||
|
|
- Ekstrakcja body = dokładnie te części, które `_parse_attachments` dziś
|
|||
|
|
pomija: inline `text/plain` preferowane; gdy mail jest HTML-only (15%),
|
|||
|
|
HTML→tekst. Charset: `get_content_charset()` z `errors="replace"` +
|
|||
|
|
`sanitize_surrogates` (korpus ma łamane kodowania — udowodnione).
|
|||
|
|
- HTML→tekst: własny `HTMLParser` (pomija `style/script/head`, skleja tekst,
|
|||
|
|
redukuje whitespace) — **zero nowych zależności**, zweryfikowany na próbce
|
|||
|
|
600 (0 błędów). Newslettery HTML dają po konwersji czysty tekst nawigacyjny,
|
|||
|
|
ale te i tak podlegają Decyzji 4.
|
|||
|
|
- Typed parse `policy.default` (dekoduje RFC 2047) z fallbackiem compat32 —
|
|||
|
|
wzorzec 1:1 z `gmail-header-backfill`, razem z jego testami regresyjnymi.
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: biblioteki `html2text`/`beautifulsoup`/`talon` — nowe
|
|||
|
|
zależności w venv na dwóch nodach dla problemu, który stdlib rozwiązuje
|
|||
|
|
wystarczająco dobrze na tym korpusie.
|
|||
|
|
|
|||
|
|
### Decyzja 2 — Cytowania-łańcuszki: TAK, obcinać
|
|||
|
|
|
|||
|
|
**Rekomendacja: obcinać quoted reply chains przed chunkowaniem, własną
|
|||
|
|
heurystyką (bez zależności), z licznikiem w bilansie.**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- 31% maili ma >30% linii cytowanych. Bez obcinania każda odpowiedź w wątku
|
|||
|
|
dubluje treść poprzedników — chunki zdominowane powtórzeniami, retrieval
|
|||
|
|
zwraca N kopii tego samego akapitu z różnych kopert.
|
|||
|
|
- Heurystyka: (a) linie `^\s*>`; (b) wszystko od markera odpowiedzi w dół:
|
|||
|
|
`On ... wrote:`, `Dnia ... napisał(a):`, `W dniu ... pisze:`,
|
|||
|
|
`-----Original Message-----`, `________________________________` (Outlook);
|
|||
|
|
(c) w HTML: poddrzewa `div.gmail_quote` / `blockquote` przed konwersją.
|
|||
|
|
- Odwracalne: archiwum nietknięte; zmiana heurystyki = re-run joba (idempotencja
|
|||
|
|
po kluczu z `model` — nowa wersja chunkera może iść pod nowym `model`-tagiem
|
|||
|
|
lub po `DELETE` starych chunków gmail — decyzja operacyjna przy re-runie).
|
|||
|
|
- Bilans: `quoted_chars_stripped` sumarycznie + per-mail flaga w logu, żeby
|
|||
|
|
bramka jakościowa mogła wykryć nadgorliwe cięcie.
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: `talon` (Mailgun) — cięższy, nieutrzymywany, uczony na
|
|||
|
|
korpusie EN; nasza heurystyka musi znać polskie markery.
|
|||
|
|
|
|||
|
|
### Decyzja 3 — Nagłówki jako kontekst chunka: TAK, prefiks w każdym chunku
|
|||
|
|
|
|||
|
|
**Rekomendacja: każdy chunk maila zaczyna się od jednej linii kontekstu
|
|||
|
|
`Temat: … | Od: … | Data: YYYY-MM-DD`, budowanej z `entities[type=headers]`
|
|||
|
|
(już w DB — zero ponownego parsowania nagłówków).**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- bge-m3 embeduje chunk w izolacji; środkowy chunk długiego maila bez tematu
|
|||
|
|
i nadawcy traci sens zapytań typu „mail od X o Y". Prefiks kosztuje
|
|||
|
|
~100–200 znaków z budżetu 2400 (4–8%).
|
|||
|
|
- Prefiks w `document_chunk.text` (nie osobna kolumna) — trafia też do
|
|||
|
|
wyników `kb-query`, co od razu poprawia czytelność UI dla maili.
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: goły body (tańsze o 5% tokenów, gubi kontekst); prefiks
|
|||
|
|
tylko w chunk_index=0 (niespójne — retrieval zwraca pojedyncze chunki).
|
|||
|
|
|
|||
|
|
### Decyzja 4 — Newslettery: chunkować, NIE embedować (`excluded_reason='newsletter'`)
|
|||
|
|
|
|||
|
|
**Rekomendacja: tanie kryterium nagłówkowe — `List-Unsubscribe` OR `List-Id`
|
|||
|
|
OR `Precedence: bulk|list` ⇒ chunki zapisane z `excluded_reason='newsletter'`
|
|||
|
|
i `embedding=NULL` (bez kosztu GPU, poza HNSW i retrievalem). Reszta korpusu
|
|||
|
|
embedowana w całości.**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- Kryterium jest darmowe (nagłówki czytamy i tak), deterministyczne
|
|||
|
|
i zmierzone: łapie 40% maili niosących 48,5% wolumenu tekstu — w tym
|
|||
|
|
praktycznie cały szum komercyjny, który zatapiałby retrieval
|
|||
|
|
(feedback „mało danych" nie znaczy „chcę promocji z 2019").
|
|||
|
|
- **Odwracalne w obie strony**: tekst chunków newsletterów JEST w bazie —
|
|||
|
|
jeśli operator zechce ich szukać, wystarczy `UPDATE … SET excluded_reason=NULL
|
|||
|
|
WHERE excluded_reason='newsletter'` + doembedowanie (backlog-query cyclic
|
|||
|
|
ingest ich nie widzi, bo filtruje `excluded_reason IS NULL` — nie zawyżą
|
|||
|
|
metryki `kb_ingest_embed_backlog`).
|
|||
|
|
- Koszt magazynowy flagowanych chunków: ~225k wierszy × ~1,5 KB tekstu ≈
|
|||
|
|
0,5 GB — akceptowalny za odwracalność.
|
|||
|
|
- Spam: Takeout „All Mail" nie zawiera folderu Spam — osobna kategoria nie
|
|||
|
|
jest potrzebna; `epoch_fallback` (2 559 maili z ts=1970) chunkujemy
|
|||
|
|
normalnie (data w prefiksie z `date_raw`, jeśli jest).
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: embedować wszystko (2× GPU, szum w wynikach — a i tak
|
|||
|
|
odwracalne tylko przez `excluded_reason`); pomijać newslettery całkiem
|
|||
|
|
(nieodwracalne bez re-parse 27 GB).
|
|||
|
|
|
|||
|
|
### Decyzja 5 — Batching embeddingów: `/api/embed`, batch 64, sekwencyjnie
|
|||
|
|
|
|||
|
|
**Rekomendacja: nowa funkcja `embed_batch(session, base_url, model, texts) ->
|
|||
|
|
list[vector]` w `packages/kb-retrieval/embed.py` na endpoint `/api/embed`
|
|||
|
|
(`input` jako lista), batch 64, batche sekwencyjnie (bez równoległości HTTP).**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- Zmierzone na żywo (§1.4): batch 64 = 8–18 ms/chunk, ~11–22× szybciej niż
|
|||
|
|
obecna ścieżka; 271k chunków ≈ **1–1,5 h**. Równoległość pojedynczych
|
|||
|
|
requestów nie jest potrzebna — GPU i tak saturuje się batchem, a sekwencyjny
|
|||
|
|
pętla = prostszy bilans i wznawialność.
|
|||
|
|
- `embed_chunk` (pojedynczy) zostaje bez zmian dla kb-query (zapytanie użytkownika
|
|||
|
|
= 1 tekst) i dla cyclic ingest paperless.
|
|||
|
|
- Plan benchmarku przed pełnym runem (wzorzec „zmierz, obejrzyj, dopiero wtedy
|
|||
|
|
zaufaj progowi"): pierwszy run Etapu A (§8) loguje `avg_embed_ms_per_chunk`
|
|||
|
|
per batch; jeśli >30 ms/chunk — stop i diagnoza (CPU fallback? model
|
|||
|
|
zewisiony?) zanim ruszy Etap B. Dodatkowo sanity-check wymiaru 1024 na
|
|||
|
|
KAŻDYM elemencie odpowiedzi batcha (guard `EXPECTED_DIM` jak w chunk_embed).
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: równoległe requesty na `/api/embeddings` (HTTP overhead,
|
|||
|
|
nieprzewidywalna kolejność błędów w bilansie); zewnętrzny serwis embeddingów
|
|||
|
|
(koszt, prywatność korpusu mailowego).
|
|||
|
|
|
|||
|
|
### Decyzja 6 — Streszczenia maili: NIE. Kaskada dostaje tryb hybrydowy
|
|||
|
|
|
|||
|
|
**Rekomendacja: maile NIE dostają `document_summary`. Zamiast tego kaskada
|
|||
|
|
w `kb-retrieval` zyskuje tryb hybrydowy: stage 1 po streszczeniach dla źródeł,
|
|||
|
|
które je mają (paperless), plus równoległy bezpośredni HNSW po chunkach źródeł
|
|||
|
|
bez streszczeń (gmail), scalenie po `dist` (ta sama metryka: cosine na bge-m3).**
|
|||
|
|
|
|||
|
|
Uzasadnienie architektoniczne:
|
|||
|
|
|
|||
|
|
- Kaskada powstała, by prefiltrować duże dokumenty OCR przez ich streszczenia.
|
|||
|
|
Mail to inny kształt danych: mediana 1 chunk/mail — „streszczenie" maila
|
|||
|
|
byłoby zwykle dłuższe od niego samego. Prefiltr nic nie wnosi, a HNSW po
|
|||
|
|
271k chunków to wciąż milisekundy (log-scale).
|
|||
|
|
- Scalanie jest uczciwe: oba tory zwracają `dist` z tego samego embeddera
|
|||
|
|
i tej samej przestrzeni — merge top-k po min-dist bez normalizacji.
|
|||
|
|
- Zmiana w jednym miejscu: `packages/kb-retrieval/retrieval.py` (nowa funkcja
|
|||
|
|
`hybrid_retrieve` / `hybrid_query`; stage 2 obecnej kaskady nietknięty)
|
|||
|
|
+ `kb-query` `mode` rozszerzony o `hybrid` (docelowo domyślny PO przejściu
|
|||
|
|
bramki, §8). Inwariant startowy `startup.py` bez zmian.
|
|||
|
|
|
|||
|
|
Ekonomia streszczeń, gdyby jednak (dla porządku, liczby do decyzji późniejszej):
|
|||
|
|
|
|||
|
|
- **Haiku 4.5 na 225k maili**: ~1,7k tok input + ~150 tok output/mail →
|
|||
|
|
~380M input + ~34M output ≈ **~550 USD** (i zderzenie z capem $200/mies.).
|
|||
|
|
- **gemma3:12b lokalnie**: pilot paperless ~10 s/dok; maile krótsze, ~4–8 s →
|
|||
|
|
**11–21 dni ciągłej pracy GPU**. Nierealne dla całości.
|
|||
|
|
- **Hybryda selektywna** (np. non-newsletter z ostatnich 2 lat, ~20–25k maili
|
|||
|
|
≈ 60–80 USD Haiku) — sensowna dopiero, jeśli bramka pokaże, że tryb hybrydowy
|
|||
|
|
nie wystarcza. Odłożona, nie odrzucona.
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: streszczenia całego korpusu (koszt/czas j.w.);
|
|||
|
|
wpuszczenie maili do obecnej kaskady bez zmian (są w niej niewidoczne — §1.7);
|
|||
|
|
przełączenie kb-query na `flat` (regresja jakości dla paperless, po to była
|
|||
|
|
faza 3).
|
|||
|
|
|
|||
|
|
### Decyzja 7 — Nowy job `jobs/mail-body-ingest/`, chunker wydzielony do pakietu
|
|||
|
|
|
|||
|
|
**Rekomendacja: nowy job `jobs/mail-body-ingest/` (MIME-walk → quote-strip →
|
|||
|
|
chunk → klasyfikacja newsletter → batch embed → insert). Wspólny chunker
|
|||
|
|
(`chunk_text`, `hard_split`) wydzielony do `packages/kb-mail`
|
|||
|
|
(`kb_mail/chunking.py`); `documents-ingest` importuje go z pakietu.**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- `chunk_embed` jest spleciony z założeniem `source='paperless'` +
|
|||
|
|
`entities[type=content]`; adapter treści mailowej to inne źródło (pliki),
|
|||
|
|
inny preprocessing (quote-strip, HTML), inna pętla embed (batch). Wspólna
|
|||
|
|
jest tylko logika chunkowania — i ją wydzielamy (dokładnie wzorzec fazy 4:
|
|||
|
|
`retrieval.py` → `packages/kb-retrieval`).
|
|||
|
|
- `is_ocr_junk` zostaje w documents-ingest (progi kalibrowane na OCR, nie na
|
|||
|
|
mailach); mail-body-ingest ma własne, prostsze wykluczenie: `body_empty`.
|
|||
|
|
- Job czyta nagłówki z `entities[type=headers]` (prefiks, Decyzja 3)
|
|||
|
|
i klasyfikuje newsletter z surowego .eml (nagłówki List-* nie są w entities —
|
|||
|
|
są tanie do odczytu w trakcie i tak wykonywanego parse'u).
|
|||
|
|
- Rodzina jobów: pełny zestaw inwariantów §1.5 (dry-run domyślny, `--apply`,
|
|||
|
|
`--limit/--offset` po `ORDER BY id`, `--since DATE` dla etapowania po
|
|||
|
|
`envelope.ts`, bilans, log do pliku, testy na fixture'ach .eml z 8-bit
|
|||
|
|
nagłówkami / HTML-only / quoted-chain).
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: rozszerzanie `documents-ingest` o adapter mailowy
|
|||
|
|
(rozrost joba o dwóch tożsamościach); copy-paste chunkera (dryf dwóch
|
|||
|
|
implementacji — dokładnie to, czego faza 4 zabroniła dla retrievalu).
|
|||
|
|
|
|||
|
|
### Decyzja 8 — Wykonanie: job na SOLARII, archiwum rsync-owane, zapis do PIHA
|
|||
|
|
|
|||
|
|
**Rekomendacja: jednorazowy rsync archiwum PIHA→SOLARIA (27 GB po LAN ~5 min,
|
|||
|
|
SOLARIA ma 2 TB NVMe), job biegnie na SOLARII: parse na 24 rdzeniach, Ollama
|
|||
|
|
na localhost, zapis do kb-postgres@PIHA (`--dsn …@piha:5433`, batched
|
|||
|
|
`executemany` po 500 — wzorzec `_insert_batch`).**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- PIHA (Pi-klasa, 8 GB RAM, dźwiga HA + Paperless + kb-postgres) nie jest
|
|||
|
|
miejscem na godziny parsowania MIME 225k plików; SOLARIA i tak musi być
|
|||
|
|
włączona (GPU). Embed z localhost eliminuje 271k×(round-trip Tailscale).
|
|||
|
|
- Archiwum jest append-only → kopia jest spójnym snapshotem; lista roboczą
|
|||
|
|
i tak wyznacza DB (`SELECT … FROM envelope WHERE source='gmail'`), nie
|
|||
|
|
filesystem. `missing_file` w bilansie łapie ewentualny dryf kopii.
|
|||
|
|
- Zapis do PIHA przez Tailscale batchami — dokładnie tak dziś działa
|
|||
|
|
`chunk_embed` na SOLARII (kierunek przećwiczony).
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: NFS PIHA→SOLARIA (kruche przy 225k małych plików,
|
|||
|
|
nic nie daje vs snapshot); run na PIHA z batch embedem przez Tailscale
|
|||
|
|
(wykonalne — batch amortyzuje sieć — ale obciąża najbardziej krytyczny node
|
|||
|
|
floty na godziny).
|
|||
|
|
|
|||
|
|
### Decyzja 9 — Etapowanie: najpierw ostatnie 12 miesięcy, bramka, potem reszta
|
|||
|
|
|
|||
|
|
**Rekomendacja: Etap A = `--since 2025-07-01` (13 712 maili → ~25–30k chunków,
|
|||
|
|
z czego po filtrze newsletterów ~10–12k aktywnych; embed <15 min) → bramka
|
|||
|
|
jakościowa (§8) + ręczna ocena operatora w kb-query → Etap B = pełne archiwum.**
|
|||
|
|
|
|||
|
|
Uzasadnienie:
|
|||
|
|
|
|||
|
|
- Ostatni rok to dane, o które operator realnie pyta; tanio weryfikuje
|
|||
|
|
cały łańcuch (quote-strip, prefiks, hybrydowy retrieval) zanim spalimy
|
|||
|
|
godziny GPU i ~5 GB bazy na dekady archiwum.
|
|||
|
|
- Uwaga kalibracyjna: w dekadzie 202x aż 64% maili ma sygnał newslettera —
|
|||
|
|
Etap A od razu pokaże, czy heurystyka nie tnie za szeroko (bilans per
|
|||
|
|
kategoria + przegląd próbki flagowanych).
|
|||
|
|
- Idempotencja sprawia, że Etap B to po prostu ten sam run bez `--since` —
|
|||
|
|
chunki Etapu A zostaną policzone jako `already_embedded`.
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: pełny run od razu (ryzyko odkrycia złej heurystyki po
|
|||
|
|
496k chunkach); etapowanie po `--offset` (kolejność po id ≠ kolejność
|
|||
|
|
merytoryczna; `ts` jest indeksowane i czytelne).
|
|||
|
|
|
|||
|
|
### Decyzja 10 — Threading przy okazji (odpowiedź na „brak połączeń"): TAK, tanio
|
|||
|
|
|
|||
|
|
**Rekomendacja: podczas i tak wykonywanego MIME-walku dopisać do kopert
|
|||
|
|
`entities[type=threading]` z `{in_reply_to, references[]}` — wzorzec
|
|||
|
|
idempotentnego appendu 1:1 z `gmail-header-backfill` (`WHERE NOT EXISTS
|
|||
|
|
type='threading'`).**
|
|||
|
|
|
|||
|
|
Uzasadnienie: dziś NIC nie łączy maili w wątki (In-Reply-To/References nie są
|
|||
|
|
nigdzie zapisane — zweryfikowane grepem). To najtańszy krok w kierunku
|
|||
|
|
„połączeń" z feedbacku operatora: drugi odczyt 27 GB tylko po to byłby
|
|||
|
|
marnotrawstwem. Sama nawigacja po wątkach / graf to osobna przyszła praca
|
|||
|
|
(§11) — tu tylko zapisujemy surowiec.
|
|||
|
|
|
|||
|
|
Odrzucona alternatywa: osobny backfill później (drugi pełny odczyt archiwum);
|
|||
|
|
pominięcie (strata jedynej okazji taniego zbioru danych).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Krok 0 — wydzielenie chunkera do `packages/kb-mail`
|
|||
|
|
|
|||
|
|
- `packages/kb-mail/src/kb_mail/chunking.py`: przeniesione 1:1 `chunk_text`,
|
|||
|
|
`hard_split` + stałe (2400/600) z `documents_ingest/chunk_embed.py`;
|
|||
|
|
chunk_embed importuje z pakietu (zero zmian zachowania, testy chunkera
|
|||
|
|
przeniesione + smoke że documents-ingest nadal przechodzi pytest).
|
|||
|
|
- Przy okazji NIE ruszamy `is_ocr_junk` ani klienta embed (Krok 1 osobno).
|
|||
|
|
|
|||
|
|
**Szacunek: 0,5 sesji.**
|
|||
|
|
|
|||
|
|
## 4. Krok 1 — `embed_batch` w `packages/kb-retrieval`
|
|||
|
|
|
|||
|
|
- `embed.py`: `embed_batch(session, base_url, model, texts: list[str])` na
|
|||
|
|
`POST /api/embed` (`{"model": …, "input": [...]}`), zwraca listę wektorów +
|
|||
|
|
czas; walidacja: `len(embeddings) == len(texts)`, każdy wymiar == 1024
|
|||
|
|
(w przeciwnym razie wyjątek klasy abort-run, jak `EmbeddingDimensionError`).
|
|||
|
|
- `embed_chunk` (pojedynczy, `/api/embeddings`) zostaje nietknięty — kb-query
|
|||
|
|
i cyclic ingest bez zmian.
|
|||
|
|
- Testy: mock aiohttp (kształt odpowiedzi `/api/embed`), mismatch długości,
|
|||
|
|
zły wymiar.
|
|||
|
|
|
|||
|
|
**Szacunek: 0,5 sesji.**
|
|||
|
|
|
|||
|
|
## 5. Krok 2 — job `jobs/mail-body-ingest/`
|
|||
|
|
|
|||
|
|
Pipeline per koperta (`SELECT id, ts, raw_ref, entities FROM envelope WHERE
|
|||
|
|
source='gmail' [AND ts >= --since] ORDER BY id`, `--limit/--offset`):
|
|||
|
|
|
|||
|
|
1. **Read**: `archive_root / raw_ref` (`--archive-root`, default snapshotu na
|
|||
|
|
SOLARII); `missing_file` / `read_error` jak w backfillu.
|
|||
|
|
2. **Parse**: typed parse + fallback compat32 (wzorzec i testy z backfillu);
|
|||
|
|
body = inline text/plain, a gdy brak — HTML→tekst (`HTMLParser` stdlib);
|
|||
|
|
charset `errors="replace"` + `sanitize_surrogates`.
|
|||
|
|
3. **Quote-strip** (Decyzja 2) z licznikiem `quoted_chars_stripped`.
|
|||
|
|
4. **Klasyfikacja**: `newsletter` (nagłówki List-*/Precedence z tego samego
|
|||
|
|
parse'u); `body_empty` (po strippingu) → koperta liczona, zero chunków.
|
|||
|
|
5. **Prefiks kontekstu** (Decyzja 3) z `entities[type=headers]`.
|
|||
|
|
6. **Chunk**: `kb_mail.chunking.chunk_text` (2400/600).
|
|||
|
|
7. **Embed**: tylko chunki nie-newsletter, `embed_batch` po 64 (batch może
|
|||
|
|
sklejać chunki wielu kopert — pętla buforuje do 64 i flushuje).
|
|||
|
|
8. **Insert**: `INSERT … ON CONFLICT (envelope_id, chunk_index, model) DO
|
|||
|
|
NOTHING`, batched po 500; newsletter → `excluded_reason='newsletter'`,
|
|||
|
|
`embedding=NULL`; model = `bge-m3`. Pre-fetch kluczy Z modelem (naprawa
|
|||
|
|
warta z §1.5).
|
|||
|
|
9. **Threading append** (Decyzja 10): `entities || [{"type":"threading",…}]`
|
|||
|
|
z podwójną idempotencją (client-side check + `WHERE NOT EXISTS`).
|
|||
|
|
|
|||
|
|
Bilans (inwariant, exit 1 przy niedomknięciu):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
mails_scanned = parse_errors + read_errors + missing_file + body_empty + mails_chunked
|
|||
|
|
chunks_total = chunks_inserted + chunks_newsletter_flagged
|
|||
|
|
+ chunks_already_embedded + chunks_conflict_skipped + chunks_errors
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
CLI: `--dsn/KB_DSN`, `--archive-root`, `--ollama-url`, `--model`, `--since`,
|
|||
|
|
`--limit`, `--offset`, `--batch-size` (64), `--apply` (dry-run domyślnie:
|
|||
|
|
parse+chunk+count, zero Ollamy, zero writes). Log ZAWSZE do pliku.
|
|||
|
|
|
|||
|
|
Testy (`jobs/mail-body-ingest/tests/`): fixture'y .eml — 8-bit nagłówki,
|
|||
|
|
HTML-only, quoted-chain (gmail/outlook/polskie markery), newsletter
|
|||
|
|
(List-Unsubscribe), pusty body, multipart z załącznikiem; testy bilansu
|
|||
|
|
i idempotencji (drugi przebieg = zero insertów). Definition of Done z CLAUDE.md
|
|||
|
|
(build/smoke + pytest przed commitem).
|
|||
|
|
|
|||
|
|
**Szacunek: 2 sesje.**
|
|||
|
|
|
|||
|
|
## 6. Krok 3 — tryb hybrydowy w `kb-retrieval` + `kb-query`
|
|||
|
|
|
|||
|
|
- `retrieval.py`: `hybrid_retrieve(conn, query_vec, n, k, summary_model,
|
|||
|
|
summaryless_sources: list[str])` — stage 1+2 jak w kaskadzie, PLUS
|
|||
|
|
równoległe zapytanie: chunki kopert źródeł z `summaryless_sources`
|
|||
|
|
(`JOIN envelope … WHERE source = ANY($…)`), merge po `dist`, top-k;
|
|||
|
|
`hybrid_query` analogicznie do `cascade_query` (jeden embed zapytania).
|
|||
|
|
- `kb-query`: `mode` pattern `^(cascade|flat|hybrid)$`; **domyślny `mode`
|
|||
|
|
przełączany na `hybrid` dopiero po PASS bramki (§8)** — do tego czasu
|
|||
|
|
hybrid dostępny jawnie. Wyniki gmail w UI już obsłużone (faza 4: subject/from
|
|||
|
|
z headers + „Kopiuj Message-ID").
|
|||
|
|
- Testy jednostkowe na mockach (merge, pusta gałąź summary, pusta gałąź mail).
|
|||
|
|
|
|||
|
|
**Szacunek: 1 sesja.**
|
|||
|
|
|
|||
|
|
## 7. Krok 4 — przygotowanie SOLARII + Etap A (ostatnie 12 miesięcy)
|
|||
|
|
|
|||
|
|
- rsync archiwum PIHA→SOLARIA (`rsync -a --info=stats` po LAN; ~27 GB).
|
|||
|
|
- Run Etapu A: `--since 2025-07-01 --apply` z logiem do pliku; weryfikacja
|
|||
|
|
bilansu; kalibracja: przegląd ~20 flagowanych newsletterów i ~20 maili po
|
|||
|
|
quote-strip (czy heurystyki nie tną za szeroko), `avg_embed_ms_per_chunk`
|
|||
|
|
vs benchmark (§ Decyzja 5).
|
|||
|
|
- Weryfikacyjne SQL po runie (przykład):
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- ile kopert gmail ma chunki, w podziale na status
|
|||
|
|
SELECT c.excluded_reason, count(*) chunks, count(DISTINCT c.envelope_id) mails
|
|||
|
|
FROM document_chunk c JOIN envelope e ON e.id = c.envelope_id
|
|||
|
|
WHERE e.source = 'gmail' GROUP BY 1;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Szacunek: 1 sesja (w tym czas runu <1 h).**
|
|||
|
|
|
|||
|
|
## 8. Krok 5 — bramka jakościowa fazy mailowej
|
|||
|
|
|
|||
|
|
Rozszerzenie `jobs/documents-ingest/eval/queries.yaml` + `retrieval_eval.py`:
|
|||
|
|
|
|||
|
|
- **Operator dostarcza 3–5 zapytań mailowych** (rzeczy, o których wie, że ma je
|
|||
|
|
w mailach z ostatniego roku — Etap A) → nowe wpisy `kind: hit` z
|
|||
|
|
`expected_envelope: "<message-id>"` (uwaga: envelope_id gmail = surowy
|
|||
|
|
Message-ID bez prefiksu source — inaczej niż `paperless:N`).
|
|||
|
|
- `retrieval_eval.py` uczy się trybu `hybrid` (trzecia ścieżka obok flat/cascade).
|
|||
|
|
- Kryteria PASS (wszystkie trzy tory na żywej bazie po Etapie A):
|
|||
|
|
1. **Regresja zero**: istniejące 7 zapytań paperless — żaden hit (<0,45)
|
|||
|
|
nie degraduje we flat ani w hybrid po dolaniu chunków mailowych
|
|||
|
|
(to mierzy realne ryzyko tej fazy: nowa masa wektorów konkuruje w HNSW).
|
|||
|
|
2. Zapytania mailowe: hit@3 w trybie hybrid dla ≥ 4/5 (lub 3/3–4/4 przy
|
|||
|
|
mniejszej liczbie), top1 dist < 0,45 dla większości.
|
|||
|
|
3. Kontrole negatywne (sernik, piaskownica) > 0,55 we wszystkich trybach.
|
|||
|
|
- PASS ⇒ `kb-query` przełącza domyślny `mode` na `hybrid` + zapis wyników
|
|||
|
|
w tym dokumencie (tabela `| Kryterium | Wynik | Werdykt |`).
|
|||
|
|
- FAIL ⇒ diagnoza przed Etapem B (podejrzani wg kolejności: zbyt szeroki
|
|||
|
|
quote-strip, brak prefiksu w praktyce, `hnsw.ef_search` do podbicia).
|
|||
|
|
|
|||
|
|
**Szacunek: 1 sesja (+ wejście od operatora).**
|
|||
|
|
|
|||
|
|
## 9. Krok 6 — Etap B: pełne archiwum
|
|||
|
|
|
|||
|
|
- Ten sam job bez `--since`, `--apply`, log do pliku, `nice`/`ionice` na I/O.
|
|||
|
|
Oczekiwane: ~496k chunków total (~271k embedowanych; 1–2 h GPU + parse),
|
|||
|
|
Etap A policzony jako `already_embedded`.
|
|||
|
|
- Po runie: weryfikacyjne SQL (j.w.), `VACUUM ANALYZE document_chunk`,
|
|||
|
|
ponowny przebieg `retrieval_eval.py` (pełna regresja na 100% korpusu —
|
|||
|
|
dystrybucja starych dekad może przesunąć sąsiedztwa HNSW).
|
|||
|
|
- Obserwacja PIHA: rozmiar bazy (oczekiwane ~5–7 GB, dysk 144 GB — zapas
|
|||
|
|
>20×), latencja `/search` w kb-query, RAM kb-postgres (`mem_limit: 1g`,
|
|||
|
|
`shared_buffers=256MB`). **Świadomie bez prewencyjnego podnoszenia limitów**
|
|||
|
|
— HNSW jest log-scale i 271k wektorów to wciąż mało; jeśli latencja
|
|||
|
|
zapytań zauważalnie wzrośnie, osobna mikro-decyzja o `mem_limit` 1g→1,5g
|
|||
|
|
(PIHA ma ~3,8 GB luzu, ale dzieli go z HA). REINDEX HNSW nie jest w planie
|
|||
|
|
(inserty inkrementalne); gdyby kiedyś był potrzebny — wymaga sesyjnego
|
|||
|
|
podbicia `maintenance_work_mem` (64 MB nie pomieści grafu ~1,1 GB) i godzin,
|
|||
|
|
co odnotowuję jako znany koszt odroczony.
|
|||
|
|
|
|||
|
|
**Szacunek: 1 sesja (run w tle).**
|
|||
|
|
|
|||
|
|
## 10. Krok 7 — IMAP/JMAP przyrostówka (zarys; szczegóły = osobny recon)
|
|||
|
|
|
|||
|
|
Zakotwiczone w kb-00 jako etapy 3–4 (`jobs/fastmail-poller`,
|
|||
|
|
`jobs/gmail-imap-poller`). Zarys decyzji do tamtego reconu:
|
|||
|
|
|
|||
|
|
- **Poll, nie IDLE**: wzorzec systemd-timer jak `kb-ingest` (offline-tolerancja,
|
|||
|
|
brak długotrwałych połączeń); świeżość „raz na godzinę/dobę" wystarcza KB.
|
|||
|
|
- **Fastmail przez JMAP** (natywne, stronicowanie po `state`), `source='fastmail'`;
|
|||
|
|
Gmail przez IMAP (OAuth2 lub app-password — do rozstrzygnięcia tam).
|
|||
|
|
- Reuse wprost: `kb_mail.save_eml` (append-only, `FileExistsError`=skip) +
|
|||
|
|
`insert_envelope` (`ON CONFLICT DO NOTHING`, dedup po Message-ID jak
|
|||
|
|
w bulk-imporcie) + hardened parse + **pipeline body z Kroku 2 jako biblioteka**
|
|||
|
|
(przyrostówka od pierwszego dnia pisze chunki, nie tylko koperty).
|
|||
|
|
- Otwarte tam: sekrety (konta IMAP) w `/opt/homelab/config/`, częstotliwość,
|
|||
|
|
koperty fastmail sprzed epoki gmaila.
|
|||
|
|
|
|||
|
|
**Szacunek: sam recon 1 sesja; poza kryterium ukończenia tej fazy.**
|
|||
|
|
|
|||
|
|
## 11. Poza zakresem fazy mailowej
|
|||
|
|
|
|||
|
|
| Temat | Gdzie zakotwiczone | Kiedy |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Drive Takeout | backlog KB | osobny moduł |
|
|||
|
|
| Backfill 70k załączników PDF → Paperless OCR → ingest | `05-documents-ingest.md`, DECYZJE #9 | osobny pakiet (sizing OCR workera) |
|
|||
|
|
| Synteza odpowiedzi / chat | faza 5 | po fazie mailowej |
|
|||
|
|
| `mail_ui_url` (klikalny link do maila w UI) | kb-00 etap 6, pole zarezerwowane w kb-query | z modułem mail-UI |
|
|||
|
|
| Graf wątków / entity_link z `entities[type=threading]` | ta faza tylko zapisuje surowiec (Decyzja 10) | przyszła faza „połączenia" |
|
|||
|
|
| Streszczenia selektywne maili (hybryda Haiku) | Decyzja 6 — odłożona | po ocenie trybu hybrid w praktyce |
|
|||
|
|
| IMAP/JMAP przyrostówka — implementacja | Krok 7 (zarys) | osobny recon + pakiet |
|
|||
|
|
|
|||
|
|
## 12. Plan implementacji (kolejność = zależności)
|
|||
|
|
|
|||
|
|
| # | Krok | Zależy od | Szacunek |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 0 | Chunker → `packages/kb-mail` | — | 0,5 sesji |
|
|||
|
|
| 1 | `embed_batch` w kb-retrieval | — | 0,5 sesji |
|
|||
|
|
| 2 | Job `mail-body-ingest` | 0, 1 | 2 sesje |
|
|||
|
|
| 3 | Tryb hybrid (kb-retrieval + kb-query) | — (równolegle z 2) | 1 sesja |
|
|||
|
|
| 4 | rsync + Etap A (12 mies.) + kalibracja | 2 | 1 sesja |
|
|||
|
|
| 5 | Bramka jakościowa (eval mailowy + regresja) | 3, 4 + zapytania od operatora | 1 sesja |
|
|||
|
|
| 6 | Etap B (pełne archiwum) + regresja + obserwacja PIHA | 5 = PASS | 1 sesja |
|
|||
|
|
| 7 | Recon przyrostówki IMAP/JMAP | — (po 6) | 1 sesja (poza DoD fazy) |
|
|||
|
|
|
|||
|
|
**Kryterium ukończenia fazy mailowej:** (a) pełny korpus gmail zchunkowany
|
|||
|
|
(bilans domknięty, `parse_errors` na poziomie pojedynczych sztuk jak
|
|||
|
|
w backfillu), (b) chunki nie-newsletter zembedowane bge-m3 i widoczne w trybie
|
|||
|
|
hybrid, (c) bramka §8 PASS wraz z regresją po Etapie B, (d) `kb-query`
|
|||
|
|
domyślnie odpowiada trybem hybrid na `kb.kapala.org`, (e) koperty gmail mają
|
|||
|
|
`entities[type=threading]`.
|
|||
|
|
|
|||
|
|
## 13. Szacunki zbiorcze
|
|||
|
|
|
|||
|
|
- **Dane**: +~496k wierszy `document_chunk` (~271k z embeddingiem, ~225k
|
|||
|
|
flagowanych `newsletter`); baza 250 MB → ~5–7 GB (dysk PIHA: 144 GB wolne,
|
|||
|
|
zapas >20×). HNSW rośnie inkrementalnie przy insertach — bez rebuildu.
|
|||
|
|
- **GPU/czas runów**: Etap A <1 h e2e; Etap B: parse ~0,5–1 h (24 rdzenie)
|
|||
|
|
+ embed ~1–1,5 h (batch 64, zmierzone 8–18 ms/chunk) + inserty do PIHA.
|
|||
|
|
- **Koszty zewnętrzne: 0 USD** (bez streszczeń — Decyzja 6).
|
|||
|
|
- **Czas sesyjny**: ~7 sesji (kroki 0–6) + 1 recon przyrostówki.
|
|||
|
|
- **Ryzyka**: (1) regresja istniejących 7 zapytań po dolaniu ćwierci miliona
|
|||
|
|
wektorów — mierzona bramką na Etapie A ZANIM spalimy pełny run; (2) jakość
|
|||
|
|
HTML→tekst i quote-strip — kalibracja na próbce w Kroku 4; (3) niestabilność
|
|||
|
|
Ollama@SOLARIA (3 incydenty/tydzień) — idempotencja + bilans + log do pliku
|
|||
|
|
czynią każdy run wznawialnym; (4) latencja kb-postgres przy 1 GB mem_limit —
|
|||
|
|
obserwacja po Etapie B, decyzja o limicie osobno.
|
|||
|
|
|
|||
|
|
## 14. Podsumowanie dla Oskara
|
|||
|
|
|
|||
|
|
Treści Twoich 225 tysięcy maili leżą dziś martwe w 27 GB archiwum na PIHA —
|
|||
|
|
w bazie są tylko nagłówki, a wyszukiwarka z fazy 4 słusznie skarży się „mało
|
|||
|
|
danych". Ten plan wprowadza je do retrievalu w ~7 sesji i za 0 USD: ponowny
|
|||
|
|
przebieg po archiwum sprawdzonym parserem z backfillu, obcięcie łańcuszków
|
|||
|
|
cytowań (co trzeci mail jest nimi zdominowany), tani filtr newsletterów
|
|||
|
|
(40% korpusu, odwracalny — tekst zostaje w bazie), chunki z prefiksem
|
|||
|
|
Temat/Od/Data i batchowy embedding na GPU SOLARII, który zmierzyłem na żywo:
|
|||
|
|
zamiast 16 godzin sekwencyjnie — nieco ponad godzinę batchami po 64.
|
|||
|
|
Streszczeń NIE robimy (Haiku ≈ 550 USD, gemma lokalnie ≈ 2 tygodnie GPU) —
|
|||
|
|
zamiast tego kaskada dostaje tryb hybrydowy, w którym maile konkurują
|
|||
|
|
bezpośrednio chunkami. Etapowanie chroni jakość: najpierw ostatni rok
|
|||
|
|
i bramka (Twoje 3–5 pytań „wiem, że to mam w mailach" + zero regresji na
|
|||
|
|
7 istniejących zapytaniach), dopiero potem dekady archiwum. Po drodze, przy
|
|||
|
|
okazji jedynego pełnego odczytu archiwum, zapisujemy In-Reply-To/References —
|
|||
|
|
surowiec pod „połączenia", których brakowało Ci w POC. Schema jest gotowa
|
|||
|
|
(zweryfikowane na żywej bazie — zero migracji), dysk na PIHA ma zapas
|
|||
|
|
dwudziestokrotny. Nic nie zostało zaimplementowane, zdeployowane ani pobrane
|
|||
|
|
w ramach tego recon — wynik to wyłącznie ten dokument.
|