This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.