The Adapter Matrix
29 adapters, one contract. Every one of them passes the same 7-test conformance suite, so the builder behaves identically across all of them — the entries below differ only in what their backend physically can and can't do (an edition requirement, a dimension floor, a metric it lacks, how lazily it becomes consistent). Each adapter lives at @nhtio/adk/batteries/vector/<name> and is reached by deep import, which is what loads its optional-peer driver. Two — in_memory and orama — are also re-exported from the core barrel because they run in the browser.
How to read the caveats
A caveat is a fact about the backend, surfaced once so it doesn't ambush you in production. None of them change the query you write — vs('docs').nearText(…).select('id').limit(10) is the same line against all 29. They change what the backend requires to run, or what it can't promise. Where a backend is eventually consistent, see Consistency & Capabilities; where it has a metric or dimension limit, it's noted inline.
In-process & browser
No server. Embedded libraries, in-memory indexes, single-file stores — the cheapest path to a working store, and the only adapters that run in a browser tab.
| Adapter | Driver | Notes |
|---|---|---|
in_memory | none | The reference adapter. Hand-rolled cosine, cross-env, zero deps. The semantics every other adapter is tested against. Use for tests, prototypes, and the browser. |
orama | @orama/orama | Cross-env, browser-capable. Real vector + hybrid store; backs in-browser retrieval (see Ask ADK). Re-exported from the core barrel. |
hnswlib | hnswlib-node | Embedded ANN (HNSW), in-process, native addon. Fast approximate search with no server. |
lancedb | @lancedb/lancedb + apache-arrow | Embedded columnar vector store; persists to disk, no server. |
sqlite_vec | better-sqlite3 + sqlite-vec | Single-file or :memory:. ACID — transactions: true. The cheapest real end-to-end backend. |
duckdb | @duckdb/node-api | In-process via the vss extension; :memory: or a file. |
SQL-extension
A real database you may already run, with a vector type or extension bolted on. Metadata is real columns; some offer true transactions.
| Adapter | Driver | Notes |
|---|---|---|
pgvector | pg | Postgres + the vector extension. ACID — transactions: true. Parameterized SQL filters, real columns, the works. |
mariadb | mariadb | Native VECTOR(N) columns (MariaDB 11.7+); vectors via VEC_FromText/VEC_ToText. |
oracle23ai | oracledb (thin mode, no Instant Client) | Native VECTOR(dims, FLOAT32); KNN via VECTOR_DISTANCE … FETCH APPROX FIRST k. Caveat: VECTOR columns are rejected in the SYSTEM tablespace — the connecting user must default to a normal tablespace (e.g. USERS) with CREATE TABLE. |
clickhouse | @clickhouse/client | Vector search over a MergeTree table; upsert is delete-then-insert (ClickHouse allows duplicate keys). |
Self-hosted server
A dedicated vector engine (or a search/graph/document engine with vector search) you run yourself.
| Adapter | Driver | Notes |
|---|---|---|
qdrant | @qdrant/js-client-rest | Named vectors, native filter translation. |
weaviate | weaviate-client | Named vectors; can embed server-side (builtInEncoding). |
milvus | @zilliz/milvus2-sdk-node | Boolean-expression filters. |
chroma | chromadb | Native where filtering. |
redis | redis | RediSearch vector index. Covers Valkey — point it at a Valkey instance and it works unchanged. |
elasticsearch | @elastic/elasticsearch | ES 8 dialect — dense_vector + a top-level knn clause. Use the v8 client against an 8.x server (a v9 client sends a compat header 8.x rejects). Distinct adapter from opensearch. |
opensearch | @opensearch-project/opensearch | OpenSearch's knn_vector + query.knn dialect. A separate adapter because an ES client can't drive it and vice-versa. |
solr | none (HTTP/JSON) | Dense-vector / kNN query parser (Solr 9+). The collection is a Solr core that must be precreated. |
typesense | typesense | Native vector search. |
meilisearch | meilisearch | Needs the vector-store experimental feature + a userProvided embedder. |
mongodb | mongodb | Atlas Vector Search ($vectorSearch). The vector index is async; the document store is strongly consistent. |
surrealdb | surrealdb | Multi-model; vector::similarity::cosine / vector::distance::euclidean. |
neo4j | neo4j-driver | Native vector index (5.13+); db.index.vector.queryNodes. |
arangodb | arangojs | Brute-force AQL COSINE_SIMILARITY / L2_DISTANCE. |
couchbase | couchbase | Enterprise Edition only for vector search — Community rejects vector fields. Scope.collection = logical collection; scoped FTS vector index for KNN. |
vespa | none (HTTP/JSON) | A collection is a document type in a deployed application package — the adapter rebuilds + redeploys the package (dependency-free store-zip) to the config server on create/drop. |
Couchbase is Enterprise-only — and that's fine
Vector search in Couchbase requires the Enterprise edition; Community throws on vector fields. The adapter ships anyway. The kit provides batteries and does not enforce a backend's licensing — whether Couchbase EE fits your deployment is your call, not something the battery gates on.
Managed / serverless
Someone else runs the infrastructure. No local container; you supply credentials. These are eventually consistent (see Consistency & Capabilities) and are omitted from the CI matrix — their suites run only when their credentials are set.
| Adapter | Driver | Notes |
|---|---|---|
pinecone | @pinecone-database/pinecone | Needs an existing index + API key. Eventually consistent (configurable consistency); a logical collection maps to a namespace. Can embed server-side. |
s3vectors | @aws-sdk/client-s3vectors | AWS S3 Vectors. Bucket provisioned out-of-band; collection = an index in it. cosine/euclidean only (no dot), topK capped at 100, index names 3–63 chars. |
cloudflare | none (Vectorize V2 REST, pure fetch) | Cloudflare Vectorize. Dimensions 32–1536, topK capped at 50 when returning values/metadata, aggressively eventually consistent. No driver dependency. |
Running an adapter's integration suite
Every adapter's functional suite is gated on an environment variable — set it to run that suite against a live backend, leave it blank and the suite skips green, not red. The zero-infra ones run out of the box; the rest you point at a container or a service.
# zero infra — run as-is
TEST_VECTOR_ORAMA_URL=1 # orama, in-process
TEST_VECTOR_SQLITE_VEC_URL=:memory: # sqlite-vec, no server
TEST_VECTOR_DUCKDB_URL=:memory: # duckdb, no server
TEST_VECTOR_HNSWLIB_URL=1 # hnswlib, embedded
TEST_VECTOR_LANCEDB_URL=1 # lancedb, embedded (temp dir per test)
# a local container (see docker-compose.vector.yml profiles)
TEST_VECTOR_PGVECTOR_URL=postgres://vector:vector@localhost:5432/vector
TEST_VECTOR_QDRANT_URL=http://localhost:6333
TEST_VECTOR_ELASTICSEARCH_URL=http://localhost:9200
# … one TEST_VECTOR_<NAME>_URL per backend
# a managed service — credentials, no container
TEST_VECTOR_PINECONE_API_KEY=… TEST_VECTOR_PINECONE_INDEX=…
TEST_VECTOR_S3VECTORS_BUCKET=… TEST_VECTOR_S3VECTORS_REGION=…
TEST_VECTOR_CLOUDFLARE_ACCOUNT_ID=… TEST_VECTOR_CLOUDFLARE_API_KEY=… TEST_VECTOR_CLOUDFLARE_ENABLED=1The Cloudflare Vectorize suite needs the extra
TEST_VECTOR_CLOUDFLARE_ENABLED=1opt-in on top of its credentials. Vectorize's public endpoint is aggressively eventually-consistent — its query index flaps for seconds after a write or delete — so even with the conformance harness's retries the read-after-write race loses often enough to red-flag an otherwise-green run. It is therefore excluded from CI and skips unless you deliberately switch it on to exercise the adapter against live Vectorize.
The full list of variables (with the exact connection-string shapes and per-backend extras like _USER/_PASS/_TOKEN) lives in .env.test.example, and the container definitions are in docker-compose.vector.yml — one Compose profile per backend. The shipped adapters are each verified at 7/7 conformance, run deterministically, against a live instance.
What didn't ship
Three backends were attempted and backed out. They are not adapters you can import; they're recorded here so the gaps are intentional and documented, not mysterious:
| Backend | Why it's not here |
|---|---|
| Kùzu | The kuzu npm package is flagged "no longer supported"; the only alternative is stale and WASM-only. No viable non-deprecated Node distribution. |
| Memgraph | Backend defect: vector_search.search throws after a DETACH DELETE of an indexed node (a dangling index tombstone that drop+recreate doesn't prune), so the conformance suite's delete-then-read can't pass. Revisit when the index-deletion pruning is fixed. |
| Vearch | The published arm64 image is non-functional — its JSON serialization falls back to a broken path and every API call returns a 500. An image/architecture defect, not an adapter problem. |
Where to go next
- Consistency & Capabilities — the eventual-consistency story for the managed backends, and the
capabilitiesflags per adapter. - Writing an Adapter — add the 30th backend yourself.
- The Query Builder & Filters — the query that runs identically across all of these.