fenecdb

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 tests

That 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.fenec

Inside 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 10

When 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=3000

web/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.

index.html
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 session

It 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 check

Then 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.ts
import { 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

CommandWhat it does
make testRust suite, then the JS suite
make wasmBuild for wasm32 and copy to web/fenec.wasm
make serveBuild the wasm and serve the browser console
make benchScale measurement
make memoryMemory footprint, for calibrating --max-memory
make sweepThe ef/recall trade-off
make compareAgainst SQLite and pgvector (run make pgvector-up first)
make smallThe 636 KB binary: --profile cli --no-default-features