get articles where year >= 2024 and tags has "rust" near embed $1 limit 10
A vector database that fits in a browser tab.
302 KB of WebAssembly with no dependencies. Vectors
are a type, near is a clause, and the HNSW index is built in
the same tab that asks the question. The same engine runs as a process
that speaks the PostgreSQL protocol.
waiting to run
- open
- —
- documents
- —
- index build
- —
- ann top-10, p50
- —
- recall@10
- —
Crossing the same ground
One vector search, top ten of a hundred thousand, on the same data in the same process with the same distance kernel. Watch them run.
The scale is compressed — run at the true ratio, the
last lane would take nearly three minutes to cross. The times beside each
lane are the measured ones. SQLite has no ANN index in core, so its lane
is a full scan; pgvector runs the same m and
ef_construction as fenecdb, and its figure carries a 0.40 ms
TCP round trip that no embedded engine pays.
Five decisions, and what each one costs
fenecdb is small because it gave things up. Here is what it dropped, what that bought, and where the bill arrives.
The bytes on disk and the bytes in memory are the same format, so a read decodes directly over the arena. No eviction policy, no dirty pages, no warm-up.
The bill: the whole file is resident. Your data has to fit in memory.
vector<768> is a type, @hnsw is an
index, near is a clause. The planner knows about all
three, so a filter and a vector search are planned together rather
than stacked.
The bill: no JOINs, no subqueries, no transactions. One collection at a time.
Own codec, own JSON, own HNSW, own lexer and parser, own SCRAM. The
core is std and nothing else, which is why the
WebAssembly output is 302 KB and auditable in an afternoon.
The bill: every bug is ours.
Reads take a shared lock, writes the exclusive one. That is what removes MVCC row headers, the WAL and the visibility map — and why the file is 2.5× smaller than the same data under pgvector.
The bill: one process per file. Two writers corrupt it.
A near result caps at 10 000 rows and expression depth
at 512 levels. Both return a query error rather than a shortened
result, because a silently cut answer is a wrong answer believed
right.
The bill: you will meet an error where another engine would have handed you something.
Everything else, measured
100 000 rows × 128 dimensions, clustered embeddings, Apple M-series. All three engines bulk load first and index after, which is the recommended order for each.
| fenecdb | SQLite 3.46 | PostgreSQL 17 + pgvector | |
|---|---|---|---|
| data write, no index | 900 k rows/s | 415 k/s | 64 k/s |
| index build | 10.2 s HNSW | 0.03 s, B-tree only | 14.2 s HNSW |
| disk size | 58.4 MB | 59.8 MB | 145.0 MB |
| scalar filter, indexed | 4.3 ms | 14.5 ms | 7.8 ms |
| vector top-10, exact | 4.0 ms | 31.3 ms | 14.8 ms |
| recall@10 | 100% | — | 100% |
| reopen | 117 ms | 1.0 ms | server stays open |
The query does not change when the transport does
db.from('notes') builds the same FenecQL
wherever it is bound: to WebAssembly in the
page, to the HTTP endpoint, to a
PostgreSQL client, or to a
local replica syncing with a server.
In the browser
Two files, no npm, no build step, no bundler.
import { Fenec, persist } from './fenec.js';
const db = await Fenec.open('./fenec.wasm');
db.run(`create collection notes (
body text,
embed vector<384> @hnsw(cosine))`);
await db.from('notes').insert({ body, embed });
const near = await db.from('notes')
.near('embed', queryVector)
.limit(5)
.rows();
await persist(db); // into IndexedDBOn a server
One process, one file, both listeners.
fenec-pg --file data.fenec \
--listen 127.0.0.1:5433 \
--http 127.0.0.1:8080
psql -h 127.0.0.1 -p 5433 -c 'get notes limit 5'
curl 'http://127.0.0.1:8080/notes?year=gte.2024&limit=10'
curl -N 'http://127.0.0.1:8080/notes/changes'
# event: seed
# data: {"seq":12,"rows":[...]}Whether this is the right database for you
When vector search has to be first-class and the database should run on the user's machine: local semantic search, offline retrieval, agent memory in the browser, an embedded recommender that ships inside the app.
When you need relational data, JOINs, transactional safety and a mature toolchain. Also for very short-lived processes: a tool that runs four queries and exits never amortises fenecdb's open cost.
When several writers share the data over a network and need ACID. On
maturity, concurrency and ecosystem the comparison is not worth
making. That is exactly why fenec-pg exists — not to
replace PostgreSQL, but to let you reach fenecdb with the same
tools.