331 lines
16 KiB
Markdown
331 lines
16 KiB
Markdown
# 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ší
|
||
|
||
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).
|