28 KiB
Final Wiki — hybrid RAG index nad poznámkami
Stav: připraveno k implementaci. Vznik: 2026-09-09.
Nahrazuje draft notes-search-hybrid-rag.md (server, 2026-09-09) — ten je superseded.
Veškerá čísla v tomto dokumentu jsou naměřená na reálném prostředí
(nanobot.hell → nvidia.hell, 2026-09-09), ne odhadnutá. Kde jde o odhad, je to napsané.
Cíl
Uživatel vede poznámky ve vlastních git repozitářích (adresářová struktura, md soubory; témata devops, traveling, mix). Nanobot dostane read-only přístup k mirrorům těchto repů a postaví nad nimi index pro rychlé hledání — tematické i syntaktické. Druhým zdrojem je nanobot workspace (živá data).
Poznámky zůstávají kanonickými daty v gitu; index je derived artifact, regenerovatelný. Do uživatelových repů se nikdy nezapisuje.
Skill je plně samostatný — žádná závislost na jiném nanobot skillu.
Ověřená fakta o prostředí
| Co | Naměřeno |
|---|---|
Ollama endpoint http://nvidia.hell:11434 |
dosažitelný z nanobota |
qwen3-embedding:0.6b |
nainstalován; 595,78M params, 1024 dims, capability embedding |
| Cold load modelu / warm | 1,81 s / 0,043 s |
| Embed throughput | 8,9 chunk/s po jednom → 108 chunk/s v batchi 32 (12×) |
keep_alive tvar |
číslo -1 i "24h" → 200; string "-1" → HTTP 400 |
| Ollama Cloud embeddingy | neexistují — 18 cloud modelů, žádný s capability embedding; /api/embed na cloud modelu vrací unauthorized, zatímco /api/generate na tomtéž projde |
| SQLite | 3.46.1; FTS5 unicode61/trigram/porter; enable_load_extension funguje |
sqlite-vec |
v0.1.6, vec0(float[1024] distance_metric=cosine) v témže souboru jako chunks+FTS5 |
vec0 KNN k=20 |
19 ms @ 10k chunků, 212 ms @ 100k (BLOB+numpy: 594 ms) |
vec0 zápis |
10k = 2,1 s; 100k = 25,3 s; soubor 466 MB (BLOB 461 MB — bez režie) |
vec0 mutace |
DELETE/re-INSERT/UPDATE po rowid fungují; ROLLBACK je transakční |
| BLOB+numpy rozpad @ 100k | read 382 ms + pack 190 ms + dot 22 ms → 96 % je Python režie |
| FTS5 čeština | remove_diacritics 1+ foldí (zaloha najde záloha); záloh* najde záloha/zálohování/zálohy |
| Reálný korpus workspace | 234 md mimo tmp/ (1,28 MB) → ~800 chunků; z toho použitelných ~99 souborů |
exec tool timeout |
60 s |
db/ ve workspace |
gitignorováno → index je automaticky mimo git |
Retrieval kvalita — měřeno na 178 chunkách reálného obsahu:
| Dotaz | BM25 | embeddingy |
|---|---|---|
| „prodloužení životnosti LEGO" | 4/5 | 5/5 |
| „jak snížit elektroodpad ze stavebnic" | 1/5 | 3/5 |
Druhý řádek je přesně ten parafrázový/cross-jazyk případ, pro který tu hybrid je: BM25 selže, vektory najdou. První řádek je opačný — lexikálně snadný dotaz zvládne BM25. Obě poloviny si vydělávají, což potvrzuje D1 měřením, ne argumentem.
Architektura
Klíčová vlastnost: chunks je jediná retrieval jednotka. RRF slučuje dva ranky téže
množiny — kdyby BM25 řadil soubory a vektory chunky, merge by neměl definovaný význam.
Zdroje:
git mirrory (RO clone/fetch, keyed na indexed-rev)
nanobot workspace (walk path+size+mtime)
↓ ingest driver (git-diff / fs-scan — dva drivery, jeden indexer)
↓ chunker (md → chunky po heading struktuře, markdown-it-py)
↓ embeddings: Ollama /api/embed (qwen3-embedding:0.6b), batch 32, jen změněné chunky
↓
db/notes-index.sqlite:
chunks (kanonické: metadata + text + stav embeddingu)
├── chunks_fts FTS5 external-content → BM25 rank nad chunks.id
└── vec_chunks vec0 virtual table → KNN rank nad chunks.id (rowid = chunks.id)
↓
RRF merge nad chunks.id
Tenhle tvar drží vec0 vyměnitelné: kdyby pre-v1 breaking change zabolel, přidá se
embedding BLOB zpět do chunks a nic jiného se nemění.
Schéma
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
-- embedding_model, embedding_dims, normalized, query_prefix,
-- chunker_version, schema_version, sqlite_vec_version
CREATE TABLE IF NOT EXISTS sources (
source_id TEXT PRIMARY KEY, -- klíč z YAML, stabilní
kind TEXT NOT NULL CHECK(kind IN ('git','workspace')),
indexed_rev TEXT, -- jen git driver
last_sync_at TEXT
);
CREATE TABLE IF NOT EXISTS files (
source_id TEXT NOT NULL REFERENCES sources(source_id),
path TEXT NOT NULL, -- relativní ke zdroji
title TEXT, tags TEXT, headings TEXT, -- tags/headings jako JSON array
sha256 TEXT NOT NULL,
size INTEGER NOT NULL,
mtime REAL, -- rychlý pre-filter workspace driveru
indexed_at TEXT NOT NULL,
PRIMARY KEY (source_id, path)
);
CREATE TABLE IF NOT EXISTS chunks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id TEXT NOT NULL,
path TEXT NOT NULL,
chunk_idx INTEGER NOT NULL,
breadcrumb TEXT NOT NULL, -- "soubor > sekce > podsekce"
text TEXT NOT NULL,
embedded_at TEXT, -- NULL = čeká na vektor (degradovaný režim)
UNIQUE (source_id, path, chunk_idx),
FOREIGN KEY (source_id, path) REFERENCES files(source_id, path) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_chunks_pending ON chunks(id) WHERE embedded_at IS NULL;
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
breadcrumb, text,
content='chunks', content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
INSERT INTO chunks_fts(rowid, breadcrumb, text) VALUES (new.id, new.breadcrumb, new.text);
END;
CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
INSERT INTO chunks_fts(chunks_fts, rowid, breadcrumb, text)
VALUES('delete', old.id, old.breadcrumb, old.text);
END;
CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
INSERT INTO chunks_fts(chunks_fts, rowid, breadcrumb, text)
VALUES('delete', old.id, old.breadcrumb, old.text);
INSERT INTO chunks_fts(rowid, breadcrumb, text) VALUES (new.id, new.breadcrumb, new.text);
END;
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(
embedding float[1024] distance_metric=cosine
);
Poznámky ke schématu, všechny ověřené smoke testem:
- Klíč je
(source_id, path, chunk_idx), nikdy jenpath—DELETE WHERE path = ?by mazalo chunky cizího zdroje. - Stav embeddingu je sloupec
chunks.embedded_at, ne absence řádku vevec_chunks. Uvec0se „chybějící vektor" dotazuje blbě, a degradovaný režim potřebuje levnýWHERE embedded_at IS NULL(proto ten partial index). - PAST:
ON DELETE CASCADEuklidíchunksichunks_fts(trigger se na kaskádě spustí), ale řádek vevec_chunksosiří —vec0není cílem foreign key. Indexer musí mazatvec_chunksexplicitně. Patří to do regresního testu. - Vektory se ukládají L2-normalizované (
meta.normalized), takže cosine == dot product. sqlite3.connect(path, isolation_level=None)— autocommit, transakce řízené explicitně. Bez tohoBEGINspadne na „cannot start a transaction within a transaction".
Chunking
Strukturně vědomý, po heading hierarchii — uživatelovy poznámky jsou md se smysluplnou strukturou (adresáře = témata, soubory, H1–H3 sekce), takže slepování přes hranice sekcí by embeddingy kazilo.
- Rozdělení po heading sekcích (H1–H3); každý chunk nese breadcrumb (
soubor > sekce > podsekce) v metadatech i v embedding textu — vektor tak nese 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ů/chunk.
- Kód bloky a tabulky se nerozbíjejí; checklistové soubory chunkujeme po bullet blocích.
- Frontmatter tagy jdou do
files.tags(na filtrování), do embedding textu jde jen title. - Token counting: aproximace ~4 znaky/token (přesný tokenizer pro sizing netřeba).
Parser je markdown-it-py (zná CommonMark edge cases — setext headings, nested listy, HTML bloky), chunkovací politika je vlastní (~50 řádků nad tokeny).
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, vždy aktuální. Základ, ne fallback.
- Tematicky — generovaný TOC (
toc.mdper zdroj: kategorie → soubor → jedna řádka; title + tagy + headings). Index-first navigace, generovaná syncem. - Fuzzy/sémanticky — hybrid: FTS5 (BM25) +
vec0(KNN), merge přes RRF.
Query kontrakt
- Instruct prefix: dotaz embedovat jako
Instruct: <task>\nQuery: <text>, dokumenty bez prefixu (Qwen3-Embedding je asymetrický instruct-tuned model; Ollama prefix nepřidá). Měření ale ukázalo, že to není kritická vlastnost: recall@5 byl s prefixem i bez něj identický (5/5 vs 5/5, 3/5 vs 3/5) a absolutní similarita s prefixem dokonce nižší. Prefix zlepšil jen top-1 na nejtěžším dotazu (e-waste-reduction.mdmístosoftware-preservation.md). Zavádíme ho, protože je zdarma a na hraničním dotazu pomohl — a hlavně proto, že musí být bitově identický při indexaci i dotazu, což je skutečný důvod, proč hodnota žije vmeta.query_prefix. - FTS5 termy s prefix wildcardem (
záloh*) — pokrývá českou flexi, kterouunicode61nestemuje. Ověřeno:záloh*izaloh*najdou záloha/zálohování/zálohy. - RRF se dělá v Pythonu nad dvěma seznamy
chunks.id(vec0KNN vyžadujek). - Mismatch
metavs. config (jiný model/dims/prefix/chunker_version) → dotaz odmítnout s „reindex needed". Nikdy tiše nemíchat vektory ze dvou modelů. - Ollama nedostupná → FTS-only a říct to ve výstupu, ne tiše degradovat.
Provozní model — offline sync
Indexace nikdy neběží v tahu agenta. exec má timeout 60 s a plný index ~10⁴ chunků při
108 chunk/s je ~77 s. Agent v tahu jen čte hotový index.
cron: * * * * * (vzor: existující remind_send / note_compile řádky; PATH v hlavičce crontabu)
↓
notes_sync.py
1. lock: db/.notes-sync.lock (O_EXCL). Držený → exit 0 bez výpisu. Žádný souběh.
2. levná detekce změn, BEZ indexace:
git zdroje: git ls-remote <url> HEAD vs. sources.indexed_rev (síť, ne fetch)
workspace zdroje: walk + (path, size, mtime) vs. files (sha256 jen na mismatch)
3. nic se nezměnilo → exit 0 (běžný případ, drtivá většina tiků)
4. změněné soubory: chunk → embed (batch 32, keep_alive -1) → upsert v transakci
smazaný/přejmenovaný soubor: DELETE z files (kaskáda uklidí chunks+FTS)
+ EXPLICITNÍ DELETE z vec_chunks
5. update sources.indexed_rev / last_sync_at
6. coverage report: zaloguj top-level adresáře s *.md, které nepokrývá žádný source
7. log do log/notes_sync.log
git ls-remoteje pro minutovou kadenci správný nástroj — zjistí remote HEAD bezfetch.fetchteprve když se rev liší.- Idempotentní: re-run nad stejnou revizí = no-op.
- Lock řeší souběh sám, žádná externí orchestrace.
- Plný re-index = tentýž skript s
--full. Vzácná operace (změna modelu nebo chunkeru). keep_alive: -1na embed requestech drží model resident. Není kritické (cold load je 1,81 s), ale je to zdarma. Pozn.: pod tlakem na VRAM od velkých chat modelů (gemma4:12b má 7,5 GB) může scheduler potřebovat místo — při 639 MB je to nepravděpodobné, ale garance to není.
Rozsah indexace — whitelist primárně, blacklist jako skalpel
Precedence: paths (whitelist — co v něm není, pro index neexistuje) → include
(whitelist přípon, default *.md) → exclude (skalpel, vyhrává nad oběma).
Proč whitelist ve workspace — směr selhání. U blacklistu nový adresář tiše vstoupí do indexu, u whitelistu tiše chybí. Embedding tabulka je de facto komprimovaná kopie obsahu (včetně osobních věcí), takže „tiše zaindexováno" je horší porucha. A workspace se mění autonomně — Dream přepisuje soubory, skilly appendují, cron zapisuje.
Doloženo měřením: ve workspace je 234 md mimo tmp/ a použitelných je ~99. Zbytek je
cml/ 39, skills/ 32, .venv/ 27, backup/ 20, tasks/ 16, .pytest_cache 1.
A tmp/ drží dalších 135 md (git klony, 5× reflect dump po ~200 kB). Blacklist by musel
hned první den správně pokrýt pět a půl adresáře a zůstat správný navždy — a nejhorší z nich
je právě ten, který je určený k tomu, aby se v něm hromadil balast.
Proč opt-out u git rep. Uživatelova poznámková repa jsou kurátorovaná a homogenní;
vyjmenovávat v nich podadresáře je zbytečná friction. Tam paths: ["**"] a malý exclude.
Co se indexuje
| Zdroj | Cesty |
|---|---|
workspace |
notes/**, projects/**, plans/**, knowledge/**, results/**, cook/** |
develop |
develop/** mimo develop/history.md |
| git zdroje | celé repo, *.md |
results/ (21 souborů, 256 kB výstupů deep-research) a develop/knowledge.md (113 kB hutných
ověřených faktů o instanci) jsou vědomé přírůstky — whitelist z nich dělá rozhodnutí.
Co se neindexuje a proč
| Cesta | Důvod |
|---|---|
tmp/ (135 md) |
git klony + reflect dumpy po 200 kB; adresář určený k balastu |
.venv/ (27), .pytest_cache/, .ruff_cache/ |
dokumentace balíčků a cache |
backup/ (20) |
near-duplicate kopie indexovaného obsahu — otrávily by top-k redundantními hity; to je horší porucha než chybějící dokument |
develop/history.md (331 kB) |
append-only deník; ~200 chunků repetitivní narativy = ~25 % indexu při nízké hustotě signálu. develop/knowledge.md vedle něj zůstává |
skills/** (32) |
instrukce pro agenta, ne znalosti; agent si skilly načítá sám |
cml/ (39) |
llm-wiki, ruší se mimo tento plán |
root AGENTS.md/SOUL.md/USER.md/keep.md/HEARTBEAT.md, memory/MEMORY.md |
vždy v kontextu nebo triviálně krátké → čistý šum |
log/, sessions/, db/, cron/, tasks/ |
provozní stav, ne obsah |
memory/history.jsonl, binárky |
vyřazuje už include: ["*.md"] — do exclude psát netřeba |
Záchranná síť proti jediné slabině whitelistu: sync na konci zaloguje top-level adresáře,
které obsahují *.md a nepokrývá je žádný source. Tím se „tiše chybí" změní z neviditelné
poruchy na řádek v log/notes_sync.log.
Konfigurace
config/notes-search.yaml. Klíč sekce = source id (stabilní; visí na něm katalog i vektory;
URL/path se můžou změnit, klíč ne; rename = explicitní invalidace indexu daného zdroje).
Source id nesmí kolidovat mezi sekcemi — katalog je sdílený přes source_id.
Formát je YAML (ne TOML — zamítnuto uživatelem, zapsáno v USER.md): config se edituje
ručně a komentáře v něm mají hodnotu, což JSON neumí.
embedding:
endpoint: http://nvidia.hell:11434
model: qwen3-embedding:0.6b # tag psát VŽDY explicitně (latest = 8b, 4,7 GB)
dims: 1024
batch: 32
keep_alive: -1 # číslo, ne string ("-1" vrací HTTP 400)
query_prefix: "Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: "
sources:
git:
travel:
url: git@host:travel-notes.git
mirror: tmp/mirrors/travel.git
paths: ["**"]
include: ["*.md"]
exclude: []
devops:
url: https://host/devops-notes.git
mirror: tmp/mirrors/devops.git
paths: ["**"]
include: ["*.md"]
nanobot:
workspace:
paths: ["notes/**", "projects/**", "plans/**", "knowledge/**", "results/**", "cook/**"]
include: ["*.md"]
exclude: ["**/inbox/**"] # rozpracované zachyty před compile
develop:
paths: ["develop/**"]
include: ["*.md"]
exclude: ["develop/history.md"]
Rozhodnutí
D1 — Embeddings jako vrstva nad BM25, ne místo něj
Embeddingy přidávají parafrázi a cross-jazyk („prodloužení životnosti LEGO" najde „e-waste, Mindstorms po ukončení podpory" — žádná slova se nepřekrývají) a chunk-level relevanci. Nevyřeší exact match a čísla. Proto hybrid: FTS5 lexikálně, vektory sémanticky, RRF merge.
Potvrzeno měřením, ne argumentem — viz tabulka retrieval kvality výše: na těžkém dotazu BM25 1/5 vs. embeddingy 3/5, na lexikálním naopak BM25 4/5.
D2 — Model qwen3-embedding:0.6b
1024 dims, 32k ctx, 639 MB (q8_0), multilingual s češtinou jako first-class. Naměřeno: cold 1,81 s, warm 0,043 s, 108 chunk/s v batchi 32. Na parafrázových dotazech 5/5 — 0,6B stačí.
- Tag psát vždy explicitně:
latestje 8b (4,7 GB), ne šestistovka. keep_alive: -1(číslo).- Volba modelu NENÍ silně vážící rozhodnutí. Při 10³–10⁴ chunků je plný re-embed jednotky
minut, takže přechod na
4b-q8_0je odpoledne, ne rewrite. Signál pro upgrade: parafrázový dotaz, kde správný chunk existuje a keyword dotaz ho najde, ale sémantická polovina ho nevrátí ani v top-10. - Storage není omezení: 10⁴ × 1024 × 4 B ≈ 40 MB. Matryoshka truncation netřeba.
D3 — Vektory v sqlite-vec (vec0), ne BLOB + numpy
Rozhodující je rozpad nákladu, ne teorie: u BLOB+numpy je při 100k chunků 594 ms celkem, z toho
read 382 ms + pack 190 ms a samotný dot jen 22 ms — 96 % je Python režie na extrakci
a packování. vec0 ji odřízne skenem v C: 19 ms @ 10k, 212 ms @ 100k.
Pozor na zdůvodnění: vec0 není ANN index. Dokumentovaná cesta dotazu je průchod
(lineární škálování 19 → 212 ms to potvrzuje). Výhoda je konstanta, ne asymptotika.
Cena: sqlite-vec je pre-v1 a README píše „expect breaking changes"; užší dotazovací plocha
(max 16 metadata sloupců, 4 partition keys, IS NULL/LIKE na metadatech nefunguje, auxiliary
sloupce nesmí do KNN WHERE); ztráta volné numpy matematiky (MMR re-ranking, truncation za běhu).
Mitigace: verze připíchnutá (sqlite-vec==0.1.6) a zapsaná v meta.sqlite_vec_version;
index je derived, db/ gitignorovaná a rebuild jsou jednotky minut; chunks zůstává kanonická,
takže návrat k BLOBu je přidání jednoho sloupce. Blast radius breaking changu je „zůstaň na
staré verzi, nebo přizpůsob a přeindexuj", ne ztráta dat.
Ověřeno smoke testem (jinak by D3 padlo): DELETE/re-INSERT/UPDATE po rowid,
transakční ROLLBACK, distance_metric=cosine, vec0 v témže souboru jako běžné tabulky,
join vec_chunks.rowid = chunks.id.
D4 — Chunking strukturně vědomý
Viz sekce Chunking. Fixed-size sliding window zamítnuto.
D5 — markdown-it-py, ne vlastní parser a ne frameworky
Dva různé případy:
- Parser patří do knihovny. Vlastní regex zná CommonMark edge cases 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 ale zůstává vlastní. - Glue kód neobírat frameworkem. Sync orchestrace, katalog, hybrid search — tady neexistuje „malá dobrá knihovna", nabídka je binární: langchain/llama-index (stovky MB, abstrakce nad sqlite3/subprocess/HTTP, měnící se API) nebo vlastních ~500 řádků. Tady „psát si sám" není NIH, je to jediná racionální volba, protože alternativa obaluje čtyři stdlib volání.
Dependency set: markdown-it-py, requests, numpy, pyyaml, sqlite-vec==0.1.6.
D6 — FTS5 tokenizer unicode61 remove_diacritics 2 + prefix wildcardy
Zavřeno měřením, ne odloženo na testování. remove_diacritics 1 a výš foldí diakritiku
(dotaz zaloha najde záloha; 0 ne). unicode61 nestemuje, ale prefix wildcard to pokryje:
záloh* najde záloha, zálohování, zálohy. Trigram tokenizer zamítnut — netřeba.
Dotazová vrstva lepí * na termy delší než 2 znaky.
D7 — Skill je samostatný, bez závislostí na jiné skilly
Vzory z remind/note se kopírují, neimportují. Vlastní db.py/store.py, vlastní
lockfile, žádný detach, nepřebírat wiki_search.py ani note_capture._ascii_fold()
(FTS5 remove_diacritics folding stejně řeší). Cena je duplikace kódu; hodnota je, že skill
nespadne s ničím jiným a dá se přenést.
D8 — Mirror jako zdroj, sync offline
Git repa jsou kanonická data, index je derived. Mirror přes git clone --mirror / fetch.
fetch-on-query zamítnut: přidával git fetch i embed nových chunků do latence dotazu,
a při nedostupné Ollamě dělal z čerstvého dokumentu FTS-only výsledek. Místo toho offline cron
- lock (viz Provozní model).
Staleness není problém: index je retrieval hint, ne source of truth — před odpovědí se soubor vždy přečte čerstvý z disku. Dotaz může vidět index o minutu starší, což je přijatelné.
D9 — chunks je jediná retrieval jednotka
chunks_fts i vec_chunks vracejí chunks.id, RRF slučuje je. Klíč (source_id, path, chunk_idx). Rank-merge dvou různých jednotek nemá definovaný význam → file-level BM25 vyloučen.
D10 — Identita embedding prostoru v meta
embedding_model, embedding_dims, normalized, query_prefix, chunker_version.
Mismatch proti configu odmítne dotaz s „reindex needed". Bez toho by se smíchané vektory
ze dvou modelů projevily jako tiché zhoršení výsledků, ne jako chyba — nejdražší druh bugu.
D11 — Indexace výhradně offline
Cron + lockfile, nikdy v tahu agenta. Důvod: exec timeout 60 s vs. plný index ~77 s.
D12 — Rozsah indexace whitelistem
Viz sekce Rozsah indexace. paths je hradlo, exclude skalpel; workspace opt-in, git repa opt-out.
Zamítnuté varianty
| Varianta | Proč ne |
|---|---|
| Ollama Cloud embeddingy | Cloud tier embedding endpoint neservíruje — 18 cloud modelů, žádný s capability embedding; /api/embed vrací unauthorized, zatímco completion na tomtéž modelu projde. Katalog na ollama.com to potvrzuje: všech 12 embedding modelů je jen ke stažení. Navíc by to porušilo lokalitu dat (celý korpus osobních poznámek do cizí služby) a plný re-index = ~10⁴ requestů na externí API |
| Generativní cloud model + pooling hidden states | Generativní modely nejsou kontrastivně trénované na retrieval; proto je capability oddělená |
nomic-embed-text (137M) |
EN-centric, pro češtinu nevýhodný |
bge-m3 (567M) |
Umí dense+sparse hybrid z jednoho modelu, ale přes Ollama embeddings API jde dostat jen dense — výhoda mizí, FTS5 dělá tutéž roli levněji |
qwen3-embedding 4b/8b jako start |
Lepší multilingual skóre, ale 2,5–15 GB na kartě sdílené s chat modely. Upgrade je odpoledne (re-embed = minuty), tak začít malým |
*-q4_K_M kvantizace |
4bit u embeddingu; šestistovka q4 v nabídce ani není |
| BLOB + numpy cosine | 96 % nákladu je Python režie; viz D3 |
.npy + mmap_mode='r' (35 ms @ 100k) |
Nejrychlejší, ale druhý soubor mimo DB, který se musí držet v sync s chunks — složitost bez přínosu, když vec0 dává 19 ms na reálné škále |
| Externí vektorová DB (Chroma/Qdrant/pgvector) | Řeší problém, který na této škále neexistuje (server, jiná backup story, >10⁶ vektorů) |
| Trigram tokenizer | Netřeba, remove_diacritics 2 + prefix wildcardy stačí (D6) |
| Entity/concept pages jako v llm-wiki | Uživatel poznámky strukturoval sám; LLM-kurátorovaná druhá vrstva by byla duplikace. Adresářová struktura repů JE primární tematický index — zpřístupňujeme ji (TOC), nereimplementujeme |
fetch-on-query / scan-on-query |
Viz D8 |
| Blacklist jako primární gate | Viz D12 |
| Embeddingy až když fuzzy dotazy selžou | Zamítnuto — jsou součástí od začátku |
Konvence implementace
Vzory z existujících skillů (kopírovat, neimportovat — D7):
| Věc | Vzor | Zdroj |
|---|---|---|
| Rozdělení kódu | db.py (SCHEMA, get_db(), _migrate(), init_db()) + store.py (connection()/transaction() + veškeré SQL) + CLI bez inline SQL |
skills/remind/scripts/ |
| Migrace | idempotentní CREATE ... IF NOT EXISTS v jednom SCHEMA přes executescript() + _migrate() s PRAGMA table_info a ALTER TABLE ADD COLUMN. Žádná schema_version tabulka, žádný framework |
remind/scripts/db.py |
| Connection | sqlite3.connect(path, isolation_level=None) + PRAGMA journal_mode=WAL, foreign_keys=ON, row_factory=sqlite3.Row |
remind/scripts/db.py |
| Cesta k DB | WORKSPACE = Path(__file__).resolve().parents[3], WORKSPACE/"db"/"notes-index.sqlite", env override pro testy |
note/scripts/note_capture.py:25 |
| Shebang | entry point #!/usr/bin/env -S uv run --script + PEP 723; importovaný modul #!/usr/bin/env python3 + PEP 723 |
remind/scripts/* |
| Cron řádek | * * * * * uv run .../scripts/notes_sync.py >> log/notes_sync_cron.log 2>&1 (PATH je v hlavičce crontabu) |
server crontab -l |
| Lockfile | notes/.compile.lock je precedens → db/.notes-sync.lock |
note/scripts/note_compile.py |
| SKILL.md | frontmatter jen name + description (folded >, EN, s Triggers on: a funkční negativní delimitací — nikdy jménem jiného skillu); tělo ~100 řádků; workspace-relativní uv run skills/<name>/scripts/x.py |
remind/SKILL.md |
| Testy | skills/<name>/tests/, uv run --with pytest pytest ..., izolace přes tmp_path + monkeypatch modulového DB_PATH |
remind/tests/ |
| Deploy | rsync -av --exclude '__pycache__' --exclude '.pytest_cache' skills/<name>/ nanobot@nanobot.hell:/home/nanobot/.nanobot/workspace/skills/<name>/ |
CLAUDE.md |
Query interface: skill s CLI skriptem notes_search.py (vzor remind_cli.py — argparse,
subcommandy, --help místo plné flag reference v SKILL.md).
Otevřené otázky
- Seznam rep — která repa, jejich URL, kam mirrorovat, velikost/historie (vliv na fetch čas).
Musí doplnit autor; bez toho nelze naplnit
config/notes-search.yaml. - Benchmark modelu na uživatelských datech — cross-jazyk test výše proběhl na workspace
obsahu (
cml/wiki,plans/). Po přidání reálných rep ho zopakovat na nich.
Verifikace
Hotovo (2026-09-09) — schéma i model ověřené smoke testem, viz tabulky faktů:
vec0 mutace a transakčnost, KNN latence na dvou škálách, FTS5 external-content triggery
s češtinou, RRF merge nad chunks.id, cold load / throughput modelu, retrieval kvalita.
Při implementaci:
- Regresní test na osiřelé vektory — smaž soubor →
vec_chunksnesmí obsahovat jeho rowidy. Tohle je jediná past, kterou schéma samo neochrání (kaskáda navec0nedosáhne). - Idempotence syncu — dvakrát za sebou nad stejnou revizí: druhý běh nesmí nic změnit
(počty v
chunks/vec_chunks/chunks_ftsshodné,indexed_revstejná). - Lock — spustit dva syncy současně; druhý musí skončit exit 0 bez zápisu.
- Degradovaný režim — s vypnutou/nedosažitelnou Ollamou: sync uloží chunky
s
embedded_at IS NULL, dotaz vrátí FTS-only výsledek a řekne to; po obnovení Ollamy další sync vektory dosadí. metaguard — podvrhni v configu jinýmodel/dims→ dotaz musí skončit „reindex needed", ne vrátit výsledky.- Coverage report — přidej md soubor do adresáře mimo
paths→ sync ho musí ohlásit v logu. - Latence dotazu end-to-end — cíl: pod 1 s při teplém modelu (embed dotazu 0,043 s
- KNN ~19 ms + BM25 + RRF).