Generalized + redeployable: config via env, local Ollama embeddings, single portable sqlite-vec db, hybrid dense+FTS5 recall, mandatory credential scrub.
3.7 KiB
SecondBrain — Architecture
SecondBrain is deliberately small: one Python file (brain.py), one SQLite database, one
external dependency you run yourself (Ollama). This document explains the design decisions.
Data model
A single SQLite file (brain.db) with:
| Object | Role |
|---|---|
chunks |
one row per stored text segment: sha (dedup), source, project, path, ref, ts, role, text, embedded, created |
vec_chunks |
sqlite-vec virtual table holding the 768-dim embedding per chunk (rowid = chunks.id) |
fts_chunks |
FTS5 external-content index over chunks.text — stores only the BM25 index, not a second copy of the text (disk-safe). Kept in sync by triggers |
facts |
optional distilled facts (for an external consolidation loop) |
Because vec_chunks and fts_chunks both key on chunks.id, the two retrieval channels can be
fused cheaply.
Ingestion pipeline
source text ─▶ scrub() ─▶ _segments() ─▶ _stage() ─▶ chunks (INSERT OR IGNORE by sha)
scrub()— mandatory. Applies a battery of regexes that redact secrets (PEM blocks, JWTs, vendor key shapes, labelledpassword/token/secret/api_key…, URL-embedded creds,Company2026!-style passwords). Optionally strips emails. Nothing is stored un-scrubbed._segments()— splits long text on paragraph boundaries into ≤MAX_CHARS(~400-token) chunks; drops sub-MIN_CHARSnoise._stage()— computes a contentshafor dedup and doesINSERT OR IGNORE, so re-ingesting the same source is a cheap no-op.
Ingestion is strictly read-only — SecondBrain never writes back to your sources.
Embedding
embed() selects WHERE embedded=0 and calls Ollama's batch /api/embed (falling back to
per-chunk /api/embeddings on older Ollama). Vectors are L2-normalized so that vec0's L2
distance ranking is equivalent to cosine similarity (cos = 1 − d²/2). A chunk that genuinely
can't be embedded is marked embedded=-1 and skipped on subsequent runs (reset to 0 to retry).
Embedding is idempotent and resumable — kill it any time; the next run continues the backlog.
Retrieval
brain_recall() is pure dense KNN. brain_recall_hybrid() is the recommended path:
- Embed the query (
search_query:prefix, per nomic's asymmetric convention). - Dense channel — top-
poolby vector distance. - Lexical channel — FTS5 BM25 over a sanitized MATCH string (alphanumeric terms, each quoted to neutralize FTS5 operators, OR-joined for recall).
- Reciprocal-rank-fusion — each candidate scores
Σ 1/(rrf_k + rank_in_channel)across the channels it appears in; top-kby fused score.
RRF needs no score calibration between the two very different channels, and the hybrid degrades to dense-only when the lexical index is empty — so it is a safe drop-in for the dense function.
Why hybrid matters: pure embeddings are great at meaning but weak at exact tokens. An invoice
number like INV/2026/00037 or an IBAN often ranks poorly by cosine yet is an exact lexical hit.
Fusion recovers those without hurting semantic queries.
Configuration & portability
Every path, endpoint and model is an environment variable with a local-first default
(~/.secondbrain/brain.db, http://localhost:11434, nomic-embed-text). Nothing is hard-coded
to a host. To move a brain between machines, copy brain.db. To offload compute, point
OLLAMA_URL at any reachable Ollama (e.g. a GPU box on your tailnet).
What SecondBrain is not
- Not a chat UI — it's a retrieval library + CLI you wire into your own tools/agents.
- Not a reasoning loop — that lives in sister components (see the wiki).
- Not multi-tenant — it's a personal brain; run one per person/box.