Quickstart
From a clone to a vector query. Pick the surface you actually want: a shell, a browser page, or a server.
Build
git clone https://github.com/fenecdb/fenec
cd fenec
make test # 261 Rust tests, 61 JS testsThat produces target/release/fenec (the shell) and
target/release/fenec-pg (the server). The Rust suite runs first on
purpose: cargo test builds fenec-pg, and the JS sync
tests run against that binary.
The wasm32 target comes from rustup. Homebrew's cargo
ships no wasm32 standard library. The Makefile prefers
~/.cargo/bin/cargo; building by hand needs
rustup target add wasm32-unknown-unknown first.
In the shell
./target/release/fenec data.fenec # interactive
./target/release/fenec data.fenec -c 'get docs limit 5'
echo 'get docs' | ./target/release/fenec data.fenecInside the shell: .help .tables .stats
.functions .save <path> .checkpoint
.quit.
A first collection, a bulk load, then the index — that order matters, and the reason is in How it works.
create collection notes (
body text,
topic int @hash,
posted timestamp,
embed vector<384>
)
put notes {body: "hnsw is a navigable small world graph", topic: 3, embed: $1}
put notes [ ... 100 000 more ... ]
create index on notes (embed) @hnsw(cosine)
get notes select body
where topic = 3
near embed $1
limit 10When a file with a vector index is closed from an interactive or piped
session, a checkpoint is written so the graph is not rebuilt on the next open.
A one-shot -c call does not write one.
In the browser
make serve # builds the wasm, serves http://localhost:8787
make serve PORT=3000web/index.html is a full console: a FenecQL editor, a sample
data generator, live statistics and IndexedDB persistence.
For your own page you need exactly two files, fenec.js and
fenec.wasm. No npm, no bundler, no build step.
import { Fenec, persist, restore } from './fenec.js';
const db = await Fenec.open('./fenec.wasm');
db.run('create collection docs (title text, embed vector<384> @hnsw(cosine))');
db.run('put docs {title: $1, embed: $2}', ['hello', embedding]);
const { rows } = db.run('get docs near embed $1 limit 5', [queryVector]);
// or with the builder, for dynamic filters and bound parameters
const near = await db.from('docs').near('embed', queryVector).limit(5).rows();
await persist(db); // write the snapshot to IndexedDB
await restore(db); // read it back next sessionIt does not open over file://.
WebAssembly.instantiateStreaming and module
imports both need an HTTP origin. Any static server works:
python3 -m http.server, npx serve, nginx.
The same module runs in Node, where fetch cannot resolve a
relative path — so hand it the bytes:
import { readFile } from 'node:fs/promises';
const db = await Fenec.open(await readFile('./fenec.wasm'));As a server
./target/release/fenec-pg --listen 127.0.0.1:5433 --file data.fenec
# the HTTP/JSON endpoint is a second listener in the same process
./target/release/fenec-pg --file data.fenec --http 127.0.0.1:8080
./target/release/fenec-pg --ping # 0 means up — a health checkThen connect with anything that speaks the PostgreSQL protocol. The language is still FenecQL:
psql -h 127.0.0.1 -p 5433 -U fenec
# fenec=# get articles select title near embed '[0.1, 0.2]' limit 5;
curl 'http://127.0.0.1:8080/articles?select=title,year&year=gte.2024&limit=10'Bring your own data
fenec import derives the schema, recognises vector columns and
follows the bulk-load-then-index order by itself.
fenec import data.sqlite --table docs --into articles \
--vector embed:384 --index "embed@hnsw(cosine)"
fenec import postgres://user@host:5432/database --table docs \
--into articles --index "embed@hnsw(cosine)"--dry-run prints the derived schema and any warnings without
writing anything. Full flag list in Import.
Generate TypeScript from the file
The schema is already in the database, so it is not written a second time in TypeScript — it is generated, and the two cannot drift.
./target/release/fenec types data.fenec > web/fenec-schema.d.tsimport { Fenec } from './fenec.js';
import type { FenecSchema } from './fenec-schema.js';
const db = await Fenec.open<FenecSchema>('./fenec.wasm');
const rows = await db.from('articles').select('title', 'year').rows();
// rows: { title: string; year: number | null }[]Make targets worth knowing
| Command | What it does |
|---|---|
make test | Rust suite, then the JS suite |
make wasm | Build for wasm32 and copy to web/fenec.wasm |
make serve | Build the wasm and serve the browser console |
make bench | Scale measurement |
make memory | Memory footprint, for calibrating --max-memory |
make sweep | The ef/recall trade-off |
make compare | Against SQLite and pgvector (run make pgvector-up first) |
make small | The 636 KB binary: --profile cli --no-default-features |