Spec 0013 — Graph-Based ANN (Vamana / DiskANN family)
Status: Normative (v0.1). The reference implementation includes batch Vamana,
compressed ADC with exact-source reranking (§4.4.1), exact-bound federation
routing (§6), and explicit raw Fresh Vamana append/consolidation (0016 §4).
0009 §8.5.3 and dreamdb-conformance/vectors/0013/ pin the static graph
format and scalar-canonical 10,000-node build/search artifacts. Those artifacts
do not imply independent-implementation agreement for every later capability;
portable incremental-build evidence remains OQ-70. No background scheduler or
compressed Fresh writer is claimed.
Depends on: spec/0001, spec/0002, spec/0004, spec/0007, spec/0010, spec/0012.
Motivation: Graph traversal offers a different candidate-selection strategy
from partition-based ANN. This document defines the static graph format and
Vamana behavior, with the cross-shard routing contract resolving OQ-51. It does
not establish a universal recall/cost advantage over IVF/PQ at a particular
corpus size; comparisons require a pinned 0023 workload and report.
1. Purpose
DreamDB's spec/0004 partition algorithms answer "which cells does this query touch?" Graph algorithms answer a different question: "starting from a known entry point, which sequence of greedy local steps converges to the nearest neighbors?" The two are independently optimal — partition for selectivity-first, graph for recall-floor-first.
By the end of this document the following are concrete:
- The graph-based SpatialIndex contract: an extension of spec/0004 §2 covering the entry-point + adjacency-list data model.
- The GraphIndex Object: immutable, content-addressed CBOR Object that distributes the graph's metadata (R, alpha, entry point, page layout, seed).
- The GraphPage Object kind: new on-disk format that holds a fixed-size batch of (node, vector, adjacency-list) tuples, page-aligned for I/O efficiency.
- The Vamana algorithm (
dreamdb.vamana-cosine): deterministic build + greedy search, suitable for 10B-scale. - The streaming-vs-batch build policy: how the graph is materialized at compaction time and (optionally) maintained incrementally.
- The cross-shard routing layer: an exact-bound router GraphIndex over shards (§6), resolving OQ-51 in
0012. - The compression interop: supported
0010compressors provide ADC candidates and direct exact-source reranking (§4.4.1); QINCo remains reserved.
What stays defined elsewhere:
- SpatialIndex Object format / registry — spec/0004 §3.
- Bucket-internal vector compression — spec/0010.
- Federation Manifest format — spec/0012 §3.
- HTTP backend contract — spec/0005.
What this document does NOT define:
- Learned graph algorithms (NGT/PANNG, learned-edge-prune networks). Defer to v0.X+1 after Vamana ships.
- Hybrid graph+partition (HNSW-style hierarchical graphs). Vamana is a single-layer R-regular graph; hierarchy is a future addition.
- Per-query graph mutation (online graph update as a side-effect of queries). DreamDB's content layer is strictly immutable; graph updates produce new GraphPage Objects (§5).
2. Graph-SpatialIndex contract (extends spec/0004 §2)
A graph-based SpatialIndex differs from a partition-based one in that it does NOT produce a deterministic spatial key. Instead it produces a search trajectory: a sequence of (node-id, partial-distance) tuples that the SDK consumes as it walks the graph.
The contract:
- Determinism (§2.1 of spec/0004): for the same query, the same
GraphIndexObject, the same fetched GraphPage bytes, two implementations MUST yield the same trajectory in the same order. - Distributability (§2.2 of spec/0004): the SDK MUST be able to reproduce the search from the GraphIndex Object + the GraphPage Objects alone.
- Locality: greedy local steps MUST converge toward the global nearest neighbors. Vamana's α-pruning property gives a provable convergence bound (Subramanya et al. 2019, Thm 1); we restate the practical implication in §4.
Partition-based and graph-based algorithms register under the same algorithm registry (spec/0004 §3.4). A modality declares one or the other in its registry entry; the SDK dispatches accordingly.
3. The GraphIndex Object
Address path:
(New top-level namespace, parallel to spatial-index/, scalar-index/, vector-compressor/, federation-manifests/.)
3.1 CBOR encoding
For Vamana, params.version = 1 is the legacy unidentified form and MUST omit
embedding_spec_id; params.version = 2 is the identified form and MUST carry
it. Every other combination is malformed. This version change is the critical-
extension boundary required by 0002 §3.1.0: a pre-identity reader rejects
version 2 before graph traversal instead of ignoring the new map key and
silently searching vectors from an unknown embedding space.
Versions 3 and 4 are compressed successor profiles. Both require a
non-null compressor_hash; embedding_spec_id may be present or absent, but
MUST agree with the Schema and registry as in §3.4. Version 3 selects ADC-only
ranking (Schema.rerank = false); version 4 selects exact-pool reranking
(Schema.rerank = true, §4.4.1). Versions 1/2 retain their existing encoding
and do not imply exact-source availability. Unsupported versions MUST be
rejected before traversal. No new optional reference-bearing map key is added.
3.2 Reference from the Manifest registry
A modality using graph-based ANN declares it in the registry, parallel to spec/0004 §3.3:
graph_index and spatial_index (spec/0004) are mutually exclusive in a single registry entry. A modality is partition-based OR graph-based, not both.
object_kind = "graph-page" is a distinct Track-index shape. Its inline and
paged leaf entries are [page_hash] in page-index order. It MUST NOT be decoded
as the unbucketed Item shape: an Unbucketed Item entry also carries a time
anchor because its Object path is <timeline>/<modality>/<time-anchor>/<hash>,
whereas a GraphPage lives at the literal graph-page/ path in §4.2. A
time-range query against a GraphPage-list Track MUST refuse rather than return
page hashes as if they were Items.
3.3 Modality-string parameterization
The .graph token replaces bucketed storage. The parameter key is lowercase
r; the mathematical symbol and GraphIndex field remain R. Optional compression
is bound by the registry/GraphIndex agreement in §3.4. The former
.compress=<algo>:<param> proposal is withdrawn (0010 §4), not an alternative
source of compressor identity or part of this valid tag example.
Example:
embedding.f32.dim=768.graph.r=64— a 768-dimensional graph with maximum out-degree 64; consult its bound GraphIndex for vector storage.
3.4 Registry-vs-GraphIndex consistency (mandatory validation)
The SDK MUST validate at load time, parallel to spec/0004 §3.3.1 and spec/0010 §3.3:
- Algorithm match: GraphIndex Object's
algorithmequals registryalgorithm. - Dimensionality / R consistency: GraphIndex Object's
dimandgraph_layout.Rmatch modality parameters. - Vector layout: Schema
compressor, registryvector_compressor, and GraphIndexvector_layout.compressor_hashMUST agree, including absence. The verified compressor MUST have the same dimension and cosine metric;record_bytesMUST equal itscode_bytes(or4 * dimwithout a compressor).page_node_countMUST be nonzero. GraphPage index, first node id, node count, record width, R and modality prefix MUST agree with the exact GraphIndex. - Entry-point validity:
entry_point < node_count. - Embedding identity: an identified modality's
specparameter, registryembed_spec, and GraphIndexembedding_spec_idMUST agree. All three are absent for a legacy unidentified graph.
Mismatch ⇒ reject Manifest as malformed (ManifestCorrupted).
4. dreamdb.vamana-cosine
The flagship graph algorithm. Vamana (Subramanya et al. 2019) is a directed R-regular graph with α-pruned edge selection: starting from a random graph, two passes refine edges so that greedy search converges quickly.
4.1 Params (CBOR)
4.2 GraphPage Object format
Each GraphPage holds page_node_count nodes. Layout:
Variable-size records (adj_count may vary per node) preclude pure positional indexing. The header's node_count_in_page plus a per-page offset table (appended after the last node) MAY be added in a follow-up if measurement shows it's needed; v0.X reads pages whole.
Address path:
4.2.1 Page binding and Fresh lineage
For dreamdb.vamana-cosine, graph_index_hash retains its v1 meaning: it
MUST equal the hash of the exact GraphIndex Object through which the page is
being assembled. A mismatch is malformed and MUST NOT be ignored.
dreamdb.fresh-vamana-cosine uses the same field as a stable lineage-root
binding, under that algorithm's recognisable parameter discriminant
(0016 §4.1.1). Its root is an immutable batch
dreamdb.vamana-cosine GraphIndex. The root's existing pages already carry
that GraphIndex hash, so an incremental snapshot can reuse an unchanged page
byte-for-byte. Every new or rewritten page in the Fresh lineage MUST put the
same root hash in graph_index_hash.
This is not permission to accept an arbitrary old page. Before assembling a
Fresh snapshot, a reader MUST validate the current GraphIndex against the root
and the invariant family fields in 0016 §4.2.1, then require every selected
page's graph_index_hash to equal that one validated root.
Page indices, node ranges, and Track membership remain properties of the
current snapshot. A page from another graph with identical algorithm
parameters has a different root and is rejected.
The field remains reference-bearing. A semantic copier or collector that
traverses GraphPages MUST treat it as a reference to the batch GraphIndex it
names. The Fresh GraphIndex root reference adds closure and therefore lives
behind the Fresh algorithm/parameter version boundary rather than an
unknown-key compatibility hatch (0002 §3.1.0 and §3.1.4).
A node-id maps to (page-index, in-page-offset). The SDK consults the GraphIndex Object's graph_layout.page_node_count to compute the page-index for any node-id; the in-page-offset is found by scanning the page header forward (cheap — pages are typically 64 KiB).
4.3 Build algorithm (compaction-time)
Vamana is built during a special compaction phase (per spec/0006 §7.3's compaction verb). The build is deterministic given:
- The set of input vectors (their content-addressed identity).
- The GraphIndex Object's
build_seed,L_build,alpha,build_passes.
Procedure:
- Random init: every node has R out-edges to randomly chosen other nodes. Initialize ChaCha20 with
build_seedas its 32-byte key, an all-zero 12-byte nonce and counter 0. Consume one continuous stream in ascending node-id order. Each candidate draw is the next 8 stream bytes interpreted as a little-endian u64, reduced modulo the node count; discard self ids and duplicates without rewinding the stream. - Pass 1 (alpha=1.0): for each node v in deterministic order (sorted by node-id):
a. Greedy-search from
entry_point(or current pass's entry point) toward v with search-list sizeL_build. b. The visited setV_vbecomes the candidate edge set. c. α-prune: starting from the nearest candidate, add edges greedily; for each candidate c, accept iff no already-accepted edge c' satisfiesdist(c, c') × α < dist(v, c). (At α=1.0 this is standard nearest-neighbor pruning.) d. Cap at R out-edges; if diversity pruning accepted fewer than R, fill in ascending(distance, node-id)order from the remaining candidates, including candidates rejected by the diversity test, until R or the candidate set is exhausted. - Pass 2 (alpha=user-supplied, default 1.2): repeat the pass with the user-supplied α. The diversity-boosting effect of α > 1 gives better recall at high
L_search. - Materialize GraphPages: assign node-ids to pages in node-id order; emit each GraphPage Object.
Determinism notes:
entry_pointMUST be deterministic. The convention: pick the medoid of the first 10,000 vectors (or all vectors if fewer); ties broken by smallest node-id. Approximate medoid (cheap to compute) suffices; full medoid is too expensive at 10B.- All inner-loop floating-point operations follow spec/0004 §5.4.1 discipline (scalar reference path, no FMA, no -ffast-math).
- The build is single-threaded by spec. Parallel build is permitted ONLY if it produces bit-identical results (typically requires careful work-stealing with deterministic seed-propagation). Implementations that can't guarantee bit-identical parallel build MUST fall back to single-thread for conformance test vectors.
4.4 Search algorithm
At query time, given query vector q and target k:
The entry point is both the initial frontier node and the first result
candidate. Discovery is not a prerequisite for result membership: in a valid
one-node graph with no edges, the loop discovers no neighbor and MUST still
return the entry point for k >= 1. Implementations MUST NOT initialize
results empty while putting the entry point only in candidates/visited;
that shape incorrectly returns an empty result for the one-node case.
Per spec/0010 interop: when the GraphIndex references a vector_compressor, dist(q, vector_for(adj_node)) is computed via the ADC lookup table built once at query start.
Latency profile (1B-scale, R=64, compressed with QINCo M=8, L_search=100):
- ~150–300 hops per query (Vamana on 1B-scale; varies with α).
- Each hop fetches one GraphPage (~64 KiB) — most are warm-cache after the first ~50 hops cover the relevant graph region.
- Cold-start: ~50 cold GETs × ~50 ms over WAN = unacceptably slow.
- Warm-cache: ~250 hops × ~50 µs of local memory access + ~50 µs ADC scoring = ~25 ms per query.
These are illustrative cache assumptions, not measured or mandatory latency bounds. An implementation may prefetch the entry-point region within its cache budget; neither a pinned K-hop neighborhood nor this latency target is a protocol conformance requirement. Query results must remain correct on a cold cache.
4.4.1 Exact reranking and compressed successors (OQ-56)
Dataset::compress_graph_index transforms an existing raw batch graph without
changing node ids or adjacency. It publishes all encoded pages, the compressor,
GraphIndex, Track, Schema and registry change under one optimistic Ref update.
Its ordinary lineage edge names the exact raw predecessor binding, which
remains in lineage, not query-active. The predecessor registry entry is
retained. Recompression of a compressed predecessor is not supported.
For version 4, the immediate predecessor MUST be a raw version-1/2 GraphIndex
of the same logical field, dimension, embedding identity, node count and page
partition. It is the exact source, addressed by node id, not by time-anchor
uniqueness. The source node's anchor MUST agree with the compressed node, and
encoding its normalized vector with the bound compressor MUST reproduce that
node's codes. Missing sources or contradictions MUST refuse; never return an
approximate fallback under rerank = true.
Traversal uses a single query LUT and ADC for all compressed nodes. The retained
beam has width max(L_search, top_k); tombstones remove ineligible result nodes
before exact reads. The remaining beam is the rerank pool. Every pool member
is rescored by deterministic cosine against the corresponding raw GraphPage
record, then sorted descending with node-id tie breaks and truncated to top-k.
No outside-pool candidate can return. This is a direct exact-vector read, not
a second graph search, and makes no global exact-nearest-neighbor guarantee.
Version 3 returns ADC ranking with approximate reconstructed vectors.
GC MUST retain registry GraphIndex roots as well as Track-listed GraphPages,
for both active and lineage bindings. A GraphPage list does not itself mark its
GraphIndex root. Collectors lacking this walk are unsafe even for raw graphs
and MUST be upgraded before collecting a graph-containing retained root.
No rerank_storage registry field is defined for graphs. Partition-based VS
references remain per SpatialBucketEntry (0010), not per graph registry.
The first implementation trades storage for compatibility: retaining raw
GraphPages retains their adjacency bytes too. Compression reduces vector bytes
read during traversal, not total retained storage; selected exact pages are
additional I/O. A compact exact-only sidecar would require its own future
format and closure contract. audit_graph_index checks every node/page and,
for version 4, its exact-source correspondence; ordinary traversal reads only
visited and selected exact pages.
Rust, Python and WASM expose raw construction, compression, reopen/query and
audit_graph_index / auditGraphIndex. The scored query path shares traversal
and reranking with the batch path. Compound graph predicates, partition trace
requests and IVF nprobe are explicitly unsupported; they MUST NOT be ignored
or reported as an executed partition pre-filter plan.
4.5 Storage cost
For 1B nodes, R=64, dim=768 with QINCo M=8 compression:
- Per node: 8 (anchor) + 8 (compressed vector) + 4 (adj_count) + 64×8 (adj_list) = 532 bytes.
- Per page (256 nodes): 256 × 532 = ~133 KB raw, ~64 KiB after typical compression (zstd at REST is operator-layer).
- Total storage: 1B × 532 B = 532 GB.
This is assumed payload arithmetic, not a measured codec ratio, recall result
or deployment recommendation. 0010 §10's code-only total excludes anchors,
indexes and retained exact sources, so comparing it with graph records is not a
complete storage comparison. Graph-versus-partition choices and their recall
must be measured under the same workload and environment (0023); QINCo remains
a proposal, not an available baseline guaranteed by this specification.
5. Incremental updates (explicit raw Fresh Vamana)
DreamDB's content layer is strictly immutable. A streaming-update FreshDiskANN-style mechanism produces new GraphPage Objects per insertion, rewriting affected pages and reusing unaffected pages under §4.2.1's stable lineage-root binding. This is expensive (~kB-MB write amplification per insertion) but does not require rewriting the whole graph merely to refresh a snapshot hash.
The reference Dataset provides caller-driven raw Fresh append and id-preserving
consolidation under 0016 §4 (#318). These operations publish new immutable
snapshots; they do not install a background scheduler or promise out-of-core
construction. Operators may still build a separate batch graph, but an auxiliary
GraphIndex is not a Track and must use its field-qualified registry binding.
The storage binding for dreamdb.fresh-vamana-cosine is fixed by §4.2.1 and
0016 §4. Compressed Fresh construction remains a separate follow-up; an
implementation that does not implement the Fresh algorithm MUST reject it
rather than interpreting its pages with the batch current-snapshot rule.
6. Cross-shard routing (resolves OQ-51)
This section defines the federation-level routing mechanism implemented by the reference coordinator (#319). Static single-graph conformance alone does not establish this capability or independent-implementation agreement.
The scatter-gather model in spec/0012 §6.2 incurs O(N_shards × per-shard-query) work. For vector queries at extreme scale (10B+ across 100+ shards), this dominates. A graph-based router resolves it.
6.1 The router GraphIndex
A router uses dreamdb.vamana-cosine params version 5, with the CLOSED
payload below. It is not a Dataset GraphPage index. Old decoders MUST reject
the unknown version; shard_routing MUST NOT be added to an older profile.
Every nested map of this profile is CLOSED. The outer map has exactly the
five ordinary keys (algorithm, dim, metric, params, graph_layout),
shard_routing, and optionally embedding_spec_id.
There is exactly one node per shard, in the Federation's canonical shard-id
order; its ordinal is its node id. Every (id, manifest) and the schema
commitment MUST match that exact Federation snapshot. Missing, foreign,
duplicate or stale entries are malformed, even if the router's own hash is
correct. No moving Ref participates in routing identity.
Vectors are finite unit representatives (squared binary64 norm within 1e-5
of 1); zero/nonfinite inputs are refused. Build normalizes finite input using
a dimension-order binary64 sum and square root, then casts each quotient to
f32. Representative quality is an operator assertion, not a certified centroid
or a global recall guarantee. Neighbor ids are strictly increasing, in range,
exclude self, and number at most R. fanout is in quorum..=len(shards).
Layout node_count == page_node_count == len(shards), page_bytes_target=0,
entry point is in range, R is positive, and vector layout has exactly
record_bytes=4*dim, with no compressor hash. All vectors and adjacency live
inline in this GraphIndex: there are no router GraphPage objects. Params
retain the six Vamana keys; alpha is finite and at least 1, build passes and
L_build are positive, and L_search is at least fanout. This first profile is
for small coordinator-resident graphs, not out-of-core routing.
Federation version 2 adds the required router multihash to its CLOSED
map (0012 §3.1.1). It is a coordinator-local GC edge. The shard Manifest
addresses repeat the Federation's remote bindings, not coordinator-local
roots. Ordinary Dataset readers MUST reject the router profile.
6.2 Router-driven query path
The public result MUST distinguish selected and intentionally pruned shard
ids from selected shards whose replicas failed. partial continues to mean
selected-shard unavailability, not global exact recall. A successful routed
result is complete only over its declared selected scope. No-router vector
queries still scatter to all shards; time-range queries retain scope routing.
An identity mismatch or malformed router MUST NOT silently fall back to all
shards or to another embedding space.
6.3 Router build and refresh
The coordinator builds from one supplied representative per shard using §4.3. It validates the exact snapshot binding before writing, makes the router durable, and publishes the Federation by its ordinary Ref CAS. A changed shard Manifest requires a new router binding and Federation snapshot, even if the representative stays identical. There is no time-based freshness exemption. Readers independently verify router bytes and binding at open; collectors retain routers for every retained Federation ancestor and refuse an incomplete closure before sweep. Router pages, automatic representative extraction, background refresh and compressed routing vectors are outside this profile.
7. Determinism conformance
The combination of f32 left-fold + scalar-reference (spec/0004 §5.4.1) + neural-MLP determinism (spec/0010 §7.3.1) covers most of what Vamana needs. Additional graph-specific requirements:
- Build node order: ascending node-id. No exceptions; even parallel implementations process completion in this order.
- Tiebreak in greedy search: when two candidates have identical distance, the smaller node-id wins. Ties at scale are rare but must be deterministic.
- Random tiebreak in α-pruning: when α-prune evaluation has identical results for two candidates, the smaller node-id wins. (Not a random tiebreak — deterministic.)
The conformance suite (spec/0009 §8.5.3) ships a canonical reference corpus and complete graph (dim=64, 10,000 nodes, R=8), plus three search trajectories. The corpus is raw row-major f32le; the graph is fixed-width row-major u32le adjacency. Their vector record pins content hashes, dimensions, build parameters, entry point, final ordered search results and every expanded (node-id, cosine-score-bits) pair. This lets an independent implementation consume the inputs and expected outputs without linking the implementation that generated them.
8. Storage and latency at 10B-scale
Worked example. 10 backends, 1B vectors each, dim=768, QINCo M=8 + Vamana R=64.
- Per-backend graph storage: 532 GB (per §4.5).
- Total federation storage: 5.32 TB.
- Cross-shard router: 10⁴ nodes × ~500 B = ~5 MB. Negligible.
Latency:
- Cold-start at scale: ~30 ms for router-graph greedy (10⁴ nodes, all warm if router is pinned) + ~30 ms × K_router shards scatter (parallel) + ~30 ms gather/rerank = ~90 ms p50.
- Warm-cache: ~25 ms total — the router fan-out reduction means we only contact 4–8 shards instead of 10.
Compared to spec/0012 §6.4 scatter-gather without router: ~40–100 ms p50 single-backend × N=10 = bounded by slowest shard, but p99 grows linearly with N. The router reduces effective N to K_router, dropping p99 proportionally.
9. Out of scope
- HNSW (hierarchical small-world graphs). Vamana is single-layer; HNSW's multi-layer hierarchy is a different storage/latency tradeoff. Defer to v0.X+1.
- Learned-edge graphs (NSG/SSG with learned pruning policies). Vamana's α-pruning is closed-form; learned variants require a training pipeline. Defer.
- Graph-aware deletion. Vamana under v0.X is append-and-rebuild. Tombstone-based deletion is theoretically tractable; defer.
- Concurrent build. v0.X is single-thread for conformance. A deterministic-parallel build is an open research question; defer.
10. Open questions
- OQ-53 (→ this spec): GraphPage offset tables. The current variable-size record format requires scanning to find a specific node within a page. Add a per-page positional index? Defer until benchmarks show scan cost dominates.
- OQ-54 (→
0016): Streaming graph updates. Resolved:0016§4 definesdreamdb.fresh-vamana-cosine; the Dataset runtime provides raw-root incremental append, caller-driven consolidation, tombstone filtering and page-lineage reuse. Its initial writer is in-memory, not an out-of-core implementation. Compressed Fresh graphs and mid-iteration learned-graph approaches remain separate work. - OQ-55 (→ spec/0009): Resolved.
dreamdb-conformance/vectors/0013/graph-build/002-*fixes the dim-64, 10,000-node, R=8 corpus, full adjacency and three complete build/search trajectories. Companion hashes and generation provenance are pinned in the JSON vector; an independent implementation needs only the documented raw f32le/u32le files. - OQ-56 (→ spec/0010): Resolved. §4.4.1 specifies direct exact-source reads for the selected candidate pool, not another graph search. The first graph implementation retains raw GraphPages through lineage; it does not introduce a graph-level VS reference or alter partition sidecars.
Next: spec/0014 — streaming/CMAF extensions for video at scale (or revisit chunking spec).