plans
This commit is contained in:
328
plans/notes-search-hybrid-rag.md
Normal file
328
plans/notes-search-hybrid-rag.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Notes Search — RO git repa → hybrid RAG index
|
||||
|
||||
Stav: DRAFT — budeme ještě opracovávat, než se pustíme do realizace.
|
||||
Vznik: diskuze 2026-09-XX (nahradit přesným datem při finalizaci).
|
||||
|
||||
## Cíl
|
||||
|
||||
Uživatel vede poznámky ve vlastních git repozitářích (adresářová struktura, md
|
||||
soubory; témata: devops, traveling, mix všeho možného). Nanobot dostane
|
||||
**read-only přístup** k mirrorům těchto repů a postaví nad nimi index, který
|
||||
umožní rychlé hledání — tematické i syntaktické. Poznámky zůstávají
|
||||
kanonickými daty v gitu; index je jen derived artifact, regenerovatelný.
|
||||
|
||||
Repozitáře jsou read-only z pohledu agenta — nikdy nic nezapisujeme do
|
||||
uživatelových repů, všechno derived žije ve workspace nanobota.
|
||||
|
||||
## Vstupní fakta a omezení (ověřené v diskuzi)
|
||||
|
||||
- **HW pro embedding inference**: RTX 4060/16 GB na serveru `nvidia.hell`
|
||||
— **fyzicky jiný host** než běžiště nanobota. Embedding je network call
|
||||
na Ollama HTTP API, ne lokální volání. Dosažitelnost endpointu zatím
|
||||
neověřena (TODO před realizací).
|
||||
- Uživatel sám navrhl dodat embeddings přes vlastní Ollama server —
|
||||
embedding inference tedy není omezující faktor.
|
||||
- Škála datasetu: pár repů, řádově stovky až tisíce md souborů,
|
||||
po chunkování ~10⁴–10⁵ chunků.
|
||||
- Obsah převážně česky + anglicky.
|
||||
- Uživatel ukládá i embedding vektory — potřebujeme vektorové úložiště,
|
||||
otázka je jen jaké (viz rozhodnutí D3).
|
||||
|
||||
## Architektura
|
||||
|
||||
```
|
||||
Zdroje:
|
||||
git mirrory (RO clone/fetch, keyed na indexed-rev)
|
||||
+ nanobot workspace (živá data, scan path+size+mtime)
|
||||
→ ingest driver (git-diff / fs-scan — dva drivery, jeden indexer)
|
||||
→ chunker (md → chunky po heading struktuře)
|
||||
→ embeddings: Ollama API (qwen3-embedding:0.6b) — jen změněné chunky
|
||||
→ SQLite: catalog + FTS5 tabulka + embedding BLOB sloupce
|
||||
→ query: hybrid search (BM25 via FTS5 + cosine), top-k merge (RRF)
|
||||
```
|
||||
|
||||
Konfigurace zdrojů: `config/notes-search.yaml` (viz sekce Konfigurace).
|
||||
|
||||
Jeden SQLite soubor (`db/notes-index.sqlite` dle konvence AGENTS.md:
|
||||
databáze do `db/*.sqlite`), který nese:
|
||||
- **catalog**: `repo, path, title, tags, headings, sha, indexed_at`
|
||||
- **FTS5 tabulka**: fulltext s BM25 rankingem (exact/lexikální půlka)
|
||||
- **embedding tabulka**: `path, chunk_idx, text, embedding BLOB`
|
||||
(sémantická půlka; BLOB + numpy cosine brute-force)
|
||||
|
||||
## Hledání — tři vrstvy, od nejlevnější
|
||||
|
||||
1. **Syntakticky — live grep** (ripgrep přes mirror): exact match,
|
||||
názvy souborů, hostname, tagy, čísla. Žádný index potřeba, vždy
|
||||
aktuální. Základ, ne fallback.
|
||||
2. **Tematicky — vygenerovaný TOC** (`toc.md` per repo: kategorie →
|
||||
soubor → jedna řádka; title + tagy + headings). Index-first navigace
|
||||
jako v llm-wiki, ale generovaná, ne LLM-kurátorovaná.
|
||||
3. **Fuzzy/sémanticky — hybrid search**: FTS5 (BM25) + embeddings
|
||||
(cosine), merge přes RRF. Používá se, když TOC browsing nezabere.
|
||||
|
||||
## Incrementální update (nové dokumenty ve zdrojích)
|
||||
|
||||
1. `git fetch` mirroru; `git diff --name-status <indexed-rev>..HEAD`
|
||||
→ seznam přidaných/změněných/smazaných/přejmenovaných md souborů.
|
||||
2. Chunker zpracuje jen změněné soubory; staré chunky daného `path`
|
||||
se smažou (`DELETE WHERE path = ?`), nové se vloží.
|
||||
3. Embedding: jen změněné chunky → Ollama API → upsert do SQLite.
|
||||
Nový soubor = pár desítek chunků, sekundy.
|
||||
4. FTS5 sync ve stejné transakci.
|
||||
5. Commit `indexed-rev = HEAD`; smazaný soubor = delete chunků + FTS.
|
||||
|
||||
Vlastnosti: **idempotentní** (re-run nad stejnou revizí = no-op),
|
||||
degradovaný režim (Ollama nedostupná → FTS5 funguje dál, chunky
|
||||
bez vektoru poznáme přes `embedding IS NULL`, dosadí se při dalším
|
||||
syncu). Full re-index jen při změně embedding modelu.
|
||||
|
||||
## Rozhodnutí a důvody (včetně diskuze)
|
||||
|
||||
### D1: Embeddings ano — jako vrstva nad BM25, ne místo něj
|
||||
|
||||
Diskuze: nejprve navržen „zero embedding" stack (grep + TOC + FTS5).
|
||||
Uživatel reagoval, že embeddings může dodat přes vlastní Ollama server,
|
||||
a upřesnil, že embedding vektory se někam musí ukládat.
|
||||
|
||||
Embeddings přidávají nad BM25 dvě věci: parafráze a cross-jazyk
|
||||
(„prodloužení životnosti LEGO" najde „e-waste, Mindstorms po ukončení
|
||||
podpory" — žádná slova se nepřekrývají, BM25 selže) a chunk-level
|
||||
relevance (najde konkrétní odstavec v dlouhém souboru, ne jen soubor).
|
||||
Nevyřeší ale exact match a čísla — proto **hybrid**: FTS5 pro
|
||||
lexikální, embeddings pro sémantickou stranu, top-k merge (RRF).
|
||||
Embeddings jsou doplněk, ne náhrada.
|
||||
|
||||
### D2: Embedding model — Qwen3-Embedding-0.6B
|
||||
|
||||
Kritéria (od uživatele): běží na 4060/16 v jeho lokálním serveru,
|
||||
rozumná velikost uložení vektorů.
|
||||
|
||||
- **VRAM ~1.2 GB** — pohodové vedle dalších modelů na Ollamě.
|
||||
- **32k context** — dlouhé dokumenty bez omezování chunkování.
|
||||
- **Multilingual, top MTEB ve své třídě** — čeština first-class,
|
||||
klíčové pro české poznámky.
|
||||
- **1024 dims** — ~10⁵ chunků × 1024 × 4 B ≈ 400 MB v SQLite. Pohodové.
|
||||
Podporuje Matryoshka truncation (512/256/128 dims) kdyby storage
|
||||
vadil — u této škály netřeba.
|
||||
|
||||
Alternativy zvažované a zamítnuté:
|
||||
- **nomic-embed-text** (137M params, nejpoužívanější na Ollamě) — EN-centric,
|
||||
pro češtinu nevýhodný.
|
||||
- **bge-m3** (567M) — multilingual a umí z jednoho modelu dense+sparse
|
||||
hybrid; ale přes Ollama embeddings API jde dostat jen dense výstup,
|
||||
takže výhoda sparse v našem stacku mizí (FTS5 dělá tutéž roli levněji).
|
||||
|
||||
Poznámka k jistotě: čísla a pozice z web-searchů (MTEB srovnání),
|
||||
ne osobní benchmark. Před realizací ověřit na reálných datech
|
||||
(dotaz „e-waste" vs „prodloužení životnosti" atp.).
|
||||
|
||||
**Volba modelu je vážící rozhodnutí** — změna = re-embed celého
|
||||
datasetu. Při ~10⁵ chunků na 4060 je full re-index otázkou minut,
|
||||
přijatelné, ale ne měnit z rozmaru.
|
||||
|
||||
### D3: Vektorové úložiště — embeddings jako BLOB v SQLite
|
||||
|
||||
Diskuze: já jsem zpočátku tvrdil „vektorovou databázi nepotřebuješ";
|
||||
uživatel oprávněně namítl, že **sqlite-vec je svým způsobem taky
|
||||
vektorová databáze, i když embedded** — a že nikdy neříkal, že buduje
|
||||
externí službu, jen že vektory někam ukládat potřebuje. Souhlas:
|
||||
šlo o spor o pojem, ne o věc.
|
||||
|
||||
Na škále ~10⁴–10⁵ vektorů je brute-force cosine v numpy pod 100 ms;
|
||||
ANNS indexy (HNSW apod.) dávají smysl nad ~10⁶ vektorů. Konkrétní volby:
|
||||
|
||||
| | Embeddings v SQLite (BLOB) | sqlite-vec (`vec0`) | Chroma/Qdrant/pgvector |
|
||||
|---|---|---|---|
|
||||
| Tvar | BLOB sloupec, cosine v Pythonu | virtuální tabulka, KNN v SQL | samostatný service/DB |
|
||||
| Vyhledávání | načti vše, spočítej top-k | `MATCH` operátor, indexovaný | KNN server-side |
|
||||
| Infra | nic | load extension | server, jiná backup story |
|
||||
| Kdy se vyplatí | do ~10⁵ vektorů | ~10⁵–10⁶, metadata filter + KNN v jednom dotazu | >10⁶ vektorů |
|
||||
|
||||
Volba: **BLOB v SQLite nyní**, schéma navrženo tak, aby migrace na
|
||||
sqlite-vec byla mechanická (import jednoho sloupce do `vec0` tabulky,
|
||||
žádná regenerace). Externí vektorové DB zamítnuty — řeší problém,
|
||||
který na této škále neexistuje (YAGNI).
|
||||
|
||||
### D4: Chunking — strukturně vědomý, po heading hierarchii
|
||||
|
||||
- Rozdělení po heading sekcích (H1–H3), každý chunk nese breadcrumb
|
||||
(`soubor > sekce > podsekce`) v metadatech i v embedding textu —
|
||||
vektor tak nesou kontext, ne izolovaný odstavec.
|
||||
- Merge malých sousedních sekcí pod stejným rodičem (< ~200 tokenů).
|
||||
- Split velkých sekcí (> ~800 tokenů) po odstavcích s overlap ~50–80
|
||||
tokenů. Cílová granularita ~200–800 tokenů per chunk; menší chunk =
|
||||
přesnější hit na místo v dokumentu. Qwen3-Embedding má 32k ctx,
|
||||
takže se krčit nemusíme.
|
||||
- Frontmatter tagy zůstávají v katalogu (na filtrování), do embedding
|
||||
textu jde jen title.
|
||||
- Kód bloky a tabulky se nerozbíjejí (drží pohromadě); checklistové
|
||||
soubory chunkujeme po bullet blocích.
|
||||
- Token counting: aproximace ~4 znaky/token (přesný tokenizer pro
|
||||
chunk sizing netřeba, cílové okno je široké).
|
||||
|
||||
Proč strukturní: uživatelovy poznámky jsou md se smysluplnou strukturou
|
||||
(adresáře = témata, soubory, H1–H3 sekce); slepování přes hranice sekcí
|
||||
by embeddings kazilo. Fixed-size sliding window zamítnuta.
|
||||
|
||||
### D5: Knihovny — markdown-it-py, ne vlastní parser a ne frameworky
|
||||
|
||||
Diskuze: původní návrh byl „100 řádků vlastního regex chunkeru,
|
||||
stdlib only". Uživatel zpochybnil, dává smysl psát si všechno sám.
|
||||
Výsledek revize — rozlišit dva případy:
|
||||
|
||||
- **Parser do knihovny**: vlastní regex zná CommonMark edge cases
|
||||
(setext headings `===`, nested listy, HTML bloky) jen do té míry,
|
||||
do jaké si je ošetříš. markdown-it-py je malá, stabilní, zero-bloat
|
||||
(jen `mdurl`), dává proper AST. Chunkovací **politika** (split body
|
||||
vs merge prahy, breadcrumb) ale zůstává vlastní (~50 řádků nad
|
||||
tokeny). Původní „vlastní regex" byla varianta šetřící na špatném
|
||||
místě — parsery patří do knihoven.
|
||||
- **Glue kód neobírat frameworkem**: sync orchestrace, katalog,
|
||||
hybrid search — tady neexistuje „malá dobrá knihovna", nabídka je
|
||||
binární: langchain / llama-index (frameworky, stovky MB, abstrakce
|
||||
nad sqlite3/subprocess/HTTP, API se mění pod rukama) nebo vlastní
|
||||
~500 řádků. „Psát si sám" tady není NIH syndrom, je to jediná
|
||||
racionální volba, protože alternativou je framework obalující
|
||||
4 stdlib/utility volání.
|
||||
|
||||
Výsledný externí dependency set: **markdown-it-py, requests, numpy**.
|
||||
Vše malé, stabilní, žádný framework. Konkrétní volby per část:
|
||||
|
||||
| Část | Řešení | Proč |
|
||||
|---|---|---|
|
||||
| MD parsing + chunking | markdown-it-py + vlastní split politika | parser hotový/testovaný, politika naše |
|
||||
| Git sync | subprocess + git CLI | nic knihovního netřeba |
|
||||
| Katalog/FTS5/BLOB | sqlite3 (stdlib) | nic k přidání |
|
||||
| Ollama client | requests | pár řádků, knihovna nic nepřidá |
|
||||
| Hybrid merge (RRF) | numpy | ~20 řádků |
|
||||
| Config (notes-search.yaml) | pyyaml | ruční editace + komentáře; TOML zamítnut uživatelem |
|
||||
|
||||
### D6: FTS5 tokenizer — unicode61, s vědomím české mezer
|
||||
|
||||
`unicode61` dělá diakritiku-case-folding („ZALOŽIT" najde „založit"),
|
||||
ale nemá stemming — „záloha" nenajde „zálohování". Nástřel: doufat,
|
||||
že BM25 + embeddings hybrid mezeru překryjí (u hybridu obvykle ano);
|
||||
záložní volba trigram tokenizer (SQLite 3.34+) pro substring matching,
|
||||
kdyby se ukázalo, že hybrid nestačí. Rozhodnutí odloženo na testování
|
||||
na reálných datech.
|
||||
|
||||
### D7: Oddělené patterny — notes search ≠ llm-wiki
|
||||
|
||||
llm-wiki (`cml/`) je kurátorovaný knowledge store pro cílené
|
||||
ingestování cizích zdrojů (LLM píše entity/concept pages). Notes
|
||||
search je **search index nad existujícími uživatelskými poznámkami**
|
||||
— uživatel je strukturoval sám, LLM-kurátorovaná druhá vrstva by
|
||||
byla duplikace. Dva patterny, nemíchat; adresářová struktura repů je
|
||||
primární tematický index, který jen zpřístupňujeme (TOC), nereimplementujeme.
|
||||
|
||||
Kontext: současný obsah llm-wiki (7 zdrojů o coding agentech + 1 o
|
||||
LEGO Mindstorms) uživatel hodnotí jako „nic moc, bude to chtít pojmout
|
||||
úplně jinak" — tohle řešení je odpovědí na ten směr.
|
||||
|
||||
### D8: Mirror jako zdroj, fetch-on-query vs cron sync
|
||||
|
||||
Git repa jsou kanonická data; index je derived. Mirror přes
|
||||
`git clone --mirror` / fetch. Staleness mirroru je jediné reálné
|
||||
riziko — mitigace: fetch-on-query (fetch vteřiny, voláno před
|
||||
dotazem) s idempotentním index update; cron/heartbeat sync volitelně
|
||||
navíc. Incremental reindex jen změněných souborů (typicky pár
|
||||
souborů per sync).
|
||||
|
||||
## Ingest zdrojů — dva drivery, jeden indexer
|
||||
|
||||
Index přijímá dva typy zdrojů, podle toho se liší jen detekce změn;
|
||||
chunker, embedding, ukládání i query vrstva jsou společné.
|
||||
|
||||
### Driver: git (RO mirrory)
|
||||
|
||||
- Detekce změn: `git fetch` + `git diff --name-status <indexed-rev>..HEAD`
|
||||
(R-status = rename → přesun záznamů, M = reindex, D = delete).
|
||||
- Uložená `indexed-rev` per source v katalogu je vstupní bod.
|
||||
- Fetch-on-query (fetch vteřiny) před dotazem; idempotentní update.
|
||||
|
||||
### Driver: nanobot workspace (živá data)
|
||||
|
||||
- Detekce změn: walk stromu + porovnání `(path, size, mtime)` proti
|
||||
katalogu; content hash (SHA-256) jako autorita, mtime jako rychlý
|
||||
pre-filter. Žádný git, žádný rev — soubory se mění pod rukama
|
||||
(Dream přepisuje MEMORY.md, skilly appendují logy).
|
||||
- Scan-on-query místo fetch-on-query (walk workspace je levný).
|
||||
- Nekonzistence v čase nevadí: index je retrieval hint, ne source of
|
||||
truth — před odpovědí se soubor vždy přečte čerstvý z disku.
|
||||
- Embeddings tabulka je de-facto komprimovaná kopie obsahu (i keep.md,
|
||||
osobní věci) v jednom SQLite souboru — vše lokální (vlastní Ollama,
|
||||
žádná třetí služba), ale uvědomit si to v backup story.
|
||||
- Vyloučené z indexace (rozumný start, doladíme měřením): `MEMORY.md`,
|
||||
`SOUL.md`, `USER.md` (krátké, vždy v kontextu — jen noise), `tmp/`,
|
||||
`src/` (git klony), binárky, `history.jsonl` (obří, low signal).
|
||||
|
||||
## Konfigurace zdrojů (YAML)
|
||||
|
||||
Formát rozhodnut v diskuzi: **YAML** (ne TOML — uživatel výslovně
|
||||
zamítl; zapsáno do USER.md). Důvod volby YAML nad JSON: config se
|
||||
edituje ručně (uživatel při přidání repa), komentáře v configu mají
|
||||
hodnotu; JSON komentáře nemá. `pyyaml` přidán do dependency set.
|
||||
|
||||
Jeden soubor `config/notes-search.yaml`, dvě sekce podle typu zdroje.
|
||||
**Klíč sekce = source id** (stabilní, visí na něm katalog i embeddingy;
|
||||
URL/path se můžou změnit, klíč ne; rename = explicitní invalidace indexu
|
||||
daného zdroje — chceme pod kontrolou, ne implicitní).
|
||||
|
||||
```yaml
|
||||
# Git zdroje — RO mirrory. Klíč = source id.
|
||||
sources:
|
||||
git:
|
||||
travel: # source id (stabilní)
|
||||
url: git@host:travel-notes.git
|
||||
mirror: tmp/mirrors/travel.git # kam clone --mirror
|
||||
devops:
|
||||
url: https://host/devops-notes.git
|
||||
mirror: tmp/mirrors/devops.git
|
||||
|
||||
# Nanobot workspace zdroje — glob vzory
|
||||
nanobot:
|
||||
notes:
|
||||
paths: ["notes/**"]
|
||||
include: ["*.md"] # default *.md
|
||||
exclude: [] # volitelné
|
||||
projects:
|
||||
paths:
|
||||
- "projects/**"
|
||||
- "cook/**"
|
||||
- "knowledge/**"
|
||||
- "plans/**"
|
||||
- "results/**"
|
||||
include: ["*.md"]
|
||||
```
|
||||
|
||||
Pravidla:
|
||||
- `paths` = glob vzory (vyjmenovat lze celý adresář i podstrom).
|
||||
- `include` default `*.md`; jiné přípony explicitně až kdyby.
|
||||
- `exclude` deny-list přidáme až když se šum objeví (YAGNI).
|
||||
- Source id nesmí kolidovat mezi `git` a `nanobot` sekcemi —
|
||||
katalog je sdílený (`source` sloupec).
|
||||
|
||||
## Otevřené otázky (před realizací)
|
||||
|
||||
1. **Adresa/endpoint Ollamy na nvidia.hell** — dohledat v projects/
|
||||
keep.md nebo od uživatele; ověřit reachability z runtime nanobota.
|
||||
2. **Benchmark embedding modelu na reálných datech** — ověřit MTEB
|
||||
pozice na pár českých dotazech (parafráze cross-jazyk test).
|
||||
3. **Seznam rep** — která repa, kde mirrorovat, velikost/historie
|
||||
(vliv na fetch čas).
|
||||
4. **Trigger syncu** — fetch/scan-on-query, heartbeat, nebo obojí.
|
||||
5. **Query interface** — jak se nanobota ptát: ad-hoc dotazy v chatu,
|
||||
skill (`search my notes`), nebo obojí.
|
||||
6. **Vyloučené workspace cesty** — potvrdit výchozí blacklist
|
||||
(MEMORY.md/SOUL.md/USER.md, tmp/, src/, history.jsonl) měřením.
|
||||
7. **Presné datum vzniku plánu** — doplnit.
|
||||
|
||||
## Nezávislá rozhodnutí (odloženo, YAGNI)
|
||||
|
||||
- Embeddings až když fuzzy dotazy selžou — **zamítnuto v diskuzi**,
|
||||
embeddings jsou součástí od začátku (uživatel je dodává přes Ollamu).
|
||||
- Externí vektorová DB (Chroma/Qdrant/pgvector) — zamítnuta (D3).
|
||||
- Entity/concept pages jako v llm-wiki — zamítnuty (D7).
|
||||
- Full re-index strategie mimo změnu modelu — netřeba (idempotentní
|
||||
incremental, D-sync).
|
||||
Reference in New Issue
Block a user