16 KiB
Notes Search — RO git repa → hybrid RAG index
Superseded by
final-wiki-hybrid-rag.md(2026-09-09) — tento draft je historie.
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ší
- 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.
- Tematicky — vygenerovaný TOC (
toc.mdper repo: kategorie → soubor → jedna řádka; title + tagy + headings). Index-first navigace jako v llm-wiki, ale generovaná, ne LLM-kurátorovaná. - 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)
git fetchmirroru;git diff --name-status <indexed-rev>..HEAD→ seznam přidaných/změněných/smazaných/přejmenovaných md souborů.- Chunker zpracuje jen změněné soubory; staré chunky daného
pathse smažou (DELETE WHERE path = ?), nové se vloží. - Embedding: jen změněné chunky → Ollama API → upsert do SQLite. Nový soubor = pár desítek chunků, sekundy.
- FTS5 sync ve stejné transakci.
- 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 (jenmdurl), 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-revper 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í).
# 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).includedefault*.md; jiné přípony explicitně až kdyby.excludedeny-list přidáme až když se šum objeví (YAGNI).- Source id nesmí kolidovat mezi
gitananobotsekcemi — katalog je sdílený (sourcesloupec).
Otevřené otázky (před realizací)
- Adresa/endpoint Ollamy na nvidia.hell — dohledat v projects/ keep.md nebo od uživatele; ověřit reachability z runtime nanobota.
- Benchmark embedding modelu na reálných datech — ověřit MTEB pozice na pár českých dotazech (parafráze cross-jazyk test).
- Seznam rep — která repa, kde mirrorovat, velikost/historie (vliv na fetch čas).
- Trigger syncu — fetch/scan-on-query, heartbeat, nebo obojí.
- Query interface — jak se nanobota ptát: ad-hoc dotazy v chatu,
skill (
search my notes), nebo obojí. - Vyloučené workspace cesty — potvrdit výchozí blacklist (MEMORY.md/SOUL.md/USER.md, tmp/, src/, history.jsonl) měřením.
- 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).