Muninn vector search
Builds and queries HNSW approximate-nearest-neighbor vector indexes in SQLite using the hnsw_index virtual table. Covers CREATE VIRTUAL TABLE with dimensions/metric/m/ef_construction, INSERT of raw float32 blobs, MATCH queries with k and ef_search, and vector blob encoding in Python (struct.pack), Node.js (Float32Array), and C. Use when the user mentions "vector search", "HNSW", "nearest neighbor", "KNN", "similarity search in SQLite", "hnsw_index", "cosine similarity", "L2 distance", "inner product", "vector MATCH", "ef_search", "float32 blob", or asks to add semantic / similarity search to a SQLite database.From its SKILL.md
npx -y skills add neozenith/sqlite-muninn --skill muninn-vector-searchAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
5.8 KB, as published. Nobody here has run it
muninn-vector-search — HNSW vector search in SQLite
The hnsw_index virtual table implements Hierarchical Navigable Small World indexes (Malkov & Yashunin, 2018) with SIMD distance kernels. Read this skill when the user wants to store vectors and run KNN queries. If the user also wants to generate those vectors from text, combine with muninn-embed-text.
Create an index
.load ./muninn
CREATE VIRTUAL TABLE vec USING hnsw_index(
dimensions=384, -- required
metric='cosine', -- 'l2' | 'cosine' | 'inner_product'
m=16, -- optional, max neighbors per layer (default 16)
ef_construction=200 -- optional, insert beam width (default 200)
);
| Metric | Formula | Range | When to use |
|---|---|---|---|
l2 | Squared Euclidean, no sqrt | [0, ∞) | Raw vectors without normalization |
cosine | 1 - cos(a, b) | [0, 2] | Normalized or text embeddings — default for most NLP |
inner_product | -dot(a, b) | (-∞, ∞) | When you want larger magnitudes to win |
The index auto-resizes; there is no max_elements parameter. Higher m and ef_construction trade insert time and memory for recall.
Insert vectors (per runtime)
Vectors are raw little-endian IEEE 754 float32 blobs — no header, 4 × dimensions bytes. A dimension-mismatched blob raises an error on INSERT (no silent truncation).
SQLite CLI
-- Vectors are typically inserted from a host language; for hand-crafted examples:
INSERT INTO vec(rowid, vector) VALUES
(1, X'0000803F000000000000000000000000'); -- (1.0, 0.0, 0.0, 0.0) for dim=4
Python
import struct
dim = 384
vector = [0.1] * dim
blob = struct.pack(f'{dim}f', *vector)
db.execute("INSERT INTO vec(rowid, vector) VALUES (?, ?)", (1, blob))
Batch loading:
import numpy as np
# vectors is an (N, 384) np.float32 array
rows = [(i, v.tobytes()) for i, v in enumerate(vectors)]
db.executemany("INSERT INTO vec(rowid, vector) VALUES (?, ?)", rows)
Node.js
const dim = 384;
const vector = new Float32Array(dim).fill(0.1);
const blob = Buffer.from(vector.buffer);
db.prepare("INSERT INTO vec(rowid, vector) VALUES (?, ?)").run(1, blob);
C
float vec[384] = { /* ... */ };
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, "INSERT INTO vec(rowid, vector) VALUES (?1, ?2)", -1, &stmt, NULL);
sqlite3_bind_int64(stmt, 1, 1);
sqlite3_bind_blob(stmt, 2, vec, sizeof(vec), SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
WASM
const dim = 384;
const blob = new Uint8Array(new Float32Array(dim).fill(0.1).buffer);
db.exec({
sql: "INSERT INTO vec(rowid, vector) VALUES (?, ?)",
bind: [1, blob]
});
KNN search
SELECT rowid, distance FROM vec
WHERE vector MATCH ?query_blob
AND k = 10;
rowid distance
----- --------
42 0.0321
17 0.0877
...
Tuning recall vs speed
-- Default ef_search = k * 2
SELECT rowid, distance FROM vec
WHERE vector MATCH ?query
AND k = 10
AND ef_search = 100; -- higher = better recall, slower
Start at ef_search = k * 2, raise until recall plateaus on your golden set.
Delete and drop
DELETE FROM vec WHERE rowid = 42; -- soft delete + neighbor reconnect
DROP TABLE vec; -- removes shadow tables too
-- UPDATE is not supported. Delete and re-insert instead.
Shadow tables auto-managed: vec_config, vec_nodes, vec_edges.
End-to-end Python recipe
import sqlite3, struct
import sqlite_muninn
db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
sqlite_muninn.load(db)
db.enable_load_extension(False)
db.execute("CREATE VIRTUAL TABLE vec USING hnsw_index(dimensions=4, metric='l2')")
pack = lambda v: struct.pack('4f', *v)
db.executemany("INSERT INTO vec(rowid, vector) VALUES (?, ?)", [
(1, pack([1.0, 0.0, 0.0, 0.0])),
(2, pack([0.9, 0.1, 0.0, 0.0])),
(3, pack([0.0, 0.0, 1.0, 0.0])),
])
query = pack([1.0, 0.0, 0.0, 0.0])
rows = db.execute(
"SELECT rowid, distance FROM vec WHERE vector MATCH ? AND k = 2",
(query,)
).fetchall()
for rowid, dist in rows:
print(rowid, round(dist, 4))
1 0.0
2 0.02
Common pitfalls
- DO NOT pass vectors as JSON arrays — they must be raw float32 blobs.
- DO NOT mismatch dimensions — blob byte length must equal
dimensions * 4; otherwise INSERT raises. - DO NOT use
idorembeddingas column names — the fixed names arerowidandvector. - DO NOT use
max_elements— it does not exist; the index grows on demand. - DO normalize inputs before using
cosine— the metric does not L2-normalize for you. - DO prefer
l2when you don't know whether inputs are normalized — its monotonicity does not depend on vector norm.
See also
- muninn-embed-text — generate the vectors in SQL via GGUF models
- muninn-node2vec — write graph embeddings into an HNSW index
- api.md#hnsw_index — full signature reference