# wiki — jak to funguje Hybrid hledání ve tvých vlastních poznámkách: dva git repozitáře (`index`, `travel`) plus živý workspace nanobota. Skill je **výhradně čtecí** — do tvých repů nikdy nezapisuje a klony drží jen jako pracovní kopii. Index je derived artifact: kdykoli se dá smazat a postavit od nuly (u dnešního objemu jsou to desítky sekund). ## Tři vrstvy, od nejlevnější ```text grep ripgrep přes soubory na disku žádný index, nikdy zastaralé, vidí i kód ↓ toc katalog `files` z databáze adresář → soubor → titulek + tagy ↓ search FTS5 (BM25) + vec0 (KNN) → RRF parafráze, když neznáš slova ``` Není to fallback řetěz, kde se jde dolů, když horní vrstva zklame — je to volba podle **tvaru otázky**: přesný string, hostname nebo identifikátor patří do `grep`, orientace („co o tom vůbec mám?") do `toc`, parafráze do `search`. `grep` je tedy základ, ne poslední záchrana. Soubory na disku beztak leží, takže nestojí nic navíc, a nad kódem je exact match to hlavní — proto se zdrojáky vůbec neindexují (viz níž). ## Index nikdy nevzniká v tahu agenta Indexuje cron každou minutu. Agent v tahu jen **čte** hotový index. Proč takhle: `exec` tool má timeout 60 s a plný index je minuty. Indexace v odpovědi by navíc při nedostupné Ollamě dělala z čerstvého dokumentu FTS-only výsledek. Drtivá většina tiků neudělá nic a **nezapíše ani řádek**. Když je `log/wiki_sync_cron.log` prázdný, je to správný stav, ne že cron neběží. ## Co se v tom tiku vlastně děje 1. Vezme lock `wiki/.sync.lock`. Když ho drží živý běh, skončí bez výpisu. Lock po mrtvém procesu (`pid` neexistuje nebo je starší než 30 min) si vezme zpátky a zaloguje `WARN stale lock, reclaiming`. 2. **Levná detekce změn, bez indexace.** U git zdrojů `git ls-remote` — zjistí remote `HEAD` **bez** `fetch`, což je na minutovou kadenci ten správný nástroj; `fetch` teprve když se revize liší. U workspace zdroje walk a porovnání `path` + `size` + `mtime`; sha256 se počítá jen při neshodě. 3. Nic se nezměnilo a nic nečeká na vektor → konec. 4. Změněné soubory se rozřežou na chunky a pošlou do Ollamy po 32. Pak se **doberou všechny chunky bez vektoru**, i když se žádný soubor nezměnil — tohle je cesta zpátky z degradovaného režimu, když byla Ollama chvíli mimo. 5. Zapíše `indexed_rev` a `last_sync_at`, přidá coverage řádek a shrnutí do `log/wiki_sync.log`. Selhání sítě je **per zdroj**: timeout nebo chyba gitu zaloguje `WARN`, ten zdroj přeskočí s nezměněnou `indexed_rev` a ostatní dojedou. Bez timeoutů by visící `ls-remote` držel lock a zablokoval i workspace zdroj, který se sítí nemá nic společného. ## Proč hybrid Každá polovina umí něco jiného a ani jedna neumí obojí: | | Umí | Neumí | |---|---|---| | **BM25** (FTS5) | exact match, čísla, jména, zkratky | parafrázi, kde se slova nepřekrývají | | **vektory** (`vec0`) | „elektroodpad ze stavebnic" → „Mindstorms po ukončení podpory" | přesné řetězce a čísla | Výsledky slučuje **RRF** — sečte převrácené ranky z obou seznamů. Aby to mělo definovaný význam, musí obě poloviny řadit **tutéž množinu**: proto je jedinou retrieval jednotkou chunk, ne soubor. Kdyby BM25 řadil soubory a vektory chunky, merge by nešlo interpretovat. Naměřeno na reálných poznámkách: RRF dává MRR 0,367 proti 0,257 (BM25 sám) a 0,261 (vektory samy), takže merge opravdu vyhrává. Plná čísla, včetně srovnání čtyř modelů, jsou v `.claude/tracking/plans/final-wiki-hybrid-rag-result.md` v trackovacím repu. Ve výstupu `search` u každého hitu stojí, která polovina ho našla (`bm25 #2, vec #1`). Hit, na kterém se poloviny **shodnou**, má výrazně větší cenu než ten, který vytáhly jen vektory. ## Česká flexe — na co narazíš Dotazová vrstva lepí `*` na slova od tří znaků, aby pokryla české koncovky. **Funguje to jen napůl** a je dobré vědět jak: wildcard je **prefixový**, takže pomůže jen tehdy, když je tvoje slovo prefixem tvaru v dokumentu. Česká flexe ale mění koncovku, ne začátek. Naměřeno nad `travel/packaging-list.md`: | Dotaz | Chunků v tom souboru | |---|---| | `cestu*` | **0** | | `cesty*` | 3 | | `cest*` | 4 | Prakticky: na *„co si vzít na cestu do zahraničí"* se doslovný seznam věcí na cestu nedostal ani do top 10. Když víš, že něco máš, a `search` to nenajde, zkus **kmen slova** (`cest`, `záloh`) nebo rovnou `grep`. Rozhodnutí, jestli to řešit systémově, je otevřená položka v `todo.md` trackovacího repa — každá varianta rozšiřuje kandidátní pool, takže to není zdarma. ## Index je nápověda, ne odpověď Dvě věci, které se snadno přehlédnou: - **Výřez v `search` není zdroj pravdy.** Je zkrácený a index může být o minutu starší než disk. Než se z nálezu odpovídá, má se ten soubor přečíst čerstvý — `SKILL.md` to agentovi říká, ale platí to i pro tebe. - **Vektorová polovina vždycky něco vrátí.** KNN nemá dolní mez podobnosti, takže na jakýkoli dotaz nabídne svých k nejbližších chunků. Pro retrieval je to záměr (posoudit relevanci je na čtenáři), ale znamená to, že prázdný výsledek nikdy neznamená „nic nesedí" — znamená prázdný index. ## Co se indexuje a co ne Rozsah řídí `wiki/config.yaml` ve třech krocích: `paths` (whitelist — co v něm není, pro index neexistuje) → `include` (whitelist přípon, `*.md`) → `exclude` (skalpel, vyhrává nad oběma). Proč whitelist a ne blacklist: rozhoduje **směr selhání**. U blacklistu nový adresář *tiše vstoupí* do indexu, u whitelistu *tiše chybí*. Index je komprimovaná kopie obsahu včetně osobních věcí, takže „tiše zaindexováno" je horší porucha — a workspace se mění i bez tebe, Dream do něj zapisuje sám. Proti té jediné slabině whitelistu stojí záchranná síť: sync na konci zaloguje řádek `coverage : markdown outside paths in …` s top-level adresáři, které markdown mají a žádný zdroj je nepokrývá. Adresáře, které tam vidíš pořád (`backup`, `tmp`, `skills`, …), jsou vědomě vynechané; zajímavý je **nový** přírůstek v tom seznamu. **Indexuje se výhradně markdown**, a to bez jakékoli detekce typu obsahu — `main.py` se netrefí do `include: ["*.md"]` a k chunkeru se nedostane. Důvod je měřený: markdown parser dělá z Python komentáře `# TODO: …` nadpis a `unicode61` neumí identifikátory (`send` uvnitř `SendAsync` je miss). Kód pokrývá `grep` — jede přes celý klon včetně `.txt` a `.mhtml`. ## Kde co leží Relativně ke workspace (`~/.nanobot/workspace`): | Cesta | Role | |---|---| | `wiki/config.yaml` | zdroje a model — **jediný verzovaný soubor** pod `wiki/` | | `wiki/index.sqlite` | `chunks` + FTS5 + vektory; derived, gitignorovaný | | `wiki/remote//` | non-bare klony git zdrojů (working tree, aby měl `grep` co číst) | | `wiki/.sync.lock` | JSON `{pid, started_at}`, jen když sync běží | | `log/wiki_sync.log` | jeden řádek na událost — co se naindexovalo, `WARN`, coverage | | `log/wiki_sync_cron.log` | stdout cronu; **prázdný je správně** | | `skills/wiki/scripts/` | `wiki_sync.py` (indexace) a `wiki_search.py` (dotazy) | Klon je záměrně non-bare: `--mirror` nemá working tree, takže by `grep` neměl co číst. Cena je dvojnásobek místa na disku, což je na těchhle repech nic. ## Ruční spuštění ```bash cd ~/.nanobot/workspace # to, co dělá cron ~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py # jen jeden zdroj ~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py --source travel # smazat index a postavit od nuly ~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py --full # dotazy ~/.local/bin/uv run skills/wiki/scripts/wiki_search.py search "zálohování" --limit 5 ~/.local/bin/uv run skills/wiki/scripts/wiki_search.py toc --source travel ~/.local/bin/uv run skills/wiki/scripts/wiki_search.py toc --tag caj ~/.local/bin/uv run skills/wiki/scripts/wiki_search.py grep "WireGuard" --source index ``` `--full` potřebuješ jen po změně embedding modelu, jeho dimenzí, `query_prefix` nebo chunkeru — tedy přesně tehdy, když dotaz začne hlásit `reindex needed`. Přeembeduje všechno, takže to nepouštěj v tahu, kde na to někdo čeká. Plná cesta k `uv` je tu proto, že v neinteraktivním SSH není v `PATH`. Crontab si `PATH` nastavuje sám, takže tam stačí `uv run …`. ## Když něco nefunguje | Hláška | Co to je | |---|---| | `note: embeddings unavailable … FTS-only results` | Ollama na `nvidia.hell` neodpovídá. Sync mezitím indexuje dál a vektory dosadí sám, až se vrátí. | | `note: N chunks still awaiting vectors` | Sync je rozjezdu nebo byl degradovaný. Sémantická polovina je zatím neúplná. | | `reindex needed: …` (exit 1) | Index byl postavený pod jiným embedding kontraktem než má config. Poznámky jsou v pořádku, řeší to `--full`. | | `WARN : git … failed` | Ten zdroj se přeskočil, ostatní dojely. `indexed_rev` zůstala, takže příští tik to zkusí znovu. | | `(no matches)` | Prázdný index — ne „nic nesedí" (viz výš). | Všechno ostatní hledej v `log/wiki_sync.log`. Když je prázdný i po změně poznámek, problém je v cronu, ne v hledání. ## Jak ověřit, že to funguje **Smoke loop v konzoli** — projde celou smyčku od zápisu po nalezení: ```bash cd ~/.nanobot/workspace printf '# Smoke\n\nZrzavý jednorožec kontroluje wiki index.\n' > notes/_smoke.md sleep 90 ~/.local/bin/uv run skills/wiki/scripts/wiki_search.py search "zrzavý jednorožec" --limit 3 rm notes/_smoke.md # cron ho z indexu odklidí sám do minuty ``` **Dotaz do WebUI na každý zdroj.** Princip: vyber fakt, který model **nemůže** znát z obecných znalostí — pak je konkrétní správná odpověď sama důkazem, že přišla z poznámek. Když odpoví obecně a čísla vynechá nebo si je vymyslí, neprošel. Příklady platné k 2026-09-09 (poznámky se mění, princip ne): | Zdroj | Dotaz | Musí padnout | |---|---|---| | `workspace` | Kolik vody a čaje mám v receptu na Teh Tarik? | 700–800 ml, dvě polévkové lžíce čaje | | `travel` | Co si mám podle poznámek vzít na Sněžku? | rozdvojka do zásuvky, power banka, Sony ANC sluchátka | | `index` | Co je projekt Sauter Bus? | RS485 proxy s odposlechem a modifikací provozu, micro:bit | **Negativní kontrola** je nejcennější z celé sady: zeptej se na téma, které v poznámkách prokazatelně není (např. pěstování bonsají). Správná odpověď je „nic o tom nemáš". Když začne vyprávět, doplňuje si výsledky z obecných znalostí — a to je zrádnější vada než nefunkční index, protože se tváří jako odpověď z poznámek. Když si nejsi jistý, odkud odpověď je, dopiš **„ze kterého souboru to je?"**. Má přijít konkrétní cesta, ne „z tvých poznámek".