11 KiB
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ší
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
- Vezme lock
wiki/.sync.lock. Když ho drží živý běh, skončí bez výpisu. Lock po mrtvém procesu (pidneexistuje nebo je starší než 30 min) si vezme zpátky a zalogujeWARN stale lock, reclaiming. - Levná detekce změn, bez indexace. U git zdrojů
git ls-remote— zjistí remoteHEADbezfetch, což je na minutovou kadenci ten správný nástroj;fetchteprve když se revize liší. U workspace zdroje walk a porovnánípath+size+mtime; sha256 se počítá jen při neshodě. - Nic se nezměnilo a nic nečeká na vektor → konec.
- 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.
- Zapíše
indexed_revalast_sync_at, přidá coverage řádek a shrnutí dolog/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
searchnení 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.mdto 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 <zdroj>: 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/<id>/ |
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í
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 <zdroj>: 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í:
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".