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=…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.