Pure MoonBit approximate nearest-neighbor search (HNSW graph index) for f32 vectors, with L2 / inner-product / cosine metrics and binary save/load.
moon add yiwnagmolu/moonhnsw@0.12.4import {
"yiwnagmolu/moonhnsw" @moonhnsw,
}
pkgtype(kind: "executable")///|
fn main raise {
let index = @moonhnsw.Hnsw::new(2, @moonhnsw.Distance::L2)
let vector : FixedArray[Float] = FixedArray::from_array([1.0, 2.0])
let _ = index.add(vector)
let hits = index.search(vector, 1, ef=64)
println("nearest id=\{hits[0].id} distance=\{hits[0].distance}")
}| Signature | Description |
|---|---|
| Hnsw::new(dim, distance, params~) | Create an empty index over dim-dimensional vectors. |
| Hnsw::add(vector) -> Int | Insert one vector, returns its id. Raises on dimension mismatch. |
| Hnsw::add_labeled(vector, label) -> Int | Insert tagged with a UInt64 label of your choice (need not be unique). |
| Hnsw::label_of(id) -> UInt64 | Label stored for an id; defaults to the id itself. |
| Hnsw::upsert_labeled(vector, label) -> Int | Insert-or-replace by label: the document-update flow. |
| Hnsw::find_by_label(label) -> Array[Int] | Ids carrying a label (labels may repeat). |
| Hnsw::search_batch(queries, k, ef~) | One call for a list of queries. |
| Hnsw::search(query, k, ef~ = 64) -> Array[Neighbor] | Approximate top-k, sorted by distance ascending. |
| Hnsw::search_exact(query, k, filter~) -> Array[Neighbor] | Exhaustive top-k over live stored vectors, distance/id tie order. |
| Hnsw::search_filtered(query, k, ef~, filter) | Top-k restricted by a filter closure over ids (metadata filtering). |
| Hnsw::search_filtered_adaptive(query, k, ef_start~, ef_max~, filter) | Grow a post-filter candidate pool until k matches or the cap; returns neighbors, final ef, rounds, and distance work. |
| Hnsw::calibrate_ef(queries, k, target_recall, efs) | Measure recall and mean distance work at each ef; select the smallest ef meeting the target. |
| Hnsw::search_mmr(query, k, lambda~, ef~) | Maximal Marginal Relevance; lambda=1 keeps relevance, lambda=0 prioritizes diversity after the first result. |
| Hnsw::search_radius(query, radius, max_results~, filter~) | Radius search; composes with the filter closure. |
| Hnsw::search_radius_exact(query, radius, max_results~, filter~) | Exhaustive radius search with a completeness guarantee within the result limit. |
| Hnsw::shrink_to_fit() | Release store slack without rebuilding the graph. |
| Hnsw::from_vectors(dim, distance, vectors, params~) | One-call construction from a list of vectors. |
| Hnsw::delete(id) | Tombstone-delete a vector; it disappears from every search. |
| Hnsw::contains(id) -> Bool | True when id is stored and not deleted. |
| Hnsw::get_vector(id) -> FixedArray[Float] | Copy of the stored vector (normalized for Cosine). |
| Hnsw::stats() -> HnswStats | Per-layer node counts and average degree, for tuning. |
| Hnsw::compact() -> CompactResult | Tombstone-free rebuild plus old-id → new-id mapping. |
| Hnsw::absorb(other) -> FixedArray[Int] | Merge another index's live vectors (labels kept); returns old-id → new-id mapping. |
| Hnsw::copy() -> Hnsw | Independent deep clone, tombstones and labels included. |
| Hnsw::ids() -> Array[Int] | Ascending ids of all non-deleted vectors. |
| Hnsw::export_dot(layer) -> String | Graphviz DOT of one graph layer, for visualization. |
| Hnsw::save() -> FixedArray[Byte] | Serialize the full index. |
| Hnsw::load(bytes) -> Hnsw | Restore an index; rejects malformed payloads with CorruptData. |
| Hnsw::save_bytes() / load_bytes(Bytes) | Same payload wrapped as immutable Bytes. |
| Hnsw::size() -> Int | Number of non-deleted (searchable) vectors. |
| Hnsw::total() -> Int | Number of vectors ever inserted, including tombstones. |
| distance(metric, a, b) -> Double | Metric value between two vectors — handy for brute-force ground truth. |
| HnswError::message() -> String | Human-readable error text. |
///|
let calibration = index.calibrate_ef(sample_queries, 10, 0.95, [16, 32, 64, 128])
///|
let hits = index.search(query, 10, ef=calibration.selected_ef)///|
let result = index.search_filtered_adaptive(
query,
10,
ef_start=32,
ef_max=512,
filter=fn(id) { id % 2 == 0 },
)
///|
let hits = result.neighbors| Parameter | Default | Guidance |
|---|---|---|
| m | 16 | Links per node on upper layers (layer 0 keeps 2m). 12–48 is the usual range; higher = better recall, more memory. |
| ef_construction | 200 | Beam width while inserting. 100–500; higher builds a better graph. |
| ef (per search) | 64 | Beam width at query time. ef >= k is enforced; 64–512 covers most workloads. |
| storage | Float32 | SQ8 stores one byte per dimension after learning bounds from the first 256 vectors (~4x smaller vector store, data-dependent recall); later inserted values clamp to those bounds, while queries are not clamped. |
| Metric | n | Build | ef=64 | ef=128 | ef=256 |
|---|---|---|---|---|---|
| L2 | 2 000 | 577 ms | recall 1.00, 8297 q/s (0.49x brute, 980 dists) | recall 1.00, 5049 q/s (0.30x) | recall 1.00, 2903 q/s (0.17x) |
| InnerProduct | 2 000 | 676 ms | recall 1.00, 10809 q/s (0.58x brute, 891 dists) | recall 1.00, 6028 q/s (0.32x) | recall 1.00, 3205 q/s (0.17x) |
| Cosine | 2 000 | 488 ms | recall 1.00, 9099 q/s (0.49x brute, 970 dists) | recall 1.00, 5174 q/s (0.28x) | recall 1.00, 3197 q/s (0.17x) |
| L2 (scale) | 10 000 | 5290 ms | recall 0.97, 5669 q/s (1.50x brute, 1390 dists) | recall 1.00, 3164 q/s (0.84x) | recall 1.00, 1435 q/s (0.38x) |
| L2 (large) | 100 000 | 210 s | recall 0.83, 2041 q/s (13.9x brute, 1687 dists) | recall 0.95, 872 q/s (6.0x) | recall 1.00, 481 q/s (3.3x) |
| L2 SQ8 | 2 000 | 713 ms | recall 0.99, 6297 q/s (0.56x brute) | recall 0.99, 4101 q/s (0.36x) | recall 0.99, 2711 q/s (0.24x) |
moon run --target wasm-gc examples/benchmark
moon run --target wasm-gc examples/benchmark-largelayer 2 e────────────── (entry, a handful of nodes)
layer 1 a───b─────g─────── (greedy descent)
layer 0 a─b─c─d─e─f─g─h─i─... (all nodes, beam search)moon test # run the test suite
moon test --target wasm-gc
moon check --target wasm-gc --deny-warn
moon info && moon fmt # refresh generated interfaces, format
moon run --target wasm-gc examples/benchmarkOn Windows the native backend additionally requires MSVC Build Tools (cl.exe or clang-cl); wasm-gc and js targets work out of the box.
pub suberror HnswError {
InvalidParams(String)
DimMismatch(Int, Int)
CorruptData(String)
InvalidId(Int)
}pub struct Hnsw {
dim : Int
distance : Distance
params : HnswParams
normalize : Bool
level_mult : Double
entry : Int
max_level : Int
count : Int
alive_count : Int
alive : FixedArray[Bool]
labels : Array[UInt64]
storage : Storage
vectors : FixedArray[Float]
f_capacity : Int
quants : FixedArray[Byte]
q_capacity : Int
q_mins : FixedArray[Float]
q_maxs : FixedArray[Float]
q_scales : FixedArray[Float]
q_frozen : Bool
f_scratch : FixedArray[Float]
cand_items : Array[(Double, Int)]
result_items : Array[(Double, Int)]
links : Array[Array[Array[Int]]]
visited : FixedArray[Int]
stamp : Int
rng_state : UInt64
dist_calls : Int64
}fn Hnsw::search_filtered_adaptive(self : Hnsw, query : FixedArray[Float], k : Int, ef_start? : Int, ef_max? : Int, filter~ : (Int) -> Bool) -> AdaptiveSearchResult raise HnswErrorfn HnswParams::new(m? : Int, ef_construction? : Int, seed? : UInt64, storage? : Storage) -> HnswParamsInstall
Download zipPure MoonBit approximate nearest-neighbor search (HNSW graph index) for f32 vectors, with L2 / inner-product / cosine metrics and binary save/load.