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
| fenecdb | PostgreSQL + 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 × 128 | fenec-server, HTTP | PostgreSQL + pgvector |
|---|---|---|
| load and index | 49.6 s | 108.5 s |
| memory the server holds | 738 MB | 1 131 MB |
| disk | 612 MB | 1 432 MB |
| ef 100: recall@10, p50 | 99.1%, 0.141 ms | 98.6%, 2.076 ms |
| a filter keeping 1%, ef 100 | 100%, 0.667 ms | 99.0%, 16.1 ms |
| 8 clients, ef 100 | 18 852 queries/s | 2 289 queries/s |
load 100 000 rows, HNSW kept (POST /docs against COPY) | 19 151 rows/s | 410 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 records | fenec-server, HTTP | PostgreSQL |
|---|---|---|
| B, 95% reads, 16 clients | 19.6 k | 17.4 k |
| A, 50% updates, 16 clients | 14.3 k | 14.1 k |
| A, 50% updates, one client | 3.27 k | 2.60 k |
| B, 95% reads, one client | 5.28 k | 3.98 k |
| E, scans of 1 to 100 rows, one client | 1.82 k | 1.39 k |
| C, reads alone, one client, buffered | 3.19 k | 3.66 k |
- Both in Docker, because the VM's fsync takes a tenth of a
millisecond where the Mac's
F_FULLFSYNCtakes 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-serveron the host, asked over kept-alive HTTP connections. PostgreSQL in Docker's virtual machine (4 GB, 8 CPUs,shared_buffers1 GB), asked through thepostgrescrate. 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 withhnsw.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,halfvecandsparseveckeep their dimensions. Anumericneeds--cast(Type mapping). - Following needs
wal_level = logicaland an integer key.fenec-server --followserves 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 10Details
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).