Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/controllers/apotheosis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ where
self.records.is_empty()
}

/// HNSW nodes and edges per layer, layer 0 first. Same data `draw()` writes as GEXF.
#[allow(clippy::type_complexity)]
pub fn draw_model(&self) -> Vec<(Vec<usize>, Vec<(String, String, f32)>)> {
self.hnsw.draw_model()
}

/// Performs an approximate k-NN search. If the `MetricId` natively maps to a Radix Tree key
/// (e.g. TLSH string), it jumps directly to the HNSW node to retrieve neighbors, bypassing full traversal.
/// Otherwise, it performs a standard HNSW k-NN search using the `query`.
Expand Down
7 changes: 7 additions & 0 deletions src/controllers/hnsw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ where
D: DistanceAlgorithm<ID> + Default,
{
pub fn new() -> Self {
const {
assert!(
M >= 2,
"M must be >= 2: with M=1, ln(1)=0 in random_level() diverges to usize::MAX"
);
};

Self {
features: vec![],
upper_layers: vec![],
Expand Down
6 changes: 3 additions & 3 deletions tests/ann_correctness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fn multi_layer_search_correct_across_layers() {
inserted.push(value);
}

let layer_count = idx.hnsw.draw_model().len();
let layer_count = idx.draw_model().len();
assert!(
layer_count > 1,
"dataset must produce upper layers to exercise the zero/upper index asymmetry, got {layer_count} layer(s)"
Expand Down Expand Up @@ -172,8 +172,8 @@ fn heuristic_produces_different_graph_than_greedy() {
.collect()
};

let greedy_edges = edges_for(greedy.hnsw.draw_model()[0].1.clone());
let heuristic_edges = edges_for(heuristic.hnsw.draw_model()[0].1.clone());
let greedy_edges = edges_for(greedy.draw_model()[0].1.clone());
let heuristic_edges = edges_for(heuristic.draw_model()[0].1.clone());

assert_ne!(
greedy_edges, heuristic_edges,
Expand Down
2 changes: 1 addition & 1 deletion tests/api_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fn draw_produces_one_gexf_file_per_layer() {
idx.insert(SimpleNumberRecord::create(i.to_string()));
}

let layer_count = idx.hnsw.draw_model().len();
let layer_count = idx.draw_model().len();
idx.draw(&base);

for n in 0..layer_count {
Expand Down
24 changes: 12 additions & 12 deletions tests/config_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ fn sync_invariant_min_viable() {
for i in 0..10u32 {
idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique
}
let zero_len = idx.hnsw.draw_model()[0].0.len();
assert_eq!(idx.records.len(), 10);
let zero_len = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 10);
assert_eq!(zero_len, 10);
assert_eq!(idx.records.len(), zero_len);
assert_eq!(idx.len(), zero_len);
}

#[test]
Expand All @@ -60,10 +60,10 @@ fn sync_invariant_inverted_ratio() {
for i in 0..10u32 {
idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique
}
let zero_len = idx.hnsw.draw_model()[0].0.len();
assert_eq!(idx.records.len(), 10);
let zero_len = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 10);
assert_eq!(zero_len, 10);
assert_eq!(idx.records.len(), zero_len);
assert_eq!(idx.len(), zero_len);
}

#[test]
Expand All @@ -72,10 +72,10 @@ fn sync_invariant_equal_m_m0() {
for i in 0..10u32 {
idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique
}
let zero_len = idx.hnsw.draw_model()[0].0.len();
assert_eq!(idx.records.len(), 10);
let zero_len = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 10);
assert_eq!(zero_len, 10);
assert_eq!(idx.records.len(), zero_len);
assert_eq!(idx.len(), zero_len);
}

#[test]
Expand All @@ -84,10 +84,10 @@ fn sync_invariant_dense() {
for i in 0..10u32 {
idx.insert(SimpleNumberRecord::create(i.to_string())); // safe: keys are unique
}
let zero_len = idx.hnsw.draw_model()[0].0.len();
assert_eq!(idx.records.len(), 10);
let zero_len = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 10);
assert_eq!(zero_len, 10);
assert_eq!(idx.records.len(), zero_len);
assert_eq!(idx.len(), zero_len);
}

// ---------------------------------------------------------------------------
Expand Down
18 changes: 9 additions & 9 deletions tests/config_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ fn build_dataset_heuristic() -> ApoNumHeuristic {
#[test]
fn ff1_sync_invariant_default() {
let idx = build_dataset_default();
let zero_layer_count = idx.hnsw.draw_model()[0].0.len();
assert_eq!(idx.records.len(), 10, "records must contain 10 entries");
let zero_layer_count = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 10, "records must contain 10 entries");
assert_eq!(zero_layer_count, 10, "zero_layer must contain 10 nodes");
assert_eq!(
idx.records.len(),
idx.len(),
zero_layer_count,
"records and zero_layer diverged"
);
Expand All @@ -79,11 +79,11 @@ fn ff1_sync_invariant_default() {
#[test]
fn ff1_sync_invariant_small() {
let idx = build_dataset_small();
let zero_layer_count = idx.hnsw.draw_model()[0].0.len();
assert_eq!(idx.records.len(), 10, "records must contain 10 entries");
let zero_layer_count = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 10, "records must contain 10 entries");
assert_eq!(zero_layer_count, 10, "zero_layer must contain 10 nodes");
assert_eq!(
idx.records.len(),
idx.len(),
zero_layer_count,
"records and zero_layer diverged"
);
Expand All @@ -92,11 +92,11 @@ fn ff1_sync_invariant_small() {
#[test]
fn ff1_sync_invariant_heuristic() {
let idx = build_dataset_heuristic();
let zero_layer_count = idx.hnsw.draw_model()[0].0.len();
assert_eq!(idx.records.len(), 10, "records must contain 10 entries");
let zero_layer_count = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 10, "records must contain 10 entries");
assert_eq!(zero_layer_count, 10, "zero_layer must contain 10 nodes");
assert_eq!(
idx.records.len(),
idx.len(),
zero_layer_count,
"records and zero_layer diverged"
);
Expand Down
16 changes: 6 additions & 10 deletions tests/fitness_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ fn sync_invariant_holds_after_bulk_insert() {
idx.insert(SimpleNumberRecord::create(i.to_string()));
}

let hnsw_len = idx.hnsw.draw_model()[0].0.len();
let hnsw_len = idx.draw_model()[0].0.len();

assert_eq!(
idx.records.len(),
idx.len(),
50,
"records[] should contain exactly 50 entries after 50 inserts"
);
Expand All @@ -41,7 +41,7 @@ fn sync_invariant_holds_after_bulk_insert() {
"HNSW zero_layer should contain exactly 50 nodes after 50 inserts"
);
assert_eq!(
idx.records.len(),
idx.len(),
hnsw_len,
"records[] and HNSW must be in sync - parallel structures diverged"
);
Expand Down Expand Up @@ -211,18 +211,14 @@ fn sync_invariant_holds_after_duplicate_insert() {
let second = idx.insert(SimpleNumberRecord::create("7".to_string()));
assert!(!second, "duplicate insert must return false");

let zero_layer_count = idx.hnsw.draw_model()[0].0.len();
assert_eq!(
idx.records.len(),
1,
"records must not grow on duplicate insert"
);
let zero_layer_count = idx.draw_model()[0].0.len();
assert_eq!(idx.len(), 1, "records must not grow on duplicate insert");
assert_eq!(
zero_layer_count, 1,
"zero_layer must not grow on duplicate insert"
);
assert_eq!(
idx.records.len(),
idx.len(),
zero_layer_count,
"records and zero_layer diverged after duplicate"
);
Expand Down
Loading