fix bounded RAG flow and input validation
Some checks failed
ruvector-verified CI / check () (push) Has been cancelled
ruvector-verified CI / check (--all-features) (push) Has been cancelled
ruvector-verified CI / check (--features all-proofs) (push) Has been cancelled
ruvector-verified CI / check (--features coherence-proofs) (push) Has been cancelled
ruvector-verified CI / check (--features hnsw-proofs) (push) Has been cancelled
ruvector-verified CI / check (--features rvf-proofs) (push) Has been cancelled
ruvector-verified CI / check (--features serde) (push) Has been cancelled
ruvector-verified CI / check (--features ultra) (push) Has been cancelled
ruvector-verified CI / clippy (push) Has been cancelled
ruvector-verified CI / test (push) Has been cancelled
ruvector-verified CI / bench (push) Has been cancelled

This commit is contained in:
ruvnet 2026-07-27 12:45:06 -04:00
parent 0b85ccc16e
commit a8d548311f
6 changed files with 222 additions and 68 deletions

View file

@ -0,0 +1,17 @@
# ruvector-bounded-rag
Research implementations of context-budgeted retrieval for RuVector.
The crate compares cosine top-k retrieval, priority traversal over a dense
similarity graph, and an EdmondsKarp min-cut partition followed by relevance
ranking and budget truncation. The graph-based variants rebuild their pairwise
similarity structures per query and are intended as auditable research
baselines rather than production-scale indexes.
```sh
cargo test -p ruvector-bounded-rag
cargo run --release -p ruvector-bounded-rag --bin benchmark
```
See `docs/adr/ADR-272-bounded-rag-mincut.md` and the associated nightly
research report for methodology and limitations.

View file

@ -1,7 +1,7 @@
///! Bounded RAG MinCut benchmark
///!
///! Measures three retrieval variants across two dataset sizes.
///! Run: cargo run --release -p ruvector-bounded-rag --bin benchmark
//! Bounded RAG MinCut benchmark
//!
//! Measures three retrieval variants across two dataset sizes.
//! Run: cargo run --release -p ruvector-bounded-rag --bin benchmark
use ruvector_bounded_rag::{
BoundedRetriever, Corpus, GraphBfsRetriever, MinCutRetriever, Query, RetrieverConfig,
TopKRetriever,
@ -49,9 +49,7 @@ fn build_corpus(case: &BenchCase, rng: &mut StdRng) -> (Corpus, Vec<Query>) {
for x in qv.iter_mut() {
*x += normal.sample(rng) * 0.05;
}
queries.push(
Query::new(qv).with_relevant([target_cluster as u32]),
);
queries.push(Query::new(qv).with_relevant([target_cluster as u32]));
}
(corpus, queries)
@ -66,11 +64,7 @@ struct Stats {
mean_budget_util: f64,
}
fn run_variant(
retriever: &dyn BoundedRetriever,
corpus: &Corpus,
queries: &[Query],
) -> Stats {
fn run_variant(retriever: &dyn BoundedRetriever, corpus: &Corpus, queries: &[Query]) -> Stats {
let mut latencies_us: Vec<f64> = Vec::with_capacity(queries.len());
let mut precisions: Vec<f64> = Vec::with_capacity(queries.len());
let mut budgets: Vec<f64> = Vec::with_capacity(queries.len());
@ -95,7 +89,14 @@ fn run_variant(
let mean_precision = precisions.iter().sum::<f64>() / n as f64;
let mean_budget_util = budgets.iter().sum::<f64>() / n as f64;
Stats { mean_us, p50_us, p95_us, throughput_qps, mean_precision, mean_budget_util }
Stats {
mean_us,
p50_us,
p95_us,
throughput_qps,
mean_precision,
mean_budget_util,
}
}
fn print_header() {

View file

@ -11,8 +11,8 @@
//! | Variant | Strategy | Strength |
//! |---------|----------|----------|
//! | `TopK` | Cosine rank, no graph | Fastest, baseline |
//! | `GraphBfs` | BFS expansion with coherence gate | Budget-safe, O(V+E) |
//! | `MinCutBounded` | Max-flow/min-cut flow network | Optimal coherent partition |
//! | `GraphBfs` | Builds a dense similarity graph, then expands with a coherence gate | Budget-safe heuristic |
//! | `MinCutBounded` | Max-flow/min-cut flow network, then top-budget truncation | Coherent-partition heuristic |
//!
//! ## Quick start
//!
@ -66,15 +66,28 @@ pub struct Corpus {
impl Corpus {
pub fn from_vecs(vecs: Vec<Vec<f32>>) -> Self {
let dim = vecs.first().map(|v| v.len()).unwrap_or(0);
assert!(
vecs.iter().all(|vector| vector.len() == dim),
"all corpus vectors must have the same dimension"
);
let chunks = vecs
.into_iter()
.enumerate()
.map(|(id, vector)| Chunk { id, vector, label: None })
.map(|(id, vector)| Chunk {
id,
vector,
label: None,
})
.collect();
Self { chunks, dim }
}
pub fn with_labels(mut self, labels: Vec<u32>) -> Self {
assert_eq!(
labels.len(),
self.chunks.len(),
"label count must match chunk count"
);
for (chunk, label) in self.chunks.iter_mut().zip(labels) {
chunk.label = Some(label);
}
@ -98,7 +111,10 @@ pub struct Query {
impl Query {
pub fn new(vector: Vec<f32>) -> Self {
Self { vector, relevant_labels: HashSet::new() }
Self {
vector,
relevant_labels: HashSet::new(),
}
}
pub fn with_relevant(mut self, labels: impl IntoIterator<Item = u32>) -> Self {
@ -179,7 +195,32 @@ fn normalise(v: &[f32]) -> Vec<f32> {
/// Cosine similarity — assumes inputs are already L2-normalised.
#[inline]
fn cosine(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<f32>().clamp(-1.0, 1.0)
assert_eq!(a.len(), b.len(), "vector dimension mismatch");
a.iter()
.zip(b.iter())
.map(|(x, y)| x * y)
.sum::<f32>()
.clamp(-1.0, 1.0)
}
fn validate_config(cfg: &RetrieverConfig) {
assert!(cfg.budget > 0, "budget must be greater than zero");
assert!(
(-1.0..=1.0).contains(&cfg.edge_threshold),
"edge_threshold must be in [-1, 1]"
);
assert!(
(-1.0..=1.0).contains(&cfg.seed_threshold),
"seed_threshold must be in [-1, 1]"
);
}
fn validate_query(corpus: &Corpus, query: &Query) {
assert_eq!(
query.vector.len(),
corpus.dim,
"query dimension must match corpus dimension"
);
}
// ── Variant 1: TopK (baseline) ─────────────────────────────────────────────────
@ -191,6 +232,7 @@ pub struct TopKRetriever {
impl TopKRetriever {
pub fn new(cfg: RetrieverConfig) -> Self {
validate_config(&cfg);
Self { cfg }
}
}
@ -202,8 +244,13 @@ impl BoundedRetriever for TopKRetriever {
fn retrieve(&self, corpus: &Corpus, query: &Query) -> RetrievalResult {
if corpus.is_empty() {
return RetrievalResult { chunks: vec![], scores: vec![], budget_utilisation: 0.0 };
return RetrievalResult {
chunks: vec![],
scores: vec![],
budget_utilisation: 0.0,
};
}
validate_query(corpus, query);
let qn = normalise(&query.vector);
// Score every chunk
@ -217,7 +264,7 @@ impl BoundedRetriever for TopKRetriever {
.collect();
// Sort descending
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
scored.sort_by(|a, b| b.1.total_cmp(&a.1));
scored.truncate(self.cfg.budget);
let n = scored.len();
@ -239,13 +286,18 @@ impl BoundedRetriever for TopKRetriever {
/// 1. Find seed chunks with cosine(query, chunk) >= seed_threshold.
/// 2. BFS: expand to neighbours with edge weight >= edge_threshold.
/// 3. Stop when budget is exhausted.
///
/// Priority queue ensures high-affinity chunks enter first.
///
/// This research implementation rebuilds a dense similarity graph for every
/// query, costing O(n²·d) before the O(V+E) traversal.
pub struct GraphBfsRetriever {
cfg: RetrieverConfig,
}
impl GraphBfsRetriever {
pub fn new(cfg: RetrieverConfig) -> Self {
validate_config(&cfg);
Self { cfg }
}
@ -273,8 +325,13 @@ impl BoundedRetriever for GraphBfsRetriever {
fn retrieve(&self, corpus: &Corpus, query: &Query) -> RetrievalResult {
if corpus.is_empty() {
return RetrievalResult { chunks: vec![], scores: vec![], budget_utilisation: 0.0 };
return RetrievalResult {
chunks: vec![],
scores: vec![],
budget_utilisation: 0.0,
};
}
validate_query(corpus, query);
let qn = normalise(&query.vector);
let normed: Vec<Vec<f32>> = corpus.chunks.iter().map(|c| normalise(&c.vector)).collect();
@ -292,7 +349,7 @@ impl BoundedRetriever for GraphBfsRetriever {
}
impl Ord for Entry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.partial_cmp(&other.0).unwrap_or(std::cmp::Ordering::Equal)
self.0.total_cmp(&other.0)
}
}
@ -309,7 +366,7 @@ impl BoundedRetriever for GraphBfsRetriever {
.iter()
.enumerate()
.map(|(i, nv)| (i, cosine(&qn, nv)))
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.max_by(|a, b| a.1.total_cmp(&b.1))
.unwrap_or((0, 0.0));
heap.push(Entry(best_sim, best_id));
}
@ -362,7 +419,8 @@ impl BoundedRetriever for GraphBfsRetriever {
/// retrieved chunks (capped at budget).
///
/// The min-cut separates "query-coherent" chunks from noise with minimum weight,
/// giving a principled coherence boundary.
/// giving a principled coherence boundary. The partition is subsequently
/// ranked and truncated, so this is not an exact budget-constrained min-cut.
pub struct MinCutRetriever {
cfg: RetrieverConfig,
/// Scale factor for inter-chunk edge capacities.
@ -371,10 +429,18 @@ pub struct MinCutRetriever {
impl MinCutRetriever {
pub fn new(cfg: RetrieverConfig) -> Self {
Self { cfg, edge_scale: 0.5 }
validate_config(&cfg);
Self {
cfg,
edge_scale: 0.5,
}
}
pub fn with_edge_scale(mut self, scale: f32) -> Self {
assert!(
scale.is_finite() && scale >= 0.0,
"edge scale must be finite and non-negative"
);
self.edge_scale = scale;
self
}
@ -387,8 +453,13 @@ impl BoundedRetriever for MinCutRetriever {
fn retrieve(&self, corpus: &Corpus, query: &Query) -> RetrievalResult {
if corpus.is_empty() {
return RetrievalResult { chunks: vec![], scores: vec![], budget_utilisation: 0.0 };
return RetrievalResult {
chunks: vec![],
scores: vec![],
budget_utilisation: 0.0,
};
}
validate_query(corpus, query);
let n = corpus.len();
let qn = normalise(&query.vector);
@ -406,6 +477,7 @@ impl BoundedRetriever for MinCutRetriever {
for i in 0..n {
let sim = cosine(&qn, &normed[i]).max(0.001);
cap[source].insert(i, sim);
cap[i].entry(source).or_insert(0.0);
}
// Chunk → sink edges
@ -413,6 +485,7 @@ impl BoundedRetriever for MinCutRetriever {
let sim = cosine(&qn, &normed[i]);
let anti = (1.0 - sim).max(0.001);
cap[i].insert(sink, anti);
cap[sink].entry(i).or_insert(0.0);
}
// Inter-chunk edges (both directions)
@ -499,7 +572,7 @@ impl BoundedRetriever for MinCutRetriever {
.filter(|&i| in_source_set[i])
.map(|i| (i, cosine(&qn, &normed[i])))
.collect();
retrieved.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
retrieved.sort_by(|a, b| b.1.total_cmp(&a.1));
retrieved.truncate(self.cfg.budget);
let retrieved_n = retrieved.len();
@ -517,11 +590,7 @@ impl BoundedRetriever for MinCutRetriever {
// ── evaluation helpers ─────────────────────────────────────────────────────────
/// Mean average precision across a query set.
pub fn mean_precision(
retriever: &dyn BoundedRetriever,
corpus: &Corpus,
queries: &[Query],
) -> f32 {
pub fn mean_precision(retriever: &dyn BoundedRetriever, corpus: &Corpus, queries: &[Query]) -> f32 {
if queries.is_empty() {
return 0.0;
}
@ -541,14 +610,14 @@ mod tests {
fn toy_corpus() -> Corpus {
// Two clusters: cluster 0 near (1,0,0), cluster 1 near (0,0,1)
let vecs = vec![
vec![1.0_f32, 0.0, 0.0], // 0 cluster 0
vec![0.95, 0.05, 0.0], // 1 cluster 0
vec![0.90, 0.10, 0.0], // 2 cluster 0
vec![0.85, 0.15, 0.05], // 3 cluster 0
vec![0.0, 0.0, 1.0], // 4 cluster 1
vec![0.05, 0.05, 0.95], // 5 cluster 1
vec![0.0, 0.1, 0.9], // 6 cluster 1
vec![0.5, 0.5, 0.5], // 7 noise
vec![1.0_f32, 0.0, 0.0], // 0 cluster 0
vec![0.95, 0.05, 0.0], // 1 cluster 0
vec![0.90, 0.10, 0.0], // 2 cluster 0
vec![0.85, 0.15, 0.05], // 3 cluster 0
vec![0.0, 0.0, 1.0], // 4 cluster 1
vec![0.05, 0.05, 0.95], // 5 cluster 1
vec![0.0, 0.1, 0.9], // 6 cluster 1
vec![0.5, 0.5, 0.5], // 7 noise
];
let labels = vec![0, 0, 0, 0, 1, 1, 1, 2];
Corpus::from_vecs(vecs).with_labels(labels)
@ -561,7 +630,10 @@ mod tests {
#[test]
fn topk_returns_budget_chunks() {
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 3, ..Default::default() };
let cfg = RetrieverConfig {
budget: 3,
..Default::default()
};
let r = TopKRetriever::new(cfg).retrieve(&corpus, &cluster0_query());
assert_eq!(r.chunks.len(), 3);
}
@ -569,7 +641,10 @@ mod tests {
#[test]
fn topk_highest_similarity_first() {
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 4, ..Default::default() };
let cfg = RetrieverConfig {
budget: 4,
..Default::default()
};
let r = TopKRetriever::new(cfg).retrieve(&corpus, &cluster0_query());
// Chunk 0 has cosine 1.0 — must be first
assert_eq!(r.chunks[0], 0);
@ -582,7 +657,10 @@ mod tests {
#[test]
fn topk_precision_cluster0() {
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 4, ..Default::default() };
let cfg = RetrieverConfig {
budget: 4,
..Default::default()
};
let retriever = TopKRetriever::new(cfg);
let q = cluster0_query();
let r = retriever.retrieve(&corpus, &q);
@ -593,7 +671,11 @@ mod tests {
#[test]
fn graphbfs_respects_budget() {
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 3, edge_threshold: 0.8, seed_threshold: 0.7 };
let cfg = RetrieverConfig {
budget: 3,
edge_threshold: 0.8,
seed_threshold: 0.7,
};
let r = GraphBfsRetriever::new(cfg).retrieve(&corpus, &cluster0_query());
assert!(r.chunks.len() <= 3);
}
@ -601,7 +683,11 @@ mod tests {
#[test]
fn graphbfs_retrieves_cluster0() {
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 5, edge_threshold: 0.75, seed_threshold: 0.60 };
let cfg = RetrieverConfig {
budget: 5,
edge_threshold: 0.75,
seed_threshold: 0.60,
};
let q = cluster0_query();
let r = GraphBfsRetriever::new(cfg.clone()).retrieve(&corpus, &q);
let p = r.precision(&corpus, &q);
@ -611,7 +697,11 @@ mod tests {
#[test]
fn mincut_respects_budget() {
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 3, edge_threshold: 0.75, ..Default::default() };
let cfg = RetrieverConfig {
budget: 3,
edge_threshold: 0.75,
..Default::default()
};
let r = MinCutRetriever::new(cfg).retrieve(&corpus, &cluster0_query());
assert!(r.chunks.len() <= 3);
}
@ -619,7 +709,11 @@ mod tests {
#[test]
fn mincut_precision_cluster0() {
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 6, edge_threshold: 0.75, seed_threshold: 0.50 };
let cfg = RetrieverConfig {
budget: 6,
edge_threshold: 0.75,
seed_threshold: 0.50,
};
let q = cluster0_query();
let r = MinCutRetriever::new(cfg).retrieve(&corpus, &q);
let p = r.precision(&corpus, &q);
@ -631,15 +725,16 @@ mod tests {
fn mincut_no_cross_cluster_bleed() {
// Strongly separated clusters — MinCut should not bleed into cluster 1
let corpus = toy_corpus();
let cfg = RetrieverConfig { budget: 4, edge_threshold: 0.85, seed_threshold: 0.70 };
let cfg = RetrieverConfig {
budget: 4,
edge_threshold: 0.85,
seed_threshold: 0.70,
};
let q = Query::new(vec![1.0_f32, 0.0, 0.0]);
let r = MinCutRetriever::new(cfg).retrieve(&corpus, &q);
// Chunk 4,5,6 are cluster 1 — they should NOT appear with tight threshold
for &id in &r.chunks {
assert!(
id < 4 || id == 7,
"MinCut bled into cluster 1: chunk={id}"
);
assert!(id < 4 || id == 7, "MinCut bled into cluster 1: chunk={id}");
}
}
@ -707,4 +802,27 @@ mod tests {
assert!(p_bfs >= 0.70, "BFS precision={p_bfs:.3} below threshold");
assert!(p_mc >= 0.70, "MinCut precision={p_mc:.3} below threshold");
}
#[test]
#[should_panic(expected = "budget must be greater than zero")]
fn zero_budget_is_rejected() {
let _ = TopKRetriever::new(RetrieverConfig {
budget: 0,
..Default::default()
});
}
#[test]
#[should_panic(expected = "all corpus vectors must have the same dimension")]
fn inconsistent_corpus_dimensions_are_rejected() {
let _ = Corpus::from_vecs(vec![vec![1.0, 0.0], vec![1.0]]);
}
#[test]
#[should_panic(expected = "query dimension must match corpus dimension")]
fn query_dimension_mismatch_is_rejected() {
let corpus = toy_corpus();
let query = Query::new(vec![1.0, 0.0]);
let _ = TopKRetriever::new(Default::default()).retrieve(&corpus, &query);
}
}

View file

@ -17,9 +17,14 @@ Standard RAG retrieves the top-k chunks by vector cosine similarity and feeds al
Agent memory systems have a third failure: accumulated memories from many sessions may share surface-level similarity to a query while being contextually unrelated. Returning them degrades inference quality and increases hallucination risk.
The insight behind this ADR: the retrieved context should be the **maximally coherent subgraph** that fits within a token budget, not the top-k arbitrary neighbours.
The hypothesis behind this ADR is that graph coherence can improve a
budget-capped context set compared with top-k retrieval alone.
Graph min-cut provides a principled mechanism. If chunks are nodes and similarity edges connect them, a min-cut between a query-seeded source partition and a noise sink separates the coherent cluster from noise with minimum edge weight sacrificed — i.e., minimum loss of coherence.
Graph min-cut provides one measurable mechanism. If chunks are nodes and
similarity edges connect them, a min-cut between a query-seeded source
partition and a noise sink separates a candidate cluster from noise. The
implementation then ranks and truncates that partition to the budget, so the
final set is not the solution to a budget-constrained min-cut objective.
---
@ -28,8 +33,8 @@ Graph min-cut provides a principled mechanism. If chunks are nodes and similarit
Add `ruvector-bounded-rag` to the workspace as a standalone research crate implementing three retrieval strategies with a shared `BoundedRetriever` trait:
1. **TopK**: cosine rank, no graph, O(n log n) — baseline.
2. **GraphBFS**: priority-queue BFS expansion on a chunk similarity graph, bounded by coherence threshold and budget, O(V+E) — practical at medium scale.
3. **MinCutBounded**: Edmonds-Karp max-flow/min-cut on a source-sink flow network built from the chunk graph, O(VE²) — optimal coherence partition, requires pre-filtering at large scale.
2. **GraphBFS**: rebuild a dense similarity graph in O(n²·d), then run priority-queue traversal bounded by coherence threshold and budget.
3. **MinCutBounded**: Edmonds-Karp max-flow/min-cut on a source-sink flow network built from the chunk graph, followed by relevance ranking and budget truncation; requires pre-filtering at large scale.
All three share `RetrieverConfig { budget, edge_threshold, seed_threshold }`.
@ -46,7 +51,7 @@ The flow network for MinCutBounded:
### Positive
- MinCutBounded provides the tightest coherence guarantee of the three strategies.
- GraphBFS is a practical middle ground: O(V+E), no quadratic cost, good precision.
- GraphBFS has a simpler traversal than MinCut, but this PoC still pays an O(n²·d) graph-build cost on every query.
- All three use the same `BoundedRetriever` trait, making them drop-in swappable.
- The budget parameter directly controls context window consumption.
- Proof-gated integration is straightforward: add a pre-filter step using `ruvector-proof-gate` to remove chunks the requester lacks access to before running the retriever.

View file

@ -1,6 +1,6 @@
# Bounded Context RAG via MinCut Graph Partitioning
**150-char summary:** MinCut on the chunk similarity graph finds the maximally coherent context window that fits a budget — a principled replacement for fixed-k RAG retrieval.
**Summary:** A measured MinCut research baseline partitions a chunk-similarity graph, then relevance-ranks and truncates the candidate partition to a context budget.
---
@ -11,8 +11,8 @@ Standard RAG retrieval returns the top-k nearest neighbours of a query vector. T
This research implements and benchmarks three retrieval strategies for bounded-context RAG:
1. **TopK** — cosine rank, no graph reasoning, O(n log n), baseline.
2. **GraphBFS**BFS expansion on a chunk similarity graph with coherence gating, O(V+E).
3. **MinCutBounded** — max-flow / min-cut on a source-sink flow network built from chunk similarities, O(VE²), optimal coherence partition.
2. **GraphBFS**O(n²·d) dense graph construction per query followed by coherence-gated priority traversal.
3. **MinCutBounded** — max-flow/min-cut on a source-sink network followed by relevance ranking and budget truncation; this is not a budget-optimal cut.
All three are measured on synthetic clustered corpora with deterministic Rust code. No external services. No placeholder numbers.
@ -160,7 +160,9 @@ Green nodes (C1, C2) land in the source partition after min-cut — these are re
**Edmonds-Karp**: Standard BFS-augmented Ford-Fulkerson. Capacity matrix stored as `Vec<HashMap<usize, f32>>` for sparsity. Flow matrix uses the same structure. Bottleneck found by path replay.
**Budget cap**: Applied after partition recovery — the source partition may be smaller than budget (correct) or requires a hard cap (applied by sort + truncate).
**Budget cap**: Applied after partition recovery. If the source partition
exceeds the budget, relevance-based truncation can change the partition's
coherence properties.
**Fallback**: If the seed set is empty (no chunks above seed_threshold), the implementation falls back to the globally highest-similarity chunk as seed, preventing empty results.
@ -272,7 +274,7 @@ If budget=2: the min-cut partition still returns {A, B, C} but the final sort+tr
2. **Edge over-sparsity**: If edge_threshold is too high, the chunk graph is disconnected and GraphBFS returns only seeded chunks. MinCutBounded returns only the seed partition. Mitigation: tune edge_threshold on a representative validation corpus.
3. **Large coherent clusters exceeding budget**: If the coherent partition has 200 chunks but budget=20, the hard truncation is applied — the bottom 180 chunks by query similarity are dropped. This is correct behaviour but may surprise users expecting a coherence guarantee for all returned chunks.
3. **Large candidate partitions exceeding budget**: If the partition has 200 chunks but budget=20, the bottom 180 chunks by query similarity are dropped. This enforces the cap but provides no optimality or coherence guarantee for the truncated set.
4. **Query similarity ties**: When many chunks have identical cosine similarity to the query, the sort order within the partition is non-deterministic. Mitigation: break ties by chunk ID for reproducibility.
@ -290,7 +292,10 @@ If budget=2: the min-cut partition still returns {A, B, C} but the final sort+tr
## Edge and WASM Implications
**GraphBFS** can run in WASM without modification. Its memory footprint scales as O(V+E) where E is proportional to sparsity-controlled adjacency. At n=200 chunks with edge_threshold=0.80, typical E ≈ 2n5n → ~1,0001,000 edges, ≈ 840KB. Viable on Cognitum Seed (256MB RAM envelope).
**GraphBFS** can compile for WASM, but this PoC constructs and stores the
thresholded graph per query after an O(n²·d) pairwise scan. Edge-device
viability therefore needs target-specific measurement and a prebuilt sparse
graph.
**MinCutBounded** requires Edmonds-Karp with a 52-node flow network for the Phase 2 pre-filter path. This is O(52 × 1,225²) ~ 78M f32 operations, well within WASM feasibility.

View file

@ -1,6 +1,6 @@
# ruvector 2026: Bounded Context RAG via MinCut Graph Partitioning for Rust Vector Search
**SEO summary (150 chars):** MinCut on the chunk similarity graph finds the maximally coherent context window that fits a budget — a principled RAG upgrade for AI agent memory.
**Summary:** A measured MinCut baseline partitions a chunk-similarity graph, then relevance-ranks and truncates the partition to a context budget.
**Value proposition**: Replace fixed-k retrieval with a graph min-cut boundary that enforces semantic coherence while respecting a context budget — implemented in pure Rust, zero external services.
@ -18,7 +18,11 @@ The fundamental problem is that top-k retrieval models chunk-query relationships
Current workarounds are insufficient. MMR (Maximal Marginal Relevance) reduces redundancy but does not maximise coherence — a diverse-but-incoherent set is worse than a redundant-but-coherent one for agent memory applications. RAPTOR builds hierarchical summaries offline and cannot track streaming agent memory. GraphRAG requires LLM-powered entity extraction — expensive to maintain on continuously growing agent memory.
This research implements min-cut bounded retrieval: model chunk relationships as a similarity graph, attach source and sink nodes weighted by query affinity, run max-flow (Edmonds-Karp), and return the source-side partition. This is the maximally coherent chunk set that fits within a budget. It is a graph-theoretic answer to the question: "which chunks should I retrieve together?"
This research implements min-cut bounded retrieval: model chunk relationships
as a similarity graph, attach source and sink nodes weighted by query affinity,
run max-flow (Edmonds-Karp), then relevance-rank and truncate the source-side
partition. It is an auditable graph heuristic, not an exact
budget-constrained min-cut.
For RuVector — a Rust-native cognition substrate for AI agents, graph memory, and MCP tooling — this is a natural capability. `ruvector-mincut` already implements subpolynomial dynamic min-cut. `ruvector-agent-memory` provides the chunk store. `ruvector-proof-gate` provides access-controlled pre-filtering. This crate (`ruvector-bounded-rag`) connects them into a coherence-aware retrieval layer.
@ -32,8 +36,8 @@ The practical constraint today is compute: O(n²) pairwise similarity matrix dom
|---------|-------------|----------------|--------|
| `BoundedRetriever` trait | Shared API for all retrieval strategies | Swappable backends in production | Implemented in PoC |
| `TopKRetriever` | Cosine rank, no graph, O(n log n) | Fastest baseline | Implemented & measured |
| `GraphBfsRetriever` | Priority-queue BFS on similarity graph | Budget-safe, O(V+E) | Implemented & measured |
| `MinCutRetriever` | Edmonds-Karp max-flow/min-cut | Optimal coherent partition | Implemented & measured |
| `GraphBfsRetriever` | O(n²·d) graph build + priority traversal | Budget-safe heuristic | Implemented & measured |
| `MinCutRetriever` | Edmonds-Karp cut + relevance truncation | Coherent-partition heuristic | Implemented & measured |
| `RetrieverConfig` | Shared budget + threshold config | Single config controls all three | Implemented in PoC |
| Precision scoring | Recall metric using chunk labels | Honest quality measurement | Implemented & measured |
| Phase 2 k-NN graph | Pre-built approximate graph | Reduces O(n²) to O(k log n) | Research direction |
@ -218,7 +222,11 @@ Note: no direct comparison benchmarks are included. All measurements in this doc
## Deep Research Notes
The core insight is that max-flow/min-cut provides a **globally optimal** coherence boundary for a given query, while BFS-based methods only provide locally greedy boundaries. On well-separated synthetic corpora both achieve precision=1.000. The difference emerges on ambiguous corpora where clusters partially overlap — MinCut will find the lower-weight edge set to cut, while BFS may expand into the wrong cluster.
Max-flow/min-cut exactly optimises the capacities of the constructed flow
network. That does not establish that the capacities model contextual
coherence, and the subsequent budget truncation is outside that optimum. On
the well-separated synthetic corpus both graph variants achieve
precision=1.000; ambiguous real-corpus behaviour remains to be validated.
The SOTA in RAG retrieval (as of July 2026) has not published min-cut bounded retrieval. The closest adjacent work is GraphRAG (Microsoft, 2024) which uses community detection on entity graphs. Community detection and min-cut are related (min-cut is equivalent to finding the minimum-weight balanced partition) but GraphRAG operates on structured entity-relation graphs extracted by LLMs, while this work operates on raw vector similarity graphs.