horon-engine
Store data as a tree. Query it as a space.
The idea
Organize data in paths, like files in folders. The engine embeds that tree in hyperbolic space, so structural similarity becomes spatial proximity. One query primitive answers most questions: "what's nearby?"
Why hyperbolic space fits trees
Hyperbolic space grows exponentially with radius. Trees grow exponentially with depth. The match is exact: the engine places every node with Sarkar's construction, items in the same branch cluster, items in distant branches sit far apart. Each leaf insertion preserves the spatial structure with O(1) work, so the tree is its own spatial index. Proof: PROOF.md.
In production
A production course recommender runs on this engine: 183 courses and 741 students in one tree file. Recommendations are geometric, courses close to a student's enrollment history rather than keyword matches. The same file reveals miscategorized courses, demand gaps, and domains that share student populations.
Quick start
use Store;
let store = new;
// Tree storage; parents are auto-created
store.put.unwrap;
store.put.unwrap;
store.put.unwrap;
// Retrieval and hierarchy
let data = store.get.unwrap;
let kids = store.children.unwrap;
// Spatial query: structurally nearby nodes.
// emdr_kind comes first (same branch), then eft (sibling branch).
let neighbors = store.neighbors.unwrap;
// Metadata
store.set_meta.unwrap;
Semantic dimensions
Any node can carry a coordinate vector encoding domain meaning: categories, popularity, demand signals. Queries then run over any slice of those dimensions, and different slices answer different questions from the same data.
// Attach a 40-dimension coordinate vector (Q64.64, 16 bytes per dim)
let coords: = encode_my_dimensions;
store.set_semantic.unwrap;
// The 5 nearest nodes by dimensions 16..33 (category axes),
// as Vec<(path, distance)> sorted by distance across exactly those dims
let similar = store.nearest_semantic.unwrap;
// Distance between two coordinate vectors, no store involved
let dist = semantic_distance;
This is how a catalog separates an item's labeled category from where its population actually positions it. The gap between the two is a miscategorization, visible in geometry and invisible in metadata.
Install
[]
= "0.5"
All arithmetic is gMath Q64.64 fixed point. The determinism contract is defined on the embedded profile, so build with:
GMATH_PROFILE=embedded
API reference
All methods take &self. Share freely via Arc<Store> across threads.
Per-symbol truth lives in the docblocks: cargo doc --open.
| Method | What it does |
|---|---|
put(key, data) |
Insert or update. Auto-creates parent nodes. |
put_data_only(key, data) |
Insert without a geometric embedding. The cheap bulk path. |
embed_existing(key) / embed_all(prefix) |
Upgrade data-only keys to full embeddings, in place. |
get(key) |
Retrieve data by path. |
remove(key) |
Delete a node. |
exists(key) |
Check existence. |
children(path) |
List direct children. |
list(prefix) |
List the full subtree. |
set_meta(key, k, v) / get_meta(key) |
Per-node key-value metadata. |
nearest(coords) |
True nearest node to a point. O(log n): the grid proposes candidates, hyperbolic distance decides. |
nearest_k(coords, k) |
The k nearest nodes to a point. |
neighbors(key, k) |
The k nearest neighbors of a stored node. |
find_within(key, r) |
All nodes within hyperbolic radius r. |
position(key) |
A stored node's Poincare coordinates. |
set_semantic(key, coords) / get_semantic(key) |
Attach or read dimensional coordinates (raw Q64.64 bytes). |
nearest_semantic(coords, k, range) |
k nearest by Euclidean distance across a dimension slice. |
neighbors_semantic(key, k, range) |
Semantic neighbors of a stored node. |
find_similar(key, k, range) |
"What's like this one?": task-shaped name for neighbors_semantic. |
find_outliers(prefix, z, range) |
Nodes anomalously far from their peers under a prefix (average k-NN distance, z-score). |
semantic_distance(a, b, range) |
Euclidean distance between two coordinate vectors. |
SemanticDisk::build(spec) |
Embed a concept taxonomy, derived from the data's own category tree, into a second Poincare disk. |
disk.concept_of(store, key) |
Which concept a node belongs to right now, from its affinity dims. The miscategorization primitive. |
disk.nearest(store, key, k) |
k nearest nodes in taxonomy-aware meaning-space. |
disk.classify_trajectory(...) |
Turn a Horon HoronHistory trajectory into a symbolic concept sequence across epochs. |
query(adapter, query) |
Execute a pluggable query via the QueryAdapter trait. |
len() / is_empty() |
Node count. |
HTTStorage, the layer below
Store wraps HTTStorage. Reach for it when you need direct control over
embedding dimension or grid resolution:
use ;
let storage = new;
storage.store.unwrap;
QueryAdapter
Pluggable query interface for building custom query languages on top of the store:
use ;
Concurrency
- Reads are lock-free:
get,exists,children,get_meta, and all spatial queries. No reader ever blocks another reader. - Writes stripe on the parent node: 64 lock stripes, so independent subtrees write in parallel.
- The spatial index is ~61 buckets, each with its own VP-tree and lock.
- There is no outer lock. Every method takes
&self; wrap inArc<Store>and share across threads, async tasks, or HTTP handlers.
How it works
Traditional trees scale lookups with depth. Spatial indexes do not understand hierarchy. Five mechanisms remove the choice:
- Sarkar embedding. Every node gets a position in the Poincare disk; children sit at hyperbolic distance tau from their parent via Mobius reflection. The tree is its own spatial index.
- Path lookup. Path to node is an O(1) HashMap access, independent of the spatial index. Nodes are additionally sharded into ~61 fixed buckets backing the VP-tree layer; that partition does not track hyperbolic growth, so occupancy is uneven and query cost suffers. Query results do not depend on it.
- Per-bucket VP-trees. Range and KNN queries in O(log n) under the hyperbolic metric.
- Nielsen power diagram. A Poincare-to-Klein projection and a uniform grid propose candidates for point location. The grid is an accelerator, not an oracle: it holds one owner per tile and Sarkar drives cells below tile size within a few levels, so the answer is always decided by hyperbolic distance.
- Semantic dimensions. Orthogonal to the spatial embedding: Euclidean distance over user-defined dimension slices, no tree change required.
Determinism
All geometry is gMath Q64.64 fixed point. There are no floats in the compute path. The same operation sequence produces bit-identical state on any platform; that property is what lets a write-ahead log double as a replication protocol. CI runs the suite on x86-64 and arm64.
Current limitations
Stated plainly, because they affect how you should configure the engine.
-
taumust scale with fan-out. PROOF.md's Delaunay guarantee holds only whentau >= -log(tan(pi / (2 * d_max)))for the tree's maximum node degree. The defaulttau = 1.0satisfies that up tod_max ~= 4.5. Beyond it the tree is still embedded, queried and returned correctly — it is simply outside the proven regime. Set it withStoreConfig::tau()for wider trees. -
Depth is bounded by precision, not by the format. A node sits at hyperbolic radius
depth * tau, and the Q64.64 distance kernel holds metric fidelity to a radius of about 17.5 (saturating near 22). Sotau = 1.0gives usable depth ~17, and raisingtaufor wider trees lowers reachable depth proportionally. -
nearestis O(log n), not O(1). The point-location grid cannot name a node whose power cell is smaller than a grid tile, which Sarkar placement causes within a few levels. The grid proposes candidates only.
Performance
Measured 2026-07 on an i7-7700 (4c/8t) with GMATH_PROFILE=embedded. Full
tables, machine specs, and reproduction commands:
BENCHMARKS.md.
| Operation | Measured cost |
|---|---|
get |
~0.9 µs |
exists |
~0.1 µs |
put (into an existing populated tree) |
~1 µs |
put (fresh flat tree, n ≤ 100) |
~3-7 ms/node (grid-tile assignment worst case) |
nearest (power diagram, full API) |
~250-290 µs |
neighbors (VP-tree KNN, full API) |
~3-10 ms (exact 0-ULP distance per candidate) |
remove |
~0.2-3 ms |
nearest_semantic (lazy per-slice VP-tree, warm) |
~283 µs at 10k nodes, ~309 µs at 100k, d=8 |
nearest_semantic (first query after a semantic write) |
~1.2 s at 10k nodes; index rebuild, amortizes after ~7 queries |
Geometric primitives run in nanoseconds (grid probe ~15 ns, power distance ~24 ns). Full-API queries pay for exact hyperbolic verification of every candidate. Concurrent read throughput: ~2.6M reads/sec on 8 threads.
These rows predate 0.5.0, whose proxy-space release cut the cost of every ranking loop (bulk insert and structural KNN most of all — fresh-tree insertion dropped roughly an order of magnitude). Treat the table as an upper bound until BENCHMARKS.md is re-measured on the reference machine.
Persistence
The engine is in-memory. Horon persists
it: the .htt single-file format with WAL durability, zstd compression,
snapshot compaction, and geometric access control.
use Horon;
let htt = open?;
htt.put?;
htt.compact?;
Architecture
Store <- public API, all &self, Arc-shareable
└─ HTTStorage <- path normalization, CRUD, striped parent locks
└─ HyperbolicTreeTensor <- path-to-signature maps (DashMap)
└─ HyperbolicTensorNetwork <- Sarkar embedding, spatial index
├─ HyperbolicHashTable <- ~61 buckets, per-bucket VP-trees
├─ PoincareDisk <- hyperbolic geometry, Mobius transforms
└─ PointLocationGrid <- candidate proposer (not an oracle)
Ecosystem
- gMath: Q64.64 fixed-point arithmetic with ZASC-Binary transcendentals.
- Horon:
.httWAL persistence over this engine.
Limits worth knowing
- Depth is a precision budget: Q64.64 supports roughly 44/τ levels before sibling separation degrades near the disk boundary; the engine warns as inserts approach it.
- Extreme fan-out (on the order of 1000+ siblings under one parent) can quantize to colliding position signatures; insertion probes forward to the next free slot, so placement stays correct but the golden-angle spacing guarantee weakens. Keep realistic tree shapes.
- Semantic dimensions are capped at 255 per node (16 reserved + up to 239 user); distances run over any slice of them.
- The engine is in-memory and single-process by design — durability, concurrent readers, and the on-disk layout live in Horon.
Recent work
- Semantic disk: the concept taxonomy embedded hyperbolically, positions derived from affinity dimensions (docs/SEMANTIC_DISK.md).
- Semantic spatial index: lazy per-slice VP-trees, ~690x faster at 10k
nodes (docs/SEMANTIC_INDEX.md), plus
find_similarandfind_outliers. - In Horon: temporal epochs (v0.6.0) and WAL-based replication (v0.5.0).
Author
Built by Niels Erik Toren. Support addresses and contribution guidelines live in the Horon README.
Disclaimer
This software is provided "as is", without warranty of any kind, express or implied. Use of this software is entirely at your own risk. In no event shall the author or contributors be held liable for any damages arising from the use or inability to use this software.
License
Apache-2.0 (see LICENSE).