The vector database
that thinks in graphs.
SwarnDB is a Rust-native vector database where a vector and a graph node are the same object. Get a fast, accurate vector store by default, and a real typed graph the moment you need it, queried together in one engine.
One identity. One store.
Vector and graph node, the same object.
A production-grade vector database in Rust, where the graph is a first-class citizen, not a bolt-on. A fast vector store out of the box, and a real typed graph when your problem grows past nearest-neighbor.
One object, not two databases
The id of a vector is the id of its graph node. No foreign key, no mirror table, no sync job between a vector store and a graph store. One storage path and one crash-recovery path for both.
A real, typed graph, not inferred from similarity
Directed, typed edges that carry confidence and provenance, which you create explicitly with put_edge and bulk import, or have an LLM extract. This is structure you author, not similarity you hope is structure.
Scope by structure, then rank by meaning
One composable query seeds candidates by similarity, walks typed edges across the graph, and ranks the surviving frontier exactly by meaning. Similarity and structure in a single plan, inside one engine.
Vector-only by default
Every collection is a fast, accurate HNSW store with nothing turned on. Four distance metrics, per-query ef_search, batch search, and correct filter-then-search. The graph is opt-in per collection; if you never want it, you never see it.
Smaller in memory, safe on crash
SQ8 scalar quantization cuts index memory by roughly 75% with exact final ranking and fast restart. Write-ahead logging means a crash never costs committed data, and collections come back queryable in seconds.
The vector id is the node id.
In SwarnDB, the id of a vector is the id of its graph node. There is no foreign key, no mirror table, no eventual consistency between two stores. The thing you searched for and the thing you traverse from are literally the same row.
That single decision makes one query possible: scope by structure, then rank by meaning, in one plan.
# mode="hybrid" turns on the first-class typed graph. client.collections.create( "articles", dimension=384, distance_metric="cosine", mode="hybrid" ) # The id each insert returns is the node's id. Vector and node are one object. a = client.vectors.insert("articles", vector=[0.1, 0.2, 0.3, ...], metadata={"topic": "physics"}) b = client.vectors.insert("articles", vector=[0.3, 0.1, 0.4, ...], metadata={"topic": "math"}) # Typed edges carry provenance, not just a pointer. client.graph.put_edge("articles", source=a, target=b, edge_type="CITES", provenance={"doc_id": "paper-1"}) # One composable hybrid query: # seed by similarity -> walk the graph -> rank the frontier exactly by meaning. result = ( client.graph.query("articles") .vector_similar([0.1, 0.2, 0.3, ...], k=20) .traverse("CITES", direction="outgoing") .vector_rank([0.1, 0.2, 0.3, ...], k=10) .return_nodes() ) for node in result.nodes: print(node.id, node.label)
Edges you author, not edges you guess.
Directed, typed edges that carry confidence and provenance. You create them explicitly with put_edge or bulk import, or have an LLM extract them, then traverse them in one query. Opt-in per collection via mode="hybrid".
Scope by structure, then rank by meaning.
One composable plan seeds candidates by similarity, walks typed edges across the graph, and ranks the surviving frontier exactly by meaning. Similarity and structure in a single plan, inside one engine.
Quality-aware & temporal traversal
opt-in · off by defaultWeight hops by confidence, recency, or a numeric property, so stronger and fresher edges count for more. Restrict a hop to edges valid at a point in time, so a query can ask what the graph knew then, not just what it knows now. Both are opt-in and off by default: a plain traverse stays a plain traverse until you ask for more.
You always know where a fact came from.
Every edge carries its type, confidence, a manual-vs-extracted flag, and a provenance record. Full edge CRUD with verify, reject, and audit history, plus CSV and JSONL bulk import.
# Author and curate edges with full provenance. client.graph.put_edge("articles", source=a, target=b, edge_type="CITES", provenance={"doc_id": "paper-1", "chunk": 4}) # Review what an LLM proposed: verify or reject, with an audit trail. client.graph.verify_edge("articles", edge_id) client.graph.reject_edge("articles", edge_id) # Bulk import authored edges from CSV or JSONL. client.graph.bulk_import_edges("articles", data, format="csv")
What you can build on top.
A fast vector engine, an opt-in typed graph that shares one identity with your vectors, and server-side math, all backed by the same crash-safe store. Index for speed, traverse authored edges for structure, compute for shape.
Vector engine
index- HNSW indexConfigurable ef_construction, ef_search, M; per-query ef_search.
- SQ8 quantizationRoughly 75% less index memory, exact final ranking, fast restart.
- Filter-then-searchAdaptive pre-filter (B-tree, hash, bitmap), then exact top-k within it.
- Batch searchMany queries in one round trip with shared overhead.
- File-based bulk ingestbulk_insert_from_path, memory-mapped; working memory bounded by the index.
First-class typed graph
graph- Vector id = node idOne identity, one store, one crash-recovery path.
- Directed typed edgesEach edge carries confidence, a manual-vs-extracted flag, and provenance.
- You author the edgesCreate via put_edge, CSV/JSONL bulk import, or LLM extraction.
- Opt-in per collectionTurn it on with mode="hybrid"; vector-only collections are unchanged.
- Not inferred from similarityOnly the node identity is shared with the vector, never the edges.
Math & internals
math- 15+ math operationsGhost vectors, SLERP, k-means, PCA, MMR, drift, and more, server-side.
- jemalloc memory releaseFreed memory is returned to the OS, not held by the process.
- WAL crash recoveryA crash never costs committed data; collections come back queryable in seconds.
- SIMD distance kernelsAVX2, SSE4.1, NEON, scalar fallback, chosen at runtime.
- Dual APISame engine over gRPC (50051) and REST (8080).
15+ vector operations, first-class.
Server-side, no round-trip. Every operation runs inside the engine and is exposed through gRPC MathService and REST POST /api/v1/collections/{id}/math/*. On a hybrid collection, several of these (analogy, diversity via MMR, cone, isolation, centroid, and interpolation) run exactly over the candidate set a graph query produced.
on hybrid collections: run over a graph-built frontier, not the whole collection
Turn text into typed edges, on your terms.
Point a hybrid collection at any OpenAI-compatible model with your own key. It proposes typed entities and edges with full provenance. Preview the cost before you spend, then verify, reject, or re-extract.
# Point a hybrid collection at any OpenAI-compatible model. You own the key. client.extraction.set_llm_config( "articles", base_url="https://openrouter.ai/api/v1", api_key="sk-or-...", model_name="openai/gpt-4o-mini", temperature=0.0, max_tokens=2048, ) client.extraction.set_ontology("articles", base_template="research-papers", replace=False) # Preview the cost before you spend a single token. estimate = client.extraction.cost_preview("articles", chunks) print(f"Estimated cost: ${estimate.estimated_cost_usd}") # Then run it. Proposed entities and edges come back for verify / reject / re-extract. result = client.extraction.start_extraction("articles", chunks)
Vector-only by default, graph when you need it.
# pip install swarndb from swarndb import SwarnDBClient with SwarnDBClient(host="localhost", port=50051) as client: # Vector-only by default. client.collections.create("articles", dimension=384, distance_metric="cosine") client.vectors.insert("articles", vector=[0.1, 0.2, 0.3, ...], metadata={"topic": "physics"}) client.vectors.insert("articles", vector=[0.3, 0.1, 0.4, ...], metadata={"topic": "math"}) results = client.search.query("articles", vector=[0.1, 0.2, 0.3, ...], k=10) for r in results.results: print(r.id, r.score) # distance score, lower is more similar
Sub-7ms p95 at 99% recall.
Vector-only search on DBpedia 1M (1536-dim float32, cosine) on a 32-core, 64 GB host with 8 concurrent searcher threads, 1,000 queries per setting averaged across 3 iterations. Default HNSW (M=16, ef_construction=200). Measured 2026-05-20; not re-run for v1.1.0.
| ef_search | QPS | Recall@10 | p50 (ms) | p95 (ms) | p99 (ms) |
|---|---|---|---|---|---|
| 25 | 2,398 | 98.16% | 3.16 | 4.91 | 6.06 |
| 50 | 2,214 | 98.94% | 3.33 | 5.26 | 6.77 |
| 100 | 1,801 | 99.21% | 4.16 | 6.85 | 8.02 |
| 200 | 1,233 | 99.35% | 6.18 | 10.19 | 12.26 |
| 400 | 760 | 99.60% | 10.00 | 16.83 | 20.48 |
| 800 | 437 | 99.74% | 17.42 | 30.43 | 35.90 |
Questions, answered honestly.
SwarnDB is one Rust engine that is a fast vector store by default and a real typed graph when structure matters, so here is a straight account of what it does well and where it does not.
SwarnDB is a production-grade vector database written in Rust where a vector and a graph node are the same object. The id of a vector is the id of its graph node, so there is only one thing to store and reason about. By default it is a fast, accurate vector store, and when you need it, it becomes a real typed graph. It is one engine, not two systems bolted together.
SIMD, end to end.
Runtime dispatch picks the widest instruction set your CPU supports. Cosine distance is fused: dot product and norms in a single pass. PQ distance tables use SIMD gather.