fenecdb
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.

Start in five minutes Read the docs

fenec.wasm waiting

waiting to run

open
documents
index build
ann top-10, p50
recall@10
waiting for this panel to come into view

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.

fenecdbhnsw · 1×
pgvectorhnsw · 5.1× slower
SQLitefull scan · 174× slower

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.

no page cache

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.

vectors in the core

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.

zero dependencies

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.

single writer

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.

limits error

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.

 fenecdbSQLite 3.46PostgreSQL 17 + pgvector
data write, no index900 k rows/s415 k/s64 k/s
index build10.2 s HNSW0.03 s, B-tree only14.2 s HNSW
disk size58.4 MB59.8 MB145.0 MB
scalar filter, indexed4.3 ms14.5 ms7.8 ms
vector top-10, exact4.0 ms31.3 ms14.8 ms
recall@10100%100%
reopen117 ms1.0 msserver stays open

Where it loses

SQLite reopens in 1 ms because it loads nothing and reads pages on demand. fenecdb pulls the vector arena into memory and validates the graph, which takes 117 ms. That is the same coin, other side: it is why the queries are 3–171× faster. The break-even is four vector queries.

How to read the rest

SQLite's 0.03 s is a single-column B-tree, not an ANN index. The cost does not vanish, it moves to query time. PostgreSQL's disk figure is MVCC row headers, WAL and the visibility map — the price paid for concurrent transactions, which the single-writer model does not need.

Full method, hardware and the ef/recall sweep

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.

page.html
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 IndexedDB

On a server

One process, one file, both listeners.

shell
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

reach for it

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.

reach for SQLite

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.

reach for PostgreSQL

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.

Quickstart The query language