# 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 ..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 ..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).