From 49a928cdfdbed402ce292fa8bf7f18c953ebe548 Mon Sep 17 00:00:00 2001 From: ruvnet Date: Sat, 8 Aug 2026 18:35:48 -0400 Subject: [PATCH] fix(cluster-rag): reject invalid coordinates --- crates/ruvector-cluster-rag/src/cluster.rs | 19 +++++++++++++++++++ docs/adr/ADR-300-hierarchical-cluster-rag.md | 17 ++++++++++------- .../README.md | 2 +- .../gist.md | 4 ++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/crates/ruvector-cluster-rag/src/cluster.rs b/crates/ruvector-cluster-rag/src/cluster.rs index 515cd7d99..d72a2675b 100644 --- a/crates/ruvector-cluster-rag/src/cluster.rs +++ b/crates/ruvector-cluster-rag/src/cluster.rs @@ -38,6 +38,13 @@ pub fn kmeans(vectors: &[Vec], k: usize, iters: usize) -> KMeansResult { vectors.iter().all(|vector| vector.len() == dim), "all vectors must have the same dimension" ); + assert!( + vectors + .iter() + .flatten() + .all(|coordinate| coordinate.is_finite()), + "all vector coordinates must be finite" + ); // Initialise centroids from distinct points (deterministic k-means++ max-dist). let init_ids = crate::dataset::initial_centroid_indices(vectors, k); @@ -233,4 +240,16 @@ mod tests { assert_eq!(result.assignments, vec![0, 0, 1]); assert_eq!(result.cluster_sizes, vec![2, 1]); } + + #[test] + #[should_panic(expected = "all vector coordinates must be finite")] + fn nan_coordinates_are_rejected() { + let _ = kmeans(&[vec![0.0], vec![f32::NAN]], 1, 1); + } + + #[test] + #[should_panic(expected = "all vector coordinates must be finite")] + fn infinite_coordinates_are_rejected() { + let _ = kmeans(&[vec![0.0], vec![f32::INFINITY]], 1, 1); + } } diff --git a/docs/adr/ADR-300-hierarchical-cluster-rag.md b/docs/adr/ADR-300-hierarchical-cluster-rag.md index 5423eeb6e..0af72276b 100644 --- a/docs/adr/ADR-300-hierarchical-cluster-rag.md +++ b/docs/adr/ADR-300-hierarchical-cluster-rag.md @@ -12,19 +12,20 @@ ## Context -Agent memory corpora grow continuously. A corpus of 10K vectors (a single agent session) can be searched by brute force at ~670 QPS. At 1M vectors the same approach yields ~6 QPS — too slow for interactive use. RuVector needs a simple, zero-dependency cluster index that: +Agent memory corpora grow continuously. In the refreshed synthetic benchmark, a corpus of 10K vectors is searched by brute force at ~2,000 QPS. Assuming linear scan scaling, 1M vectors would yield roughly 20 QPS — too slow for latency-sensitive interactive use. RuVector needs a simple, zero-dependency cluster index that: 1. Reduces per-query scan cost without requiring a full HNSW graph. 2. Integrates with the coherence primitives already in `ruvector-coherence`. 3. Compiles to WASM for edge deployments. 4. Supports incremental inserts without graph maintenance. -This ADR records the design decision for a two-level cluster-summary index (ClusterTree) and a coherence-weighted query routing variant (CoherenceTree), both benchmarked against brute-force baseline. +This Proposed ADR records a prototype for a two-level cluster-summary index (`ClusterTree`) and a coherence-weighted query routing variant (`CoherenceTree`), both benchmarked against a brute-force baseline. The decision remains Proposed until validation on a real embedding corpus is complete. **Correction (2026-08-08):** renumbered this Proposed decision from ADR-298 to ADR-300 because ADR-298 is already accepted for namespace-merge routing. -K-means now performs a final assignment pass against the returned centroids so -cohesion and inverted lists cannot retain membership from the prior Lloyd step. +The prototype K-means now performs a final assignment pass against the returned +centroids so cohesion and inverted lists cannot retain membership from the prior +Lloyd step. --- @@ -102,7 +103,7 @@ Full RAPTOR builds cluster summaries using an LLM — the text summary becomes t ## Implementation Plan -1. `ruvector-cluster-rag` crate: **done** (this ADR). +1. `ruvector-cluster-rag` prototype crate: **done**; acceptance remains pending real-corpus validation. 2. Validate on real embedding corpus: next step (ann-benchmarks SIFT1M or MS-MARCO embeddings). 3. Online insert feature: buffer new vectors, absorb into nearest centroid after `N` inserts or `ttl` seconds. 4. Adaptive nprobe controller: borrow ruFlo feedback loop from `ruvector-speculative-ann`. @@ -112,7 +113,7 @@ Full RAPTOR builds cluster summaries using an LLM — the text summary becomes t --- -## Benchmark Evidence +## Prototype Benchmark Evidence Run: `cargo run --release -p ruvector-cluster-rag --bin benchmark` Date: 2026-08-08, x86_64 Linux, release build. @@ -125,9 +126,11 @@ Dataset: n=10,000, dim=128, k=10, 500 queries, k_clusters=40, nprobe=20. | CoherenceTree (50% nprobe) | 336.4 | 469.1 | 2972 | 0.775 | Memory overhead: 2.0% above raw leaf storage. -Acceptance gate: PASS (both cluster variants ≥ 0.70 recall@10). +Synthetic prototype gate: PASS (both cluster variants ≥ 0.70 recall@10). All numbers are from a real `cargo run --release` invocation. No aspirational values. +This evidence validates only the synthetic prototype gate; it does not accept the +ADR or establish production readiness without the planned real-corpus validation. --- diff --git a/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/README.md b/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/README.md index 1e1350f46..5ba387581 100644 --- a/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/README.md +++ b/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/README.md @@ -293,7 +293,7 @@ The 2% overhead is negligible. For edge/WASM deployments the centroid-only struc Search cost per query: O(k × d + nprobe × (n/k) × d) = O(d × (k + nprobe × n/k)) -Optimal nprobe balances the two terms. At k=40, n=10K, d=128, nprobe=20: this is 128 × (40 + 20 × 250) = 128 × 5040 ≈ 645K FLOP vs. FlatBrute's 128 × 10K = 1.28M FLOP — a theoretical 1.99× speedup. Measured speedup is 1.44–1.52×, consistent (remainder from sorting overhead and memory bandwidth). +Optimal nprobe balances the two terms. At k=40, n=10K, d=128, nprobe=20: this is 128 × (40 + 20 × 250) = 128 × 5040 ≈ 645K FLOP vs. FlatBrute's 128 × 10K = 1.28M FLOP — a theoretical 1.99× speedup. Measured speedup is 1.49–1.52×, consistent (remainder from sorting overhead and memory bandwidth). --- diff --git a/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/gist.md b/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/gist.md index 3d7250fde..7178dac28 100644 --- a/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/gist.md +++ b/docs/research/nightly/2026-08-07-hierarchical-cluster-rag/gist.md @@ -16,11 +16,11 @@ Decision record: [ADR-300](../../../adr/ADR-300-hierarchical-cluster-rag.md). AI agents that run across multiple sessions accumulate memory — past decisions, retrieved context, user preferences, task histories. A single long-running agent session can build tens of thousands of embedding vectors. Retrieving the right memory at query time is the linchpin of effective agent reasoning, and the naive approach — brute-force cosine or L2 scan over all stored vectors — does not scale. -At 10K vectors with 128 dimensions, brute-force search takes ~1.5ms per query on x86_64. At 1M vectors it would take ~150ms — far too slow for interactive agents or high-throughput pipelines. The industry default solution is HNSW (Hierarchical Navigable Small World graphs), which achieves excellent recall (~95%) with sub-millisecond latency but requires O(n·M·log n) memory and a significant bookkeeping cost for every insert and delete. For growing agent memory corpora that are continuously updated, this maintenance overhead is a real production burden. +At 10K vectors with 128 dimensions, the refreshed benchmark measures brute-force search at ~0.5ms per query on x86_64. Assuming linear scan scaling, 1M vectors would take roughly 50ms — too slow for latency-sensitive interactive agents or high-throughput pipelines. The industry default solution is HNSW (Hierarchical Navigable Small World graphs), which achieves excellent recall (~95%) with sub-millisecond latency but requires O(n·M·log n) memory and a significant bookkeeping cost for every insert and delete. For growing agent memory corpora that are continuously updated, this maintenance overhead is a real production burden. This nightly research implements a simpler alternative: a two-level cluster tree, loosely inspired by RAPTOR (Paranjape et al., ICLR 2024). The idea is to partition agent memory into k clusters via k-means, then at query time score each cluster's relevance and expand only the top-nprobe most promising ones. This is structurally equivalent to Inverted File Indexing (IVF) from FAISS, with one important addition: each cluster is also scored by its *internal cohesion* — the mean cosine similarity of members to their centroid — so tight, semantically concentrated clusters are preferred over loose, spread-out ones. -The result: 1.44–1.52× speedup over brute force at 50% nprobe coverage, with only 2% memory overhead above the raw vector storage, in a zero-dependency Rust crate that compiles to WASM. +The result: 1.49–1.52× speedup over brute force at 50% nprobe coverage, with only 2% memory overhead above the raw vector storage, in a zero-dependency Rust crate that compiles to WASM. The honest finding: on *uniform random data*, the coherence weighting adds no recall advantage — all clusters look equally cohesive. The benefit emerges on *structured data* where topic clusters have meaningfully different tightness. Measuring this on real agent memory embeddings is the next step.