fenecdb and PostgreSQL with pgvector

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

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.8, 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.
  • You search by meaning and by words together: BM25 and rank fusion are built in (Hybrid search).
  • The client keeps a replica that reads offline and redraws live (Sync).
  • A file per tenant suits your service (Tenants and sharding).
  • 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
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)
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
Hybrid search fuse, reciprocal rank fusion (Hybrid search) No operator; fusion written in SQL (an example)
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
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)
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.8, first released in 2026 Descends from POSTGRES, 1986 (history); pgvector 0.8.7 (README)
Details

What fenecdb gives up for its size is listed in Limits: no SQL and no join (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

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. We measured these on one laptop; the method is below.

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

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):

YCSB, 1 000 000 recordsfenec-server, HTTPPostgreSQL
B, 95% reads, 16 clients19.6 k17.4 k
A, 50% updates, 16 clients14.3 k14.1 k
A, 50% updates, one client3.27 k2.60 k
B, 95% reads, one client5.28 k3.98 k
E, scans of 1 to 100 rows, one client1.82 k1.39 k
C, reads alone, one client, buffered3.19 k3.66 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 13% at one client: each server's own part of a read is under 0.02 ms of a 0.29 ms round trip, and the rest moves with Docker's VM, PostgreSQL's own B doing 2.65 k beside its C's 3.66 k. At 16 clients fenec-server leads C, 19.6 k against 17.7 k.
  • 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 details

The numbers are make scale-bench's and make load-bench's, as At scale and Loading rows report them.

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 each side was run on its own after four idle minutes, once.
  • Where each ran. 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 search in each:

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
Details

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