horon-engine

Geodineum

horon-engine

Store data as a tree. Query it as a space.

Crates.io Documentation License: Apache-2.0

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. Placing a leaf is O(1) geometric work — one Möbius reflection, and no existing coordinate moves. Proof: PROOF.md, which is also candid about where the shipped embedding falls short of the theorem's hypothesis.

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 horon_engine::Store;

let store = Store::new();

// Tree storage; parents are auto-created
store.put("/courses/trauma/emdr_basis", b"EMDR Basiscursus").unwrap();
store.put("/courses/trauma/emdr_kind", b"EMDR Kind & Jeugd").unwrap();
store.put("/courses/systemisch/eft", b"Emotion Focused Therapy").unwrap();

// Retrieval and hierarchy
let data = store.get("/courses/trauma/emdr_basis").unwrap();
let kids = store.children("/courses/trauma").unwrap();

// Spatial query: structurally nearby nodes.
// emdr_kind comes first (same branch), then eft (sibling branch).
let neighbors = store.neighbors("/courses/trauma/emdr_basis", 3).unwrap();

// Metadata
store.set_meta("/courses/trauma/emdr_basis", "capacity", "24").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: Vec<u8> = encode_my_dimensions(...);
store.set_semantic("/courses/trauma/emdr_basis", coords.clone()).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(&coords, 5, 16..33).unwrap();

// Distance between two coordinate vectors, no store involved
let dist = Store::semantic_distance(&coords_a, &coords_b, 16..33);

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

[dependencies]
horon-engine = "0.6"

All arithmetic is gMath Q64.64 fixed point. The determinism contract is defined on the embedded profile, so build with:

GMATH_PROFILE=embedded cargo build

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. Exact: rings expand until a proven bound rules out the rest.
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 the embedding dimension:

use horon_engine::{HTTStorage, HTTStorageConfig};

let storage = HTTStorage::new(HTTStorageConfig {
    dimension: 8,
    max_memory_nodes: 50_000,
    cache_size: 5_000,
    ..Default::default()
});
storage.store("/data", b"value", Some("text/plain".into())).unwrap();

QueryAdapter

Pluggable query interface for building custom query languages on top of the store:

use horon_engine::store::{QueryAdapter, QueryResult};

impl QueryAdapter for MyAdapter {
    fn execute(&self, store: &Store, query: &str) -> Result<Vec<QueryResult>, StoreError> {
        // Parse query, call store methods, return results
    }
}

Concurrency

How it works

Traditional trees scale lookups with depth. Spatial indexes do not understand hierarchy. Four mechanisms remove the choice:

  1. Sarkar embedding. Every node gets a position in the Poincare disk; children sit at hyperbolic distance tau from their parent via Mobius reflection. Position is derived from tree shape, never stored.
  2. Path lookup. Path to node is an O(1) HashMap access, entirely independent of the spatial index.
  3. The cell index. The disk is cut into radial bands (by squared norm) and angular sectors (by a diamond pseudo-angle), both sized so cells stay comparable in hyperbolic terms as the disk stretches toward the boundary. A query reads its own cell, then walks rings outward, and stops only when a proven lower bound rules out every cell it has not visited. Exact — no candidate cap, no window, no count-based stopping rule. Where the older grid returned a tile owner and hoped, this one either proves it may stop or keeps looking.
  4. 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.

Architecture

ARCHITECTURE.md is the orientation document: the three positions a node can have (structural, semantic, concept) and which query serves each, the three independent things called "modes", the layer stack, and where the limits below come from. Read it before the source.

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
                 ├─ CellIndex            <- radial bands x angular sectors, exact
                 ├─ SemanticIndexCache   <- epoch-invalidated per-slice VP-trees
                 └─ PoincareDisk         <- hyperbolic geometry, Mobius transforms

Limits worth knowing

Stated plainly, because they affect how you should configure the engine.

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 vs 0.5.x
get ~0.9 µs
exists ~0.1 µs
put (into an existing populated tree) ~1.2 µs
put (fresh flat tree, n ≤ 100) 46.8–95.6 µs/node 37–150×
remove 7.9–30.7 µs/op 25–98×
nearest (full API) 51–99 µs, n = 10…200 3–5×
nearest at the origin 7.6 µs 10.8×
neighbors (k = 1…5, full API) 38–223 µs 19–55×
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

Structural rows re-measured 2026-08-22 for 0.6.0 on the machine above. Semantic rows are unchanged by that release.

Full-API queries pay for exact hyperbolic verification of every surviving candidate — that is the guarantee, not an inefficiency. The index's job is to make the number of survivors small, and the ring bound is what proves it may stop. Concurrent read throughput: ~2.6M reads/sec on 8 threads.

Read the nearest comparison honestly: the 0.5.x figure is the cost of an answer that was frequently wrong, so it measures the replacement of a broken thing rather than a tuning win.

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::Horon;

let htt = Horon::open("data.htt")?;
htt.put("/config/db", b"postgres://localhost")?;
htt.compact()?;

Ecosystem

Recent work

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).