aistwin_

SQLite is enough for vector search at my scale

· #retrieval #sqlite

After chunking, my notes turn into roughly 21,000 chunks. Every guide I read assumed I would now set up a vector database. I almost did, then did the arithmetic first.

The arithmetic

21,000 vectors × 384 dimensions × 4 bytes is about 32 MB. That fits in memory on anything. A brute-force search is one matrix-vector multiplication, which numpy does in a few milliseconds.

import numpy as np, sqlite3

db = sqlite3.connect("twin.db")
rows = db.execute("SELECT id, vec FROM chunks").fetchall()
ids = [r[0] for r in rows]
mat = np.frombuffer(b"".join(r[1] for r in rows), dtype=np.float32).reshape(len(rows), -1)

def search(query_vec, k=8):
    scores = mat @ query_vec          # vectors are normalised at insert time
    top = np.argpartition(-scores, k)[:k]
    return [ids[i] for i in top[np.argsort(-scores[top])]]

On my 2021 laptop that's 9 ms per query, including the sort. Embedding the query itself takes longer than searching.

What SQLite does for me

  • Stores chunks, vectors (as BLOBs), file paths and modification times in one file.
  • Full-text search with FTS5, which I combine with the vector score for exact terms like error codes and command names.
  • Incremental updates: a note whose mtime changed gets re-chunked and re-embedded, everything else stays.

When I'd change my mind

Somewhere around a few million vectors, or if I needed filtering that has to happen inside the index. Neither is going to happen with my notes. There's also the sqlite-vec extension, which I'll probably try once it's in the Debian packages.