fenecdb and PostgreSQL

PostgreSQL is a relational database server, and pgvector adds vector types and indexes to it. fenecdb is a document database that runs in a page, in an app, or as a server over HTTP, with full-text and vector search built in.

The facts about PostgreSQL and pgvector on this page were checked on 2026-10-02 against the pages each one links to. fenecdb is at version 0.1.12, and is alpha software.

Which to choose

Choose PostgreSQL with pgvector when

  • You want SQL: joins, subqueries, window functions, and every ORM, driver and tool written for it.
  • Several clients write at once, in transactions they hold open across statements (MVCC).
  • You need what a mature server has: row-level security, logical replication, extensions such as PostGIS, and managed services from many providers.
  • Your vectors already live beside relational data you query together.

Choose fenecdb when

  • The same database runs in the browser, in a mobile app and on the server, with one query language.
  • The client keeps a replica that reads offline and redraws live (Sync).
  • Reads and writes over HTTP with nothing in front: on YCSB fenec-server leads PostgreSQL in every workload at 4 and 16 clients, both in Docker (measured below).
  • A file per tenant suits your service (Tenants and sharding).
  • You search by words, with typos, highlights and facets, and by meaning too: BM25 and rank fusion are built in (Hybrid search).
  • Search latency at a million vectors matters (measured below).

Feature by feature

 fenecdbPostgreSQL + pgvector
Where it runs Browser (WebAssembly), Rust, iOS, Android, Flutter, a server over HTTP (Where it runs) A server; in the browser and Node through PGlite (fenecdb and PGlite)
Data model Typed documents in collections, json fields (Objects and paths) Relational tables, plus jsonb
Query language FenecQL; builders in JS, Python, Go, .NET, Swift, Kotlin, Dart (FenecQL) SQL
Clients JS/TS, Python, Go, .NET, Swift, Kotlin, Dart; any language over HTTP (Your language) Drivers in every major language; pgvector lists libraries for 40 or more (pgvector)
Transactions One writer a file; a batch lands whole or not at all; no transaction held open between requests (Single writer) ACID, MVCC, many writers, interactive transactions (MVCC)
Replication Read replicas over HTTP, archives restored to a moment (Replication) Streaming and logical replication
Sync, live queries, offline A replica in the page or app: offline reads, queued writes, live queries (Sync) Not in core; LISTEN/NOTIFY and logical decoding
Access control A server token; JWT rules added to every statement (Scoped access). No TLS: a terminator goes in front Roles, GRANT, row-level security
Schema and migrations Typed schema, alter collection, a schema declared in code and checked at every open (Schema in code) Transactional DDL (ALTER TABLE); migrations by outside tools
Full text BM25 (The text clause) tsvector ranked by ts_rank; BM25 through an extension such as pg_textsearch
Highlights and facets highlight(), snippet() and facet (Highlights, Facets) ts_headline over a tsquery; a facet is a GROUP BY of its own
Vector index HNSW (The vector clause) HNSW and IVFFlat (pgvector)
Quantization f16 storage; int8 and bit codes, re-ordered by the full vectors (Quantized indexes) halfvec, bit, binary quantization by expression index (pgvector)
Filtered vector search Planned with the filter, an exact fallback when the filter leaves too few (A filter and a vector together) Iterative index scans since 0.8.0 (release)
Sparse vectors sparse<N>, pgvector's text form (Sparse vectors) sparsevec (pgvector)
Hybrid search fuse, reciprocal rank fusion (Hybrid search) No operator; fusion written in SQL (an example)
Size A 2.99 MB server image (Binary size) Not measured here
Licence and hosting Apache-2.0; self-hosted only PostgreSQL Licence (pgvector's); managed by many providers
Maturity Version 0.1.12, first released in 2026 Descends from POSTGRES, 1986 (history); pgvector 0.8.7 (README)
What PostgreSQL does that fenecdb does not

What fenecdb gives up for its size is listed in Limits: no SQL dialect (its everyday spellings aside) and a join only on a key (a query reads one collection and attaches related rows with lookup), one writer a file, no transaction held open across requests, and no TLS on the server. PostgreSQL's server has none of those limits.

pgvector's HNSW indexes take vector up to 2 000 dimensions, halfvec up to 4 000, bit up to 64 000 and sparsevec up to 1 000 non-zero elements (README). pgvectorscale adds a disk-based index, StreamingDiskANN, for data larger than memory (pgvectorscale). fenecdb keeps its indexes in memory and its documents in a mapped file (Memory and scale).

Measured

YCSB's core workloads over a million records, fenec-server over HTTP against PostgreSQL 17, both in Docker on the Mac, durable, operations a second, the median of three runs (every cell, and MongoDB). We measured these on one laptop; the method is below.

YCSB, 1 000 000 recordsfenec-server, HTTPPostgreSQL
B, 95% reads, 16 clients18.2 k13.0 k
A, 50% updates, 16 clients13.3 k10.5 k
A, 50% updates, one client3.34 k2.52 k
B, 95% reads, one client4.50 k3.09 k
E, scans of 1 to 100 rows, one client1.69 k1.28 k
C, reads alone, one client, buffered3.07 k3.14 k
  • Both in Docker, because the VM's fsync takes a tenth of a millisecond where the Mac's F_FULLFSYNC takes four, and its network a quarter of a millisecond a request: fenec-server on the Mac against PostgreSQL in Docker would compare those, not the two.
  • C, reads alone, is where PostgreSQL leads, by 2% at one client, and F by 1%: each server's own part of a read is under 0.02 ms of a 0.29 ms round trip, and the rest is Docker's network, which moves more than that from one day to the next (PostgreSQL's C at one client did 3.66 k on 2026-10-05, its median now 3.14 k). At 4 and 16 clients fenec-server leads every workload; at 16 fenec-server leads C, 18.4 k against 14.8 k.

Then search by meaning: a million 128-dim vectors, fenec-server asked over HTTP and PostgreSQL with pgvector over its own protocol, by the same client program with the same index settings.

1 000 000 × 128fenec-server, HTTPPostgreSQL + pgvector
load and index49.6 s108.5 s
memory the server holds738 MB1 131 MB
disk612 MB1 432 MB
ef 100: recall@10, p5099.1%, 0.141 ms98.6%, 2.076 ms
a filter keeping 1%, ef 100100%, 0.667 ms99.0%, 16.1 ms
8 clients, ef 10018 852 queries/s2 289 queries/s
load 100 000 rows, HNSW kept (POST /docs against COPY)19 151 rows/s410 rows/s
  • One machine, one run of each side. An Apple M1 MacBook Air; PostgreSQL ran in Docker.
  • Every figure, and the 768-dim run, is in Benchmarks.
Method, and what was not measured

YCSB is make ycsb's, every cell of it in Everyday reads and writes with its method: each cell 30 s after a 5 s warm-up, the systems in turns, PostgreSQL with shared_buffers 1 GB and prepared statements. The vectors are make scale-bench's and make load-bench's, as At scale and Loading rows report them.

make ycsb
make pgvector-up      # PostgreSQL 17 with pgvector, the pgvector/pgvector:pg17 image
make scale-bench
make load-bench
  • Hardware. An Apple M1 MacBook Air. It has no fan and slows under minutes of load, so for the vectors each side was run on its own after four idle minutes, once.
  • Where each ran, for the vectors. fenec-server on the host, asked over kept-alive HTTP connections. PostgreSQL in Docker's virtual machine (4 GB, 8 CPUs, shared_buffers 1 GB), asked through the postgres crate. An empty query's round trip was 0.13 to 0.22 ms to PostgreSQL and 0.06 to 0.11 ms to fenec-server: part of each answer, not the rest of it.
  • Same index settings. HNSW with m 16 and ef_construction 64, pgvector's defaults, on both.
  • Each side's best way to load. fenec-server builds its graph as JSON arrays of rows arrive. pgvector builds after a binary COPY, on every core, then reads its index into its buffers (pg_prewarm) and searches a filter with hnsw.iterative_scan = relaxed_order.
  • Data. Vectors spread along 32 directions around 64 centres, 1 000 held-out queries, recall against the exact ten found by brute force.

Not measured yet: a Linux machine, PostgreSQL outside Docker, data larger than memory and pgvectorscale. Several clients writing at once are YCSB's.

Moving from PostgreSQL

fenec import copies a table into a collection over the network, vectors and index included. With --follow it keeps applying the table's changes as they commit.

fenec import postgres://app@db:5432/app --table docs --into articles \
    --index "embed@hnsw(cosine)" --follow
  • pgvector's types come across: vector, halfvec and sparsevec keep their dimensions. A numeric needs --cast (Type mapping).
  • Following needs wal_level = logical and an integer key. fenec-server --follow serves the mirror as it follows (Serving the mirror).
  • Queries are rewritten. fenecdb does not speak SQL or PostgreSQL's protocol, so a pgvector client library cannot connect to it.

The same query in each, and then a nearest-neighbour search:

SELECT title FROM articles
WHERE year >= 2024
ORDER BY published DESC
LIMIT 10;
get articles select title
  where year >= 2024
  order published desc
  limit 10
SELECT title FROM articles
WHERE year >= 2024
ORDER BY embed <=> $1
LIMIT 10;
get articles select title
  where year >= 2024
  near embed $1
  limit 10
How a join, a transaction and row-level security carry over

A join becomes lookup, which attaches each row's related rows, or in (get …), which filters by another collection's rows (The lookup clause, Subqueries). A transaction of several statements becomes a /batch, which lands whole or not at all but cannot read between its writes (Batches). Row-level security becomes a JWT rule (Scoped access).

The import reads the table with COPY ... TO STDOUT and needs nothing installed on the server, and the follower reads a logical replication slot through PostgreSQL's own pgoutput. Drop the slot when you stop following, or PostgreSQL keeps its log for it (Following changes).