1use crate::def::{evaluate, is_keymatch_rooted, NodeView, Predicate, RuleDef};
2use crate::hnsw::HnswIndex;
3use crate::index::{
4 candidate_spec, candidate_spec_approx_with_k, ivf_drift_rebuild_threshold, CandidateSpec,
5 RuleIndex,
6};
7use core_storage::v8::encode::{decode_ivf_bytes, decode_provenance_bytes};
8use core_storage::v8::seam::ColumnsView;
9use core_storage::{EdgeProps, IdMap, Interner, Topology, Value};
10
11fn decode_ivf_bytes_to_export(bytes: &[u8]) -> BTreeMap<String, RuleIvfExport> {
14 decode_ivf_bytes(bytes)
15 .into_iter()
16 .map(|(name, ps)| {
17 (
18 name,
19 (
20 (ps.src.centroids, ps.src.clusters, ps.src.drift),
21 (ps.dst.centroids, ps.dst.clusters, ps.dst.drift),
22 ),
23 )
24 })
25 .collect()
26}
27use std::collections::{BTreeMap, BTreeSet};
28use std::sync::{Mutex, OnceLock};
29
30#[derive(Debug, Clone)]
37pub struct EngineEdgeDelta {
38 pub rule: String,
39 pub src_key: String,
41 pub dst_key: String,
43 pub edge_type: String,
45 pub etype_sym: u32,
47 pub src_id: u32,
49 pub dst_id: u32,
51 pub fired: bool,
53}
54
55#[cfg(test)]
56pub use crate::index::{with_ivf_drift_rebuild, with_vector_dim_reject, with_vector_early_exit};
57
58#[cfg(test)]
61pub(crate) static PEAK_DESIRED_PAIRS: std::sync::atomic::AtomicUsize =
62 std::sync::atomic::AtomicUsize::new(0);
63
64#[cfg(test)]
65pub(crate) fn record_desired_len(n: usize) {
66 use std::sync::atomic::Ordering;
67 let mut cur = PEAK_DESIRED_PAIRS.load(Ordering::Relaxed);
68 while n > cur {
69 match PEAK_DESIRED_PAIRS.compare_exchange_weak(cur, n, Ordering::Relaxed, Ordering::Relaxed)
70 {
71 Ok(_) => break,
72 Err(actual) => cur = actual,
73 }
74 }
75}
76
77pub struct GraphMut<'a> {
79 pub ids: &'a IdMap,
80 pub syms: &'a mut Interner,
81 pub labels: &'a [u32],
82 pub props: ColumnsView<'a>,
83 pub topo: &'a mut Topology,
84 pub edge_props: &'a mut EdgeProps,
85}
86
87pub const DEFAULT_MAX_EDGES: u64 = 1_000_000;
89
90type Triple = (u32, u32, u32);
92type Touch = (u32, u32, u32, u32);
94
95pub type SideIvfExport = (Vec<Vec<f64>>, BTreeMap<u32, usize>, u64);
98pub type RuleIvfExport = (SideIvfExport, SideIvfExport);
100
101type HnswBlobMap = BTreeMap<String, (Vec<u8>, Vec<u8>)>;
103type LazyHnswMap = BTreeMap<String, (Option<HnswIndex>, Option<HnswIndex>)>;
105
106#[derive(Debug, Default)]
114struct LazyProvenance {
115 provenance: BTreeMap<String, BTreeSet<Triple>>,
116 by_node: BTreeMap<u32, BTreeSet<Touch>>,
117 intern_rule: Vec<String>,
118}
119
120#[derive(Debug, Default)]
121pub struct RuleEngine {
122 rules: BTreeMap<String, RuleDef>,
123 indexes: BTreeMap<String, RuleIndex>,
124 provenance: BTreeMap<String, BTreeSet<Triple>>,
125 owned: BTreeSet<Triple>,
126 by_node: BTreeMap<u32, BTreeSet<Touch>>,
129 rule_intern: BTreeMap<String, u32>,
134 intern_rule: Vec<String>,
135 tripped: BTreeMap<String, bool>,
136 fires: BTreeMap<String, u64>,
137 pending_deltas: Vec<EngineEdgeDelta>,
144 emit_deltas: bool,
157 rebuild_needed: BTreeSet<String>,
161 indexes_populated: bool,
167 retained_hnsw_blobs: Mutex<HnswBlobMap>,
176 retained_ivf_bytes: Mutex<Option<Vec<u8>>>,
184 retained_provenance_bytes: Mutex<Option<Vec<u8>>>,
193 lazy_provenance: OnceLock<LazyProvenance>,
201 lazy_hnsw: OnceLock<LazyHnswMap>,
208}
209
210fn candidate_spec_for(def: &RuleDef) -> CandidateSpec<'_> {
217 if def.approximate {
218 let k = def.max_edges.map(|me| me.max(64)).unwrap_or(64) as usize;
221 candidate_spec_approx_with_k(&def.predicate, k)
222 } else {
223 candidate_spec(&def.predicate)
224 }
225}
226
227fn src_lookup_spec_for(def: &RuleDef) -> CandidateSpec<'_> {
233 if is_keymatch_rooted(&def.predicate) {
234 let field =
235 keymatch_field(&def.predicate).expect("keymatch-rooted predicate has a KeyMatch field");
236 CandidateSpec::Scalar { field }
237 } else {
238 candidate_spec_for(def)
239 }
240}
241
242fn predicate_covers_field(p: &Predicate, field: &str) -> bool {
244 match p {
245 Predicate::VectorSimilar { field: f, .. } => f == field,
246 Predicate::All(parts) | Predicate::Any(parts) => {
247 parts.iter().any(|q| predicate_covers_field(q, field))
248 }
249 _ => false,
250 }
251}
252
253fn keymatch_field(p: &Predicate) -> Option<&str> {
255 match p {
256 Predicate::KeyMatch { field } => Some(field),
257 Predicate::All(parts) => parts.first().and_then(keymatch_field),
258 Predicate::Any(_) => None,
259 _ => None,
260 }
261}
262
263fn compute_desired(
266 def: &RuleDef,
267 index: &RuleIndex,
268 n: u32,
269 on_src_side: bool,
270 g: &GraphMut<'_>,
271) -> BTreeMap<(u32, u32), f64> {
272 let (my_label, other_label) = if on_src_side {
273 (&def.src_label, &def.dst_label)
274 } else {
275 (&def.dst_label, &def.src_label)
276 };
277
278 let Some(my_sym) = g.syms.get(my_label) else {
279 return BTreeMap::new();
280 };
281 if g.labels.get(n as usize).copied() != Some(my_sym) {
282 return BTreeMap::new();
283 }
284 let other_sym = g.syms.get(other_label);
285
286 let n_key = match g.ids.key_of(n) {
287 Some(k) => k,
288 None => return BTreeMap::new(),
289 };
290 let n_get = |f: &str| g.props.get(n, f).map(|vr| vr.into_value());
291
292 let spec = candidate_spec_for(def);
293 let candidates: BTreeSet<u32> = if on_src_side {
294 if is_keymatch_rooted(&def.predicate) {
295 let field = keymatch_field(&def.predicate).expect("ByKey always comes from KeyMatch");
299 match n_get(field) {
300 Some(Value::Str(ref target_key)) => match g.ids.get(target_key) {
301 Some(dst_id) => std::iter::once(dst_id).collect(),
302 None => BTreeSet::new(),
303 },
304 _ => BTreeSet::new(),
305 }
306 } else {
307 index.dst_side.candidates(&spec, &n_get)
308 }
309 } else {
310 let src_spec = src_lookup_spec_for(def);
312 if is_keymatch_rooted(&def.predicate) {
313 let key_getter = |_: &str| Some(Value::Str(n_key.to_string()));
316 index.src_side.candidates(&src_spec, &key_getter)
317 } else {
318 index.src_side.candidates(&src_spec, &n_get)
319 }
320 };
321
322 let n_early_exit_hint: Option<(Vec<f64>, f64, [f64; 8])> = if !def.approximate {
337 if let Predicate::VectorSimilar { field, .. } = &def.predicate {
338 if crate::index::vector_early_exit_enabled() {
339 let n_side = if on_src_side {
340 &index.src_side
341 } else {
342 &index.dst_side
343 };
344 if let Some(vn_v) = n_get(field) {
345 if let Some(vn) = crate::index::as_numeric_list(&vn_v) {
346 if let Some((norm_n, ckpts_n)) = n_side.fresh_ckpts_for(n, &vn) {
347 Some((vn, norm_n, *ckpts_n))
348 } else {
349 None
350 }
351 } else {
352 None
353 }
354 } else {
355 None
356 }
357 } else {
358 None
359 }
360 } else {
361 None
362 }
363 } else {
364 None
365 };
366
367 let mut out = BTreeMap::new();
368 for m in candidates {
369 if m == n {
370 continue; }
372 if g.labels.get(m as usize).copied() != other_sym {
373 continue; }
375 let m_key = match g.ids.key_of(m) {
376 Some(k) => k,
377 None => continue,
378 };
379 let m_get = |f: &str| g.props.get(m, f).map(|vr| vr.into_value());
380 let (s_view, d_view, s_id, d_id) = if on_src_side {
381 (
382 NodeView {
383 key: n_key,
384 props: &n_get,
385 },
386 NodeView {
387 key: m_key,
388 props: &m_get,
389 },
390 n,
391 m,
392 )
393 } else {
394 (
395 NodeView {
396 key: m_key,
397 props: &m_get,
398 },
399 NodeView {
400 key: n_key,
401 props: &n_get,
402 },
403 m,
404 n,
405 )
406 };
407
408 if let (Some((ref vn, norm_n, ckpts_n)), Predicate::VectorSimilar { field, min }) =
410 (&n_early_exit_hint, &def.predicate)
411 {
412 let m_side = if on_src_side {
413 &index.dst_side
414 } else {
415 &index.src_side
416 };
417 if let Some(vm_v) = m_get(field) {
418 if let Some(vm) = crate::index::as_numeric_list(&vm_v) {
419 if let Some((norm_m, ckpts_m)) = m_side.fresh_ckpts_for(m, &vm) {
420 let (va, ckpts_a, na, vb, ckpts_b, nb) = if on_src_side {
421 (
422 vn.as_slice(),
423 ckpts_n,
424 *norm_n,
425 vm.as_slice(),
426 ckpts_m,
427 norm_m,
428 )
429 } else {
430 (
431 vm.as_slice(),
432 ckpts_m,
433 norm_m,
434 vn.as_slice(),
435 ckpts_n,
436 *norm_n,
437 )
438 };
439 match crate::def::cosine_early_exit(va, vb, ckpts_a, ckpts_b, na, nb, *min)
440 {
441 None => continue, Some(score) => {
443 out.insert((s_id, d_id), score);
444 continue; }
446 }
447 }
448 }
449 }
450 }
451
452 if let Some(score) = evaluate(&def.predicate, &s_view, &d_view) {
453 out.insert((s_id, d_id), score);
454 }
455 }
456 #[cfg(test)]
457 record_desired_len(out.len());
458 out
459}
460
461fn compute_desired_via(
474 def: &RuleDef,
475 anchor: ViaAnchor,
476 g: &GraphMut<'_>,
477) -> BTreeMap<(u32, u32), f64> {
478 let via_label = def.via_label.as_deref().unwrap();
479 let via_edge_str = def.via_edge.as_deref().unwrap();
480 let via_dir = def.via_dir.unwrap_or(core_storage::Direction::Out);
481
482 let src_sym = match g.syms.get(&def.src_label) {
483 Some(s) => s,
484 None => return BTreeMap::new(),
485 };
486 let via_sym = match g.syms.get(via_label) {
487 Some(s) => s,
488 None => return BTreeMap::new(),
489 };
490 let dst_sym = match g.syms.get(&def.dst_label) {
491 Some(s) => s,
492 None => return BTreeMap::new(),
493 };
494 let via_etype = match g.syms.get(via_edge_str) {
495 Some(e) => e,
496 None => return BTreeMap::new(),
497 };
498
499 let srcs: Vec<u32> = match anchor {
501 ViaAnchor::Src(src_id) => {
502 if g.labels.get(src_id as usize).copied() == Some(src_sym) {
503 vec![src_id]
504 } else {
505 return BTreeMap::new();
506 }
507 }
508 ViaAnchor::Dst(_) => {
509 (0..g.ids.len() as u32)
511 .filter(|&id| {
512 matches!(
513 g.labels.get(id as usize).copied(),
514 Some(s) if s != u32::MAX && s == src_sym
515 )
516 })
517 .collect()
518 }
519 };
520
521 let anchored_dst: Option<u32> = match anchor {
523 ViaAnchor::Dst(dst_id) => {
524 if g.labels.get(dst_id as usize).copied() == Some(dst_sym) {
525 Some(dst_id)
526 } else {
527 return BTreeMap::new();
528 }
529 }
530 _ => None,
531 };
532
533 let mut out = BTreeMap::new();
534
535 for src in srcs {
536 let _src_key = match g.ids.key_of(src) {
537 Some(k) => k,
538 None => continue,
539 };
540 let via_neighbors: Vec<u32> = g
542 .topo
543 .neighbors(via_etype, via_dir, src)
544 .iter()
545 .copied()
546 .filter(|&v| g.labels.get(v as usize).copied() == Some(via_sym))
547 .collect();
548
549 if via_neighbors.is_empty() {
550 continue;
551 }
552
553 let dsts: Vec<u32> = if let Some(dst_id) = anchored_dst {
555 vec![dst_id]
556 } else {
557 (0..g.ids.len() as u32)
558 .filter(|&id| {
559 id != src
560 && matches!(
561 g.labels.get(id as usize).copied(),
562 Some(s) if s != u32::MAX && s == dst_sym
563 )
564 })
565 .collect()
566 };
567
568 for dst in dsts {
569 if dst == src {
570 continue; }
572 let dst_key = match g.ids.key_of(dst) {
573 Some(k) => k,
574 None => continue,
575 };
576 let dst_get = |f: &str| g.props.get(dst, f).map(|vr| vr.into_value());
577 let dst_view = NodeView {
578 key: dst_key,
579 props: &dst_get,
580 };
581
582 let mut best: Option<f64> = None;
584 for &via_id in &via_neighbors {
585 let via_key = match g.ids.key_of(via_id) {
586 Some(k) => k,
587 None => continue,
588 };
589 let via_get = |f: &str| g.props.get(via_id, f).map(|vr| vr.into_value());
590 let via_view = NodeView {
591 key: via_key,
592 props: &via_get,
593 };
594 if let Some(score) = evaluate(&def.predicate, &via_view, &dst_view) {
595 best = Some(match best {
596 None => score,
597 Some(prev) => prev.max(score),
598 });
599 }
600 }
601
602 if let Some(score) = best {
603 out.insert((src, dst), score);
604 }
605 }
606 }
607
608 out
609}
610
611enum ViaAnchor {
613 Src(u32),
615 Dst(u32),
618}
619
620fn edge_budget(def: &RuleDef) -> u64 {
621 def.max_edges.unwrap_or(DEFAULT_MAX_EDGES)
624}
625
626pub(crate) fn filter_src_top_k(
648 per_src: BTreeMap<(u32, u32), f64>,
649 k: u64,
650 ids: &core_storage::IdMap,
651) -> BTreeMap<(u32, u32), f64> {
652 if per_src.len() as u64 <= k {
653 return per_src;
654 }
655 let mut candidates: Vec<((u32, u32), f64)> = per_src.into_iter().collect();
656 candidates.sort_by(|&((_, da), sa), &((_, db), sb)| {
658 sb.total_cmp(&sa).then_with(|| {
659 let ka = ids.key_of(da).unwrap_or("");
660 let kb = ids.key_of(db).unwrap_or("");
661 ka.cmp(kb)
662 })
663 });
664 candidates.truncate(k as usize);
665 candidates.into_iter().collect()
666}
667
668fn apply_per_src_top_k(
675 def: &RuleDef,
676 src: u32,
677 desired_from_src: BTreeMap<(u32, u32), f64>,
678 prov: &mut ProvSets<'_>,
679 g: &mut GraphMut<'_>,
680) {
681 let et = g.syms.intern(&def.edge_type);
682
683 let current: Vec<Triple> = {
687 let rid = prov.rule_intern.get(&def.name).copied();
688 prov.by_node
689 .get(&src)
690 .into_iter()
691 .flatten()
692 .filter(|(r, t, s, _d)| Some(*r) == rid && *t == et && *s == src)
693 .map(|(_, t, s, d)| (*t, *s, *d))
694 .collect()
695 };
696
697 for (t, s, d) in current {
699 if !desired_from_src.contains_key(&(s, d)) {
700 g.topo.remove_edge(t, s, d);
701 g.edge_props.remove_edge(t, s, d);
702 prov.remove(&def.name, (t, s, d), g.ids, g.syms);
703 }
704 }
705
706 for ((s, d), score) in &desired_from_src {
708 let triple = (et, *s, *d);
709 let already = prov.contains(&triple);
710 if !already {
711 let newly = g.topo.add_edge(et, *s, *d);
712 if newly {
713 prov.insert(&def.name, triple, g.ids, g.syms);
714 }
715 }
716 let is_owned = already || prov.contains(&triple);
717 if is_owned {
718 if let Some(p) = &def.weight_prop {
719 g.edge_props.set(et, *s, *d, p, Value::Float(*score));
720 }
721 }
722 }
723}
724
725fn intern_rule(intern: &mut BTreeMap<String, u32>, names: &mut Vec<String>, rule: &str) -> u32 {
727 if let Some(&id) = intern.get(rule) {
728 return id;
729 }
730 let id = names.len() as u32;
731 intern.insert(rule.to_string(), id);
732 names.push(rule.to_string());
733 id
734}
735
736type ByNodeRebuild = (
737 BTreeMap<u32, BTreeSet<Touch>>,
738 BTreeMap<String, u32>,
739 Vec<String>,
740);
741
742fn rebuild_by_node(provenance: &BTreeMap<String, BTreeSet<Triple>>) -> ByNodeRebuild {
743 let mut by_node = BTreeMap::new();
744 let mut intern = BTreeMap::new();
745 let mut names = Vec::new();
746 for (rule, set) in provenance {
747 let rid = intern_rule(&mut intern, &mut names, rule);
748 for &triple in set {
749 touch_insert(&mut by_node, rid, triple);
750 }
751 }
752 (by_node, intern, names)
753}
754
755fn touch_insert(by_node: &mut BTreeMap<u32, BTreeSet<Touch>>, rid: u32, triple: Triple) {
756 let (t, s, d) = triple;
757 let entry = (rid, t, s, d);
758 by_node.entry(s).or_default().insert(entry);
759 if s != d {
760 by_node.entry(d).or_default().insert(entry);
761 }
762}
763
764fn touch_remove(by_node: &mut BTreeMap<u32, BTreeSet<Touch>>, rid: u32, triple: Triple) {
765 let (t, s, d) = triple;
766 let entry = (rid, t, s, d);
767 if let Some(set) = by_node.get_mut(&s) {
768 set.remove(&entry);
769 if set.is_empty() {
770 by_node.remove(&s);
771 }
772 }
773 if s != d {
774 if let Some(set) = by_node.get_mut(&d) {
775 set.remove(&entry);
776 if set.is_empty() {
777 by_node.remove(&d);
778 }
779 }
780 }
781}
782
783#[cfg(test)]
784fn resolve_by_node(
785 by_node: &BTreeMap<u32, BTreeSet<Touch>>,
786 names: &[String],
787) -> BTreeMap<u32, BTreeSet<(String, Triple)>> {
788 by_node
789 .iter()
790 .map(|(&n, set)| {
791 let resolved = set
792 .iter()
793 .map(|&(rid, t, s, d)| (names[rid as usize].clone(), (t, s, d)))
794 .collect();
795 (n, resolved)
796 })
797 .collect()
798}
799
800struct ProvSets<'a> {
803 set: &'a mut BTreeSet<Triple>,
804 owned: &'a mut BTreeSet<Triple>,
805 by_node: &'a mut BTreeMap<u32, BTreeSet<Touch>>,
806 rule_intern: &'a mut BTreeMap<String, u32>,
807 intern_rule: &'a mut Vec<String>,
808 deltas: &'a mut Vec<EngineEdgeDelta>,
812 emit: bool,
815}
816
817impl ProvSets<'_> {
818 fn insert(&mut self, rule: &str, triple: Triple, ids: &IdMap, syms: &Interner) -> bool {
822 if !self.set.insert(triple) {
823 return false;
824 }
825 self.owned.insert(triple);
826 let rid = intern_rule(self.rule_intern, self.intern_rule, rule);
827 touch_insert(self.by_node, rid, triple);
828 let (etype, src, dst) = triple;
829 if self.emit {
830 if let (Some(sk), Some(dk), Some(et)) =
831 (ids.key_of(src), ids.key_of(dst), syms.resolve(etype))
832 {
833 self.deltas.push(EngineEdgeDelta {
834 rule: rule.to_string(),
835 src_key: sk.to_string(),
836 dst_key: dk.to_string(),
837 edge_type: et.to_string(),
838 etype_sym: etype,
839 src_id: src,
840 dst_id: dst,
841 fired: true,
842 });
843 }
844 }
845 true
846 }
847
848 fn remove(&mut self, rule: &str, triple: Triple, ids: &IdMap, syms: &Interner) -> bool {
849 if !self.set.remove(&triple) {
850 return false;
851 }
852 self.owned.remove(&triple);
853 let rid = intern_rule(self.rule_intern, self.intern_rule, rule);
854 touch_remove(self.by_node, rid, triple);
855 let (etype, src, dst) = triple;
856 if self.emit {
857 if let (Some(sk), Some(dk), Some(et)) =
858 (ids.key_of(src), ids.key_of(dst), syms.resolve(etype))
859 {
860 self.deltas.push(EngineEdgeDelta {
861 rule: rule.to_string(),
862 src_key: sk.to_string(),
863 dst_key: dk.to_string(),
864 edge_type: et.to_string(),
865 etype_sym: etype,
866 src_id: src,
867 dst_id: dst,
868 fired: false,
869 });
870 }
871 }
872 true
873 }
874
875 fn contains(&self, triple: &Triple) -> bool {
876 self.set.contains(triple)
877 }
878
879 fn len(&self) -> usize {
880 self.set.len()
881 }
882}
883
884fn apply_desired(
895 def: &RuleDef,
896 desired: BTreeMap<(u32, u32), f64>,
897 retract_touching: Option<u32>,
898 prov: &mut ProvSets<'_>,
899 tripped: &mut bool,
900 g: &mut GraphMut<'_>,
901) {
902 let budget = edge_budget(def);
903 let et = g.syms.intern(&def.edge_type);
904
905 let current: Vec<Triple> = match retract_touching {
906 None => prov
907 .set
908 .iter()
909 .filter(|(t, _, _)| *t == et)
910 .copied()
911 .collect(),
912 Some(n) => {
913 let rid = prov.rule_intern.get(&def.name).copied();
914 prov.by_node
915 .get(&n)
916 .into_iter()
917 .flatten()
918 .filter(|(r, t, _, _)| Some(*r) == rid && *t == et)
919 .map(|(_, t, s, d)| (*t, *s, *d))
920 .collect()
921 }
922 };
923
924 for (t, s, d) in current {
925 if !desired.contains_key(&(s, d)) {
926 g.topo.remove_edge(t, s, d);
927 g.edge_props.remove_edge(t, s, d);
928 prov.remove(&def.name, (t, s, d), g.ids, g.syms);
929 }
930 }
931
932 for ((s, d), score) in desired {
933 let triple = (et, s, d);
934 let already = prov.contains(&triple);
935 if !already {
936 if *tripped || prov.len() as u64 >= budget {
937 *tripped = true;
938 continue;
939 }
940 let newly = g.topo.add_edge(et, s, d);
941 if newly {
942 prov.insert(&def.name, triple, g.ids, g.syms);
943 }
944 }
945 let is_owned_here = already || prov.contains(&triple);
949 if is_owned_here {
950 if let Some(p) = &def.weight_prop {
951 g.edge_props.set(et, s, d, p, Value::Float(score));
952 }
953 }
954 }
955}
956
957#[cfg(test)]
965#[allow(dead_code)]
966fn compute_full_desired(
967 def: &RuleDef,
968 index: &RuleIndex,
969 g: &GraphMut<'_>,
970) -> BTreeMap<(u32, u32), f64> {
971 let mut desired = BTreeMap::new();
972 let src_sym = g.syms.get(&def.src_label);
973 for id in 0..g.ids.len() as u32 {
974 let label_sym = match g.labels.get(id as usize).copied() {
975 Some(s) if s != u32::MAX => s,
976 _ => continue,
977 };
978 if src_sym == Some(label_sym) {
979 desired.extend(compute_desired(def, index, id, true, g));
980 #[cfg(test)]
981 record_desired_len(desired.len());
982 }
983 }
984 desired
985}
986
987fn pair_still_desired(def: &RuleDef, s: u32, d: u32, g: &GraphMut<'_>) -> bool {
994 let src_sym = match g.syms.get(&def.src_label) {
995 Some(sym) => sym,
996 None => return false,
997 };
998 let dst_sym = match g.syms.get(&def.dst_label) {
999 Some(sym) => sym,
1000 None => return false,
1001 };
1002 if g.labels.get(s as usize).copied() != Some(src_sym) {
1003 return false;
1004 }
1005 if g.labels.get(d as usize).copied() != Some(dst_sym) {
1006 return false;
1007 }
1008 let s_key = match g.ids.key_of(s) {
1009 Some(k) => k,
1010 None => return false,
1011 };
1012 let d_key = match g.ids.key_of(d) {
1013 Some(k) => k,
1014 None => return false,
1015 };
1016 let s_get = |f: &str| g.props.get(s, f).map(|vr| vr.into_value());
1017 let d_get = |f: &str| g.props.get(d, f).map(|vr| vr.into_value());
1018 evaluate(
1019 &def.predicate,
1020 &NodeView {
1021 key: s_key,
1022 props: &s_get,
1023 },
1024 &NodeView {
1025 key: d_key,
1026 props: &d_get,
1027 },
1028 )
1029 .is_some()
1030}
1031
1032fn count_desired_up_to(def: &RuleDef, index: &RuleIndex, limit: u64, g: &GraphMut<'_>) -> u64 {
1037 let mut count = 0u64;
1038 let src_sym = g.syms.get(&def.src_label);
1039 for id in 0..g.ids.len() as u32 {
1040 let label_sym = match g.labels.get(id as usize).copied() {
1041 Some(s) if s != u32::MAX => s,
1042 _ => continue,
1043 };
1044 if src_sym != Some(label_sym) {
1045 continue;
1046 }
1047 count += compute_desired(def, index, id, true, g).len() as u64;
1048 if count > limit {
1049 return count;
1050 }
1051 }
1052 count
1053}
1054
1055fn apply_streaming_create(
1077 def: &RuleDef,
1078 index: &RuleIndex,
1079 prov: &mut ProvSets<'_>,
1080 tripped: &mut bool,
1081 g: &mut GraphMut<'_>,
1082) {
1083 let budget = edge_budget(def);
1084 let et = g.syms.intern(&def.edge_type);
1085 let src_sym = g.syms.get(&def.src_label);
1086
1087 'outer: for id in 0..g.ids.len() as u32 {
1088 let label_sym = match g.labels.get(id as usize).copied() {
1089 Some(s) if s != u32::MAX => s,
1090 _ => continue,
1091 };
1092 if src_sym != Some(label_sym) {
1093 continue;
1094 }
1095 let per_src = compute_desired(def, index, id, true, g);
1096 for ((s, d), score) in per_src {
1097 let triple = (et, s, d);
1098 let already = prov.contains(&triple);
1103 if !already {
1104 if *tripped || prov.len() as u64 >= budget {
1105 *tripped = true;
1106 break 'outer;
1107 }
1108 let newly = g.topo.add_edge(et, s, d);
1109 if newly {
1110 prov.insert(&def.name, triple, g.ids, g.syms);
1111 }
1112 }
1113 let is_owned_here = already || prov.contains(&triple);
1114 if is_owned_here {
1115 if let Some(p) = &def.weight_prop {
1116 g.edge_props.set(et, s, d, p, Value::Float(score));
1117 }
1118 }
1119 }
1120 }
1121}
1122
1123fn apply_streaming_create_top_k(
1130 def: &RuleDef,
1131 k: u64,
1132 index: &RuleIndex,
1133 prov: &mut ProvSets<'_>,
1134 g: &mut GraphMut<'_>,
1135) {
1136 let src_sym = g.syms.get(&def.src_label);
1137 for id in 0..g.ids.len() as u32 {
1138 let label_sym = match g.labels.get(id as usize).copied() {
1139 Some(s) if s != u32::MAX => s,
1140 _ => continue,
1141 };
1142 if src_sym != Some(label_sym) {
1143 continue;
1144 }
1145 let per_src = compute_desired(def, index, id, true, g);
1146 let top_k = filter_src_top_k(per_src, k, g.ids);
1147 apply_per_src_top_k(def, id, top_k, prov, g);
1148 }
1149}
1150
1151fn apply_streaming_rebuild_top_k(
1158 def: &RuleDef,
1159 k: u64,
1160 index: &RuleIndex,
1161 prov: &mut ProvSets<'_>,
1162 g: &mut GraphMut<'_>,
1163) {
1164 let et = g.syms.intern(&def.edge_type);
1165
1166 let existing_srcs: BTreeSet<u32> = prov
1169 .set
1170 .iter()
1171 .filter(|(t, _, _)| *t == et)
1172 .map(|(_, s, _)| *s)
1173 .collect();
1174
1175 let src_sym = g.syms.get(&def.src_label);
1176 let mut all_srcs: BTreeSet<u32> = existing_srcs;
1177 for id in 0..g.ids.len() as u32 {
1178 let label_sym = match g.labels.get(id as usize).copied() {
1179 Some(s) if s != u32::MAX => s,
1180 _ => continue,
1181 };
1182 if src_sym == Some(label_sym) {
1183 all_srcs.insert(id);
1184 }
1185 }
1186
1187 for src in all_srcs {
1188 let desired_src = compute_desired(def, index, src, true, g);
1189 let top_k = filter_src_top_k(desired_src, k, g.ids);
1190 apply_per_src_top_k(def, src, top_k, prov, g);
1191 }
1192}
1193
1194fn apply_streaming_rebuild(
1207 def: &RuleDef,
1208 index: &RuleIndex,
1209 prov: &mut ProvSets<'_>,
1210 tripped: &mut bool,
1211 g: &mut GraphMut<'_>,
1212) {
1213 let budget = edge_budget(def);
1214 let et = g.syms.intern(&def.edge_type);
1215
1216 let total = count_desired_up_to(def, index, budget, g);
1218 if total > budget {
1219 *tripped = true;
1220 return; }
1222
1223 *tripped = false;
1225
1226 let current: Vec<Triple> = prov
1229 .set
1230 .iter()
1231 .filter(|(t, _, _)| *t == et)
1232 .copied()
1233 .collect();
1234 for (t, s, d) in current {
1235 if !pair_still_desired(def, s, d, g) {
1236 g.topo.remove_edge(t, s, d);
1237 g.edge_props.remove_edge(t, s, d);
1238 prov.remove(&def.name, (t, s, d), g.ids, g.syms);
1239 }
1240 }
1241
1242 let src_sym = g.syms.get(&def.src_label);
1245 for id in 0..g.ids.len() as u32 {
1246 let label_sym = match g.labels.get(id as usize).copied() {
1247 Some(s) if s != u32::MAX => s,
1248 _ => continue,
1249 };
1250 if src_sym != Some(label_sym) {
1251 continue;
1252 }
1253 let per_src = compute_desired(def, index, id, true, g);
1254 for ((s, d), score) in per_src {
1255 let triple = (et, s, d);
1256 let already = prov.contains(&triple);
1257 if !already {
1258 let newly = g.topo.add_edge(et, s, d);
1259 if newly {
1260 prov.insert(&def.name, triple, g.ids, g.syms);
1261 }
1262 }
1263 let is_owned_here = already || prov.contains(&triple);
1264 if is_owned_here {
1265 if let Some(p) = &def.weight_prop {
1266 g.edge_props.set(et, s, d, p, Value::Float(score));
1267 }
1268 }
1269 }
1270 }
1271}
1272
1273fn bump_fires_for_participants(def: &RuleDef, g: &GraphMut<'_>, fires: &mut u64) {
1276 let src_sym = g.syms.get(&def.src_label);
1277 let dst_sym = g.syms.get(&def.dst_label);
1278 for id in 0..g.ids.len() as u32 {
1279 let label_sym = match g.labels.get(id as usize).copied() {
1280 Some(s) if s != u32::MAX => s,
1281 _ => continue,
1282 };
1283 if src_sym == Some(label_sym) || dst_sym == Some(label_sym) {
1284 *fires += 1;
1285 }
1286 }
1287}
1288
1289fn index_node_for_rule(
1291 id: u32,
1292 label_sym: u32,
1293 def: &RuleDef,
1294 index: &mut RuleIndex,
1295 syms: &Interner,
1296 props: ColumnsView<'_>,
1297) {
1298 let get = |f: &str| props.get(id, f).map(|vr| vr.into_value());
1299 if syms.get(&def.src_label) == Some(label_sym) {
1300 let spec = src_lookup_spec_for(def);
1301 index.src_side.insert(&spec, id, &get);
1302 }
1303 if syms.get(&def.dst_label) == Some(label_sym) {
1304 let spec = candidate_spec_for(def);
1305 index.dst_side.insert(&spec, id, &get);
1306 }
1307}
1308
1309impl RuleEngine {
1314 pub fn new() -> Self {
1315 Self::default()
1316 }
1317
1318 pub fn rules(&self) -> impl Iterator<Item = &RuleDef> {
1319 self.rules.values()
1320 }
1321
1322 pub fn is_owned(&self, etype: u32, src: u32, dst: u32) -> bool {
1323 self.owned.contains(&(etype, src, dst))
1324 }
1325
1326 fn provenance_is_retained(&self) -> bool {
1331 self.retained_provenance_bytes
1332 .lock()
1333 .expect("lock poisoned")
1334 .is_some()
1335 }
1336
1337 pub fn provenance(&self) -> &BTreeMap<String, BTreeSet<(u32, u32, u32)>> {
1342 if self.provenance_is_retained() {
1343 self.ensure_provenance_loaded();
1344 &self.lazy_provenance.get().unwrap().provenance
1345 } else {
1346 &self.provenance
1347 }
1348 }
1349
1350 pub fn provenance_touching(
1355 &self,
1356 node: u32,
1357 ) -> impl Iterator<Item = (&str, u32, u32, u32)> + '_ {
1358 let use_lazy = self.provenance_is_retained();
1359 let (by_node, intern_rule): (&BTreeMap<u32, BTreeSet<Touch>>, &Vec<String>) = if use_lazy {
1360 self.ensure_provenance_loaded();
1361 let lp = self.lazy_provenance.get().unwrap();
1362 (&lp.by_node, &lp.intern_rule)
1363 } else {
1364 (&self.by_node, &self.intern_rule)
1365 };
1366 by_node
1367 .get(&node)
1368 .into_iter()
1369 .flatten()
1370 .map(move |&(rid, t, s, d)| (intern_rule[rid as usize].as_str(), t, s, d))
1371 }
1372
1373 pub fn provenance_touching_len(&self, node: u32) -> usize {
1375 if self.provenance_is_retained() {
1376 self.ensure_provenance_loaded();
1377 self.lazy_provenance
1378 .get()
1379 .unwrap()
1380 .by_node
1381 .get(&node)
1382 .map_or(0, BTreeSet::len)
1383 } else {
1384 self.by_node.get(&node).map_or(0, BTreeSet::len)
1385 }
1386 }
1387
1388 pub fn is_tripped(&self, name: &str) -> bool {
1391 self.tripped.get(name).copied().unwrap_or(false)
1392 }
1393
1394 pub fn fire_count(&self, name: &str) -> u64 {
1398 self.fires.get(name).copied().unwrap_or(0)
1399 }
1400
1401 pub fn drain_deltas(&mut self) -> Vec<EngineEdgeDelta> {
1415 std::mem::take(&mut self.pending_deltas)
1416 }
1417
1418 pub fn pending_delta_count(&self) -> usize {
1421 self.pending_deltas.len()
1422 }
1423
1424 pub fn pending_deltas_since(&self, cursor: usize) -> &[EngineEdgeDelta] {
1433 &self.pending_deltas[cursor..]
1434 }
1435
1436 #[allow(clippy::type_complexity)]
1440 pub fn to_persist(
1441 &self,
1442 ) -> (
1443 Vec<RuleDef>,
1444 BTreeMap<String, BTreeSet<(u32, u32, u32)>>,
1445 BTreeMap<String, bool>,
1446 BTreeMap<String, u64>,
1447 ) {
1448 (
1449 self.rules.values().cloned().collect(),
1450 self.provenance.clone(),
1451 self.tripped.clone(),
1452 self.fires.clone(),
1453 )
1454 }
1455
1456 pub fn from_persist(
1458 rules: Vec<RuleDef>,
1459 prov: BTreeMap<String, BTreeSet<(u32, u32, u32)>>,
1460 tripped: BTreeMap<String, bool>,
1461 fires: BTreeMap<String, u64>,
1462 ) -> Self {
1463 let mut owned = BTreeSet::new();
1464 for set in prov.values() {
1465 owned.extend(set.iter().copied());
1466 }
1467 let indexes = rules
1468 .iter()
1469 .map(|r| (r.name.clone(), RuleIndex::default()))
1470 .collect();
1471 let rules: BTreeMap<String, RuleDef> =
1472 rules.into_iter().map(|r| (r.name.clone(), r)).collect();
1473 let mut tripped = tripped;
1475 let mut fires = fires;
1476 for name in rules.keys() {
1477 tripped.entry(name.clone()).or_insert(false);
1478 fires.entry(name.clone()).or_insert(0);
1479 }
1480 let (by_node, rule_intern, intern_rule) = rebuild_by_node(&prov);
1481 Self {
1482 rules,
1483 indexes,
1484 provenance: prov,
1485 owned,
1486 by_node,
1487 rule_intern,
1488 intern_rule,
1489 tripped,
1490 fires,
1491 pending_deltas: Vec::new(),
1492 emit_deltas: false,
1493 rebuild_needed: BTreeSet::new(),
1494 indexes_populated: false,
1498 retained_hnsw_blobs: Mutex::new(BTreeMap::new()),
1499 retained_ivf_bytes: Mutex::new(None),
1500 retained_provenance_bytes: Mutex::new(None),
1501 lazy_provenance: OnceLock::new(),
1502 lazy_hnsw: OnceLock::new(),
1503 }
1504 }
1505
1506 pub fn set_emit_deltas(&mut self, emit: bool) {
1512 self.emit_deltas = emit;
1513 }
1514
1515 pub fn emit_deltas(&self) -> bool {
1517 self.emit_deltas
1518 }
1519
1520 pub fn take_rebuild_needed(&mut self) -> Vec<String> {
1523 std::mem::take(&mut self.rebuild_needed)
1524 .into_iter()
1525 .collect()
1526 }
1527
1528 pub fn queue_rebuild_needed(&mut self, name: String) {
1532 self.rebuild_needed.insert(name);
1533 }
1534
1535 fn maybe_queue_ivf_rebuild(&mut self, rule_name: &str, def: &RuleDef) {
1536 if !def.approximate {
1537 return;
1538 }
1539 let Some(idx) = self.indexes.get(rule_name) else {
1540 return;
1541 };
1542 if idx.dst_side.ivf_drift > ivf_drift_rebuild_threshold() {
1543 self.rebuild_needed.insert(rule_name.to_string());
1544 }
1545 }
1546
1547 pub fn export_ivf_state(&self) -> BTreeMap<String, RuleIvfExport> {
1551 let mut out = BTreeMap::new();
1552 for (name, def) in &self.rules {
1553 if def.approximate {
1554 if let Some(idx) = self.indexes.get(name) {
1555 out.insert(
1556 name.clone(),
1557 (
1558 idx.src_side.export_ivf_state(),
1559 idx.dst_side.export_ivf_state(),
1560 ),
1561 );
1562 }
1563 }
1564 }
1565 out
1566 }
1567
1568 pub fn reindex_all(
1570 &mut self,
1571 ids: &IdMap,
1572 syms: &Interner,
1573 labels: &[u32],
1574 props: ColumnsView<'_>,
1575 ) {
1576 for idx in self.indexes.values_mut() {
1577 *idx = RuleIndex::default();
1578 }
1579 let rule_names: Vec<String> = self.rules.keys().cloned().collect();
1582
1583 for name in &rule_names {
1585 if self.rules[name].approximate {
1586 let idx = self.indexes.get_mut(name).unwrap();
1587 idx.src_side.init_hnsw(name);
1588 idx.dst_side.init_hnsw(name);
1589 }
1590 }
1591
1592 for id in 0..ids.len() as u32 {
1593 let label_sym = match labels.get(id as usize).copied() {
1594 Some(s) if s != u32::MAX => s,
1595 _ => continue,
1596 };
1597 for name in &rule_names {
1598 let def = self.rules[name].clone();
1599 let idx = self.indexes.get_mut(name).unwrap();
1600 index_node_for_rule(id, label_sym, &def, idx, syms, props);
1601 }
1602 }
1603 for name in &rule_names {
1606 if self.rules[name].approximate {
1607 let idx = self.indexes.get_mut(name).unwrap();
1608 idx.src_side.fit_ivf_clusters(name);
1609 idx.dst_side.fit_ivf_clusters(name);
1610 }
1611 }
1612 self.indexes_populated = true;
1613 }
1614
1615 pub fn reindex_all_load_ivf(
1625 &mut self,
1626 ids: &IdMap,
1627 syms: &Interner,
1628 labels: &[u32],
1629 props: ColumnsView<'_>,
1630 ivf_state: BTreeMap<String, RuleIvfExport>,
1631 ) {
1632 for idx in self.indexes.values_mut() {
1633 *idx = RuleIndex::default();
1634 }
1635 let rule_names: Vec<String> = self.rules.keys().cloned().collect();
1636
1637 for name in &rule_names {
1640 if self.rules[name].approximate {
1641 let idx = self.indexes.get_mut(name).unwrap();
1642 idx.src_side.init_hnsw(name);
1643 idx.dst_side.init_hnsw(name);
1644 }
1645 }
1646
1647 for id in 0..ids.len() as u32 {
1648 let label_sym = match labels.get(id as usize).copied() {
1649 Some(s) if s != u32::MAX => s,
1650 _ => continue,
1651 };
1652 for name in &rule_names {
1653 let def = self.rules[name].clone();
1654 let idx = self.indexes.get_mut(name).unwrap();
1655 index_node_for_rule(id, label_sym, &def, idx, syms, props);
1656 }
1657 }
1658 for name in &rule_names {
1662 if !self.rules[name].approximate {
1663 continue;
1664 }
1665 let idx = self.indexes.get_mut(name).unwrap();
1666 if let Some(((sc, sa, sd), (dc, da, dd))) = ivf_state.get(name) {
1667 idx.src_side.load_ivf_state(sc.clone(), sa.clone(), *sd);
1668 idx.dst_side.load_ivf_state(dc.clone(), da.clone(), *dd);
1669 } else {
1670 idx.src_side.fit_ivf_clusters(name);
1672 idx.dst_side.fit_ivf_clusters(name);
1673 }
1674 }
1675 self.indexes_populated = true;
1676 }
1677
1678 pub fn store_snapshot_state(
1686 &self,
1687 hnsw_blobs: BTreeMap<String, (Vec<u8>, Vec<u8>)>,
1688 ivf_bytes: Vec<u8>,
1689 ) {
1690 *self
1691 .retained_hnsw_blobs
1692 .lock()
1693 .expect("retained_hnsw_blobs lock poisoned") = hnsw_blobs;
1694 *self
1695 .retained_ivf_bytes
1696 .lock()
1697 .expect("retained_ivf_bytes lock poisoned") = if ivf_bytes.is_empty() {
1698 None
1699 } else {
1700 Some(ivf_bytes)
1701 };
1702 }
1704
1705 pub fn store_provenance_bytes(&self, bytes: Vec<u8>) {
1712 *self
1713 .retained_provenance_bytes
1714 .lock()
1715 .expect("lock poisoned") = if bytes.is_empty() { None } else { Some(bytes) };
1716 }
1717
1718 pub fn ensure_provenance_loaded(&self) {
1725 self.lazy_provenance.get_or_init(|| {
1726 let guard = self
1729 .retained_provenance_bytes
1730 .lock()
1731 .expect("retained_provenance_bytes lock poisoned");
1732 let bytes = match &*guard {
1733 Some(b) if !b.is_empty() => b,
1734 _ => return LazyProvenance::default(),
1735 };
1736 let prov = decode_provenance_bytes(bytes);
1737 let (by_node, _rule_intern, intern_rule) = rebuild_by_node(&prov);
1738 LazyProvenance {
1739 provenance: prov,
1740 by_node,
1741 intern_rule,
1742 }
1743 });
1744 }
1745
1746 pub fn ensure_provenance_loaded_mut(&mut self) {
1752 let bytes = match self
1753 .retained_provenance_bytes
1754 .lock()
1755 .expect("lock poisoned")
1756 .take()
1757 {
1758 Some(b) => b,
1759 None => return,
1760 };
1761 let prov = decode_provenance_bytes(&bytes);
1762 for set in prov.values() {
1763 self.owned.extend(set.iter().copied());
1764 }
1765 let (by_node, rule_intern, intern_rule) = rebuild_by_node(&prov);
1766 self.provenance = prov;
1767 self.by_node = by_node;
1768 self.rule_intern = rule_intern;
1769 self.intern_rule = intern_rule;
1770 }
1771
1772 pub fn consume_retained_state_eager(
1778 &mut self,
1779 ids: &IdMap,
1780 syms: &Interner,
1781 labels: &[u32],
1782 props: ColumnsView<'_>,
1783 ) {
1784 if self.indexes_populated {
1785 return;
1786 }
1787 self.ensure_provenance_loaded_mut();
1790 let hnsw = std::mem::take(
1791 &mut *self
1792 .retained_hnsw_blobs
1793 .lock()
1794 .expect("retained_hnsw_blobs lock poisoned"),
1795 );
1796 let ivf_bytes = self
1797 .retained_ivf_bytes
1798 .lock()
1799 .expect("retained_ivf_bytes lock poisoned")
1800 .take()
1801 .unwrap_or_default();
1802 let ivf = decode_ivf_bytes_to_export(&ivf_bytes);
1803 self.reindex_all_load_ivf(ids, syms, labels, props, ivf);
1804 self.load_hnsw_state(hnsw);
1806 }
1807
1808 pub fn ensure_hnsw_loaded(&self) {
1819 self.lazy_hnsw.get_or_init(|| {
1820 let snapshot: Vec<(String, Vec<u8>, Vec<u8>)> = {
1824 let guard = self
1825 .retained_hnsw_blobs
1826 .lock()
1827 .expect("retained_hnsw_blobs lock poisoned");
1828 if guard.is_empty() {
1829 return BTreeMap::new();
1830 }
1831 guard
1832 .iter()
1833 .map(|(name, (sb, db))| (name.clone(), sb.clone(), db.clone()))
1834 .collect()
1835 }; snapshot
1837 .into_iter()
1838 .map(|(name, sb, db)| {
1839 let src = if !sb.is_empty() {
1840 bincode::deserialize::<HnswIndex>(&sb).ok()
1841 } else {
1842 None
1843 };
1844 let dst = if !db.is_empty() {
1845 bincode::deserialize::<HnswIndex>(&db).ok()
1846 } else {
1847 None
1848 };
1849 (name, (src, dst))
1850 })
1851 .collect()
1852 });
1853 }
1854
1855 pub fn indexes_populated(&self) -> bool {
1858 self.indexes_populated
1859 }
1860
1861 pub fn export_hnsw_state(&self) -> BTreeMap<String, (Vec<u8>, Vec<u8>)> {
1866 let mut out = BTreeMap::new();
1867 for (name, def) in &self.rules {
1868 if def.approximate {
1869 if let Some(idx) = self.indexes.get(name) {
1870 out.insert(
1871 name.clone(),
1872 (
1873 idx.src_side.export_hnsw_blob(),
1874 idx.dst_side.export_hnsw_blob(),
1875 ),
1876 );
1877 }
1878 }
1879 }
1880 out
1881 }
1882
1883 pub fn export_hnsw_state_passthrough(&self) -> BTreeMap<String, (Vec<u8>, Vec<u8>)> {
1887 if !self.indexes_populated {
1888 let guard = self
1889 .retained_hnsw_blobs
1890 .lock()
1891 .expect("retained_hnsw_blobs lock poisoned");
1892 if !guard.is_empty() {
1893 return guard.clone();
1894 }
1895 }
1896 self.export_hnsw_state()
1897 }
1898
1899 pub fn retained_ivf_bytes_clone(&self) -> Option<Vec<u8>> {
1905 self.retained_ivf_bytes
1906 .lock()
1907 .expect("retained_ivf_bytes lock poisoned")
1908 .clone()
1909 }
1910
1911 pub fn load_hnsw_state(&mut self, blobs: BTreeMap<String, (Vec<u8>, Vec<u8>)>) {
1916 for (name, (src_blob, dst_blob)) in blobs {
1917 if let Some(idx) = self.indexes.get_mut(&name) {
1918 if !src_blob.is_empty() {
1919 idx.src_side.load_hnsw_blob(&src_blob);
1920 }
1921 if !dst_blob.is_empty() {
1922 idx.dst_side.load_hnsw_blob(&dst_blob);
1923 }
1924 }
1925 }
1926 }
1927
1928 pub fn hnsw_search_dst(
1933 &self,
1934 field: &str,
1935 dst_label: &str,
1936 q: &[f64],
1937 k: usize,
1938 ) -> Option<Vec<(u32, f64)>> {
1939 for (name, def) in &self.rules {
1940 if !def.approximate || def.dst_label != dst_label {
1941 continue;
1942 }
1943 if !predicate_covers_field(&def.predicate, field) {
1945 continue;
1946 }
1947 if let Some(idx) = self.indexes.get(name) {
1948 if let Some(h) = idx.dst_side.hnsw_ref() {
1949 if !h.is_empty() {
1950 return Some(h.search(q, k));
1951 }
1952 }
1953 }
1954 if let Some(lazy) = self.lazy_hnsw.get() {
1957 if let Some((_, Some(h))) = lazy.get(name) {
1958 if !h.is_empty() {
1959 return Some(h.search(q, k));
1960 }
1961 }
1962 }
1963 }
1964 None
1965 }
1966
1967 pub fn hnsw_has_rule(&self, field: &str) -> bool {
1974 self.rules
1975 .values()
1976 .any(|def| def.approximate && predicate_covers_field(&def.predicate, field))
1977 }
1978
1979 pub fn hnsw_search_any_dst(&self, field: &str, q: &[f64], k: usize) -> Option<Vec<(u32, f64)>> {
1989 let mut merged: std::collections::BTreeMap<u32, f64> = std::collections::BTreeMap::new();
1990 let mut found_index = false;
1991
1992 for (name, def) in &self.rules {
1993 if !def.approximate {
1994 continue;
1995 }
1996 if !predicate_covers_field(&def.predicate, field) {
1997 continue;
1998 }
1999 let hits: Option<Vec<(u32, f64)>> = if let Some(idx) = self.indexes.get(name) {
2000 if let Some(h) = idx.dst_side.hnsw_ref() {
2001 if !h.is_empty() {
2002 found_index = true;
2003 Some(h.search(q, k))
2004 } else {
2005 None
2006 }
2007 } else {
2008 None
2009 }
2010 } else if let Some(lazy) = self.lazy_hnsw.get() {
2011 if let Some((_, Some(h))) = lazy.get(name) {
2012 if !h.is_empty() {
2013 found_index = true;
2014 Some(h.search(q, k))
2015 } else {
2016 None
2017 }
2018 } else {
2019 None
2020 }
2021 } else {
2022 None
2023 };
2024
2025 if let Some(hits) = hits {
2026 for (id, score) in hits {
2027 merged
2028 .entry(id)
2029 .and_modify(|s| {
2030 if score > *s {
2031 *s = score;
2032 }
2033 })
2034 .or_insert(score);
2035 }
2036 }
2037 }
2038
2039 if !found_index {
2040 return None;
2041 }
2042 let mut result: Vec<(u32, f64)> = merged.into_iter().collect();
2043 result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
2044 result.truncate(k);
2045 Some(result)
2046 }
2047
2048 pub fn create_rule(&mut self, def: RuleDef, g: &mut GraphMut<'_>) -> Result<(), String> {
2051 def.validate()?;
2052 if self.rules.contains_key(&def.name) {
2053 return Err(format!("rule {:?} already exists", def.name));
2054 }
2055 let name = def.name.clone();
2056 self.rules.insert(name.clone(), def);
2057 self.indexes.insert(name.clone(), RuleIndex::default());
2058 self.provenance.entry(name.clone()).or_default();
2059 self.tripped.insert(name.clone(), false);
2060 self.fires.insert(name.clone(), 0);
2061
2062 let n_total = g.ids.len() as u32;
2064 let def = self.rules[&name].clone();
2065
2066 if def.approximate {
2069 let idx = self.indexes.get_mut(&name).unwrap();
2070 idx.src_side.init_hnsw(&name);
2071 idx.dst_side.init_hnsw(&name);
2072 }
2073
2074 for id in 0..n_total {
2075 let label_sym = match g.labels.get(id as usize).copied() {
2076 Some(s) if s != u32::MAX => s,
2077 _ => continue,
2078 };
2079 let idx = self.indexes.get_mut(&name).unwrap();
2080 index_node_for_rule(id, label_sym, &def, idx, g.syms, g.props);
2081 }
2082
2083 if def.approximate {
2086 let idx = self.indexes.get_mut(&name).unwrap();
2087 idx.src_side.fit_ivf_clusters(&name);
2088 idx.dst_side.fit_ivf_clusters(&name);
2089 }
2090
2091 let mut prov = ProvSets {
2096 set: self.provenance.get_mut(&name).unwrap(),
2097 owned: &mut self.owned,
2098 by_node: &mut self.by_node,
2099 rule_intern: &mut self.rule_intern,
2100 intern_rule: &mut self.intern_rule,
2101 deltas: &mut self.pending_deltas,
2102 emit: self.emit_deltas,
2103 };
2104 if def.via_label.is_some() {
2105 let budget = edge_budget(&def);
2107 let et = g.syms.intern(&def.edge_type);
2108 let src_sym = g.syms.get(&def.src_label);
2109 let tripped = self.tripped.get_mut(&name).unwrap();
2110 'via_outer: for id in 0..g.ids.len() as u32 {
2111 let label_sym = match g.labels.get(id as usize).copied() {
2112 Some(s) if s != u32::MAX => s,
2113 _ => continue,
2114 };
2115 if src_sym != Some(label_sym) {
2116 continue;
2117 }
2118 let per_src = compute_desired_via(&def, ViaAnchor::Src(id), g);
2119 if let Some(k) = def.max_edges {
2120 let top_k = filter_src_top_k(per_src, k, g.ids);
2121 apply_per_src_top_k(&def, id, top_k, &mut prov, g);
2122 } else {
2123 for ((s, d), score) in per_src {
2124 let triple = (et, s, d);
2125 let already = prov.contains(&triple);
2126 if !already {
2127 if *tripped || prov.len() as u64 >= budget {
2128 *tripped = true;
2129 break 'via_outer;
2130 }
2131 let newly = g.topo.add_edge(et, s, d);
2132 if newly {
2133 prov.insert(&name, triple, g.ids, g.syms);
2134 }
2135 }
2136 let is_owned_here = already || prov.contains(&triple);
2137 if is_owned_here {
2138 if let Some(p) = &def.weight_prop {
2139 g.edge_props.set(et, s, d, p, Value::Float(score));
2140 }
2141 }
2142 }
2143 }
2144 }
2145 } else if let Some(k) = def.max_edges {
2146 apply_streaming_create_top_k(&def, k, &self.indexes[&name], &mut prov, g);
2147 } else {
2148 let tripped = self.tripped.get_mut(&name).unwrap();
2149 apply_streaming_create(&def, &self.indexes[&name], &mut prov, tripped, g);
2150 }
2151 let fires = self.fires.get_mut(&name).unwrap();
2154 bump_fires_for_participants(&def, g, fires);
2155
2156 self.indexes_populated = true;
2160
2161 Ok(())
2162 }
2163
2164 pub fn delete_rule(&mut self, name: &str, g: &mut GraphMut<'_>) -> Result<(), String> {
2166 if !self.rules.contains_key(name) {
2167 return Err(format!("rule {:?} not found", name));
2168 }
2169 let def = self.rules.remove(name).unwrap();
2170 self.indexes.remove(name);
2171 self.tripped.remove(name);
2172 self.fires.remove(name);
2173 let mut leftover = self.provenance.remove(name).unwrap_or_default();
2174 let _et = g.syms.intern(&def.edge_type);
2176 let triples: Vec<Triple> = leftover.iter().copied().collect();
2177 let mut sets = ProvSets {
2178 set: &mut leftover,
2179 owned: &mut self.owned,
2180 by_node: &mut self.by_node,
2181 rule_intern: &mut self.rule_intern,
2182 intern_rule: &mut self.intern_rule,
2183 deltas: &mut self.pending_deltas,
2184 emit: self.emit_deltas,
2185 };
2186 for triple in triples {
2187 let (t, s, d) = triple;
2188 g.topo.remove_edge(t, s, d);
2189 g.edge_props.remove_edge(t, s, d);
2190 sets.remove(name, triple, g.ids, g.syms);
2191 }
2192 let same_etype_survivors: Vec<String> = self
2198 .rules
2199 .values()
2200 .filter(|r| r.edge_type == def.edge_type)
2201 .map(|r| r.name.clone())
2202 .collect();
2203 for survivor in same_etype_survivors {
2204 let _ = self.rebuild(&survivor, g);
2206 }
2207 Ok(())
2208 }
2209
2210 pub fn on_node_changed(
2220 &mut self,
2221 n: u32,
2222 changed: Option<(&str, Option<Value>)>,
2223 g: &mut GraphMut<'_>,
2224 ) {
2225 self.ensure_provenance_loaded_mut();
2227 if !self.indexes_populated && !self.rules.is_empty() {
2233 let hnsw = std::mem::take(
2234 &mut *self
2235 .retained_hnsw_blobs
2236 .lock()
2237 .expect("retained_hnsw_blobs lock poisoned"),
2238 );
2239 let ivf_bytes = self
2240 .retained_ivf_bytes
2241 .lock()
2242 .expect("retained_ivf_bytes lock poisoned")
2243 .take()
2244 .unwrap_or_default();
2245 let ivf = decode_ivf_bytes_to_export(&ivf_bytes);
2246 self.reindex_all_load_ivf(g.ids, g.syms, g.labels, g.props, ivf);
2247 self.load_hnsw_state(hnsw);
2248 }
2249
2250 let n_label = g.labels.get(n as usize).copied();
2251 let rule_names: Vec<String> = self.rules.keys().cloned().collect();
2252
2253 for rule_name in rule_names {
2254 let def = self.rules[&rule_name].clone();
2255
2256 if def.via_label.is_some() {
2257 self.on_node_changed_via(&rule_name, &def, n, n_label, changed.clone(), g);
2259 } else {
2260 let src_sym = g.syms.get(&def.src_label);
2262 let dst_sym = g.syms.get(&def.dst_label);
2263 let as_src = src_sym.is_some() && n_label == src_sym;
2264 let as_dst = dst_sym.is_some() && n_label == dst_sym;
2265
2266 let fires = match changed {
2267 None => as_src || as_dst,
2268 Some((field, _)) => def.watched_fields().contains(field) && (as_src || as_dst),
2269 };
2270 if !fires {
2271 continue;
2272 }
2273 *self.fires.entry(rule_name.clone()).or_default() += 1;
2274
2275 if let Some((field, ref old_val)) = changed {
2277 let old_val_cloned = old_val.clone();
2278 let old_getter = |f: &str| {
2279 if f == field {
2280 old_val_cloned.clone()
2281 } else {
2282 g.props.get(n, f).map(|vr| vr.into_value())
2283 }
2284 };
2285 let idx = self.indexes.get_mut(&rule_name).unwrap();
2286 if as_src {
2287 let spec = src_lookup_spec_for(&def);
2288 idx.src_side.remove(&spec, n, &old_getter);
2289 }
2290 if as_dst {
2291 let spec = candidate_spec_for(&def);
2292 idx.dst_side.remove(&spec, n, &old_getter);
2293 }
2294 }
2295
2296 {
2297 let cur_getter = |f: &str| g.props.get(n, f).map(|vr| vr.into_value());
2298 let idx = self.indexes.get_mut(&rule_name).unwrap();
2299 if as_src {
2300 let spec = src_lookup_spec_for(&def);
2301 idx.src_side.insert(&spec, n, &cur_getter);
2302 }
2303 if as_dst {
2304 let spec = candidate_spec_for(&def);
2305 idx.dst_side.insert(&spec, n, &cur_getter);
2306 }
2307 }
2308
2309 self.maybe_queue_ivf_rebuild(&rule_name, &def);
2310
2311 if let Some(k) = def.max_edges {
2313 let et = g.syms.intern(&def.edge_type);
2314 let affected_srcs_for_n_dst: BTreeSet<u32> = if as_dst {
2315 let rid = self.rule_intern.get(&def.name).copied();
2316 self.by_node
2317 .get(&n)
2318 .into_iter()
2319 .flatten()
2320 .filter(|(r, t, _s, d)| Some(*r) == rid && *t == et && *d == n)
2321 .map(|(_, _, s, _)| *s)
2322 .collect()
2323 } else {
2324 BTreeSet::new()
2325 };
2326
2327 let mut prov = ProvSets {
2328 set: self.provenance.entry(rule_name.clone()).or_default(),
2329 owned: &mut self.owned,
2330 by_node: &mut self.by_node,
2331 rule_intern: &mut self.rule_intern,
2332 intern_rule: &mut self.intern_rule,
2333 deltas: &mut self.pending_deltas,
2334 emit: self.emit_deltas,
2335 };
2336
2337 if as_src {
2338 let desired_n_src =
2339 compute_desired(&def, &self.indexes[&rule_name], n, true, g);
2340 let top_k = filter_src_top_k(desired_n_src, k, g.ids);
2341 apply_per_src_top_k(&def, n, top_k, &mut prov, g);
2342 }
2343
2344 if as_dst {
2345 let new_desired =
2346 compute_desired(&def, &self.indexes[&rule_name], n, false, g);
2347 let new_srcs: BTreeSet<u32> = new_desired.keys().map(|(s, _)| *s).collect();
2348 let affected_srcs: BTreeSet<u32> =
2349 affected_srcs_for_n_dst.union(&new_srcs).copied().collect();
2350 for src in affected_srcs {
2351 if src == n {
2352 continue;
2353 }
2354 let desired_src =
2355 compute_desired(&def, &self.indexes[&rule_name], src, true, g);
2356 let top_k = filter_src_top_k(desired_src, k, g.ids);
2357 apply_per_src_top_k(&def, src, top_k, &mut prov, g);
2358 }
2359 }
2360 } else {
2361 let mut desired = BTreeMap::new();
2362 if as_src {
2363 desired.extend(compute_desired(
2364 &def,
2365 &self.indexes[&rule_name],
2366 n,
2367 true,
2368 g,
2369 ));
2370 }
2371 if as_dst {
2372 desired.extend(compute_desired(
2373 &def,
2374 &self.indexes[&rule_name],
2375 n,
2376 false,
2377 g,
2378 ));
2379 }
2380 let tripped = self.tripped.entry(rule_name.clone()).or_default();
2381 apply_desired(
2382 &def,
2383 desired,
2384 Some(n),
2385 &mut ProvSets {
2386 set: self.provenance.entry(rule_name).or_default(),
2387 owned: &mut self.owned,
2388 by_node: &mut self.by_node,
2389 rule_intern: &mut self.rule_intern,
2390 intern_rule: &mut self.intern_rule,
2391 deltas: &mut self.pending_deltas,
2392 emit: self.emit_deltas,
2393 },
2394 tripped,
2395 g,
2396 );
2397 }
2398 }
2399 }
2400 }
2401
2402 fn on_node_changed_via(
2417 &mut self,
2418 rule_name: &str,
2419 def: &RuleDef,
2420 n: u32,
2421 n_label: Option<u32>,
2422 changed: Option<(&str, Option<Value>)>,
2423 g: &mut GraphMut<'_>,
2424 ) {
2425 let src_sym = g.syms.get(&def.src_label);
2426 let dst_sym = g.syms.get(&def.dst_label);
2427 let via_sym = def.via_label.as_deref().and_then(|l| g.syms.get(l));
2428
2429 let as_src = src_sym.is_some() && n_label == src_sym;
2430 let as_dst = dst_sym.is_some() && n_label == dst_sym;
2431 let as_via = via_sym.is_some() && n_label == via_sym;
2432
2433 let fires = match changed {
2437 None => as_src || as_via || as_dst,
2438 Some((field, _)) => {
2439 let wf = def.watched_fields();
2440 (wf.contains(field)) && (as_src || as_via || as_dst)
2441 }
2442 };
2443 if !fires {
2444 return;
2445 }
2446 *self.fires.entry(rule_name.to_string()).or_default() += 1;
2447
2448 let mut affected_srcs: BTreeSet<u32> = BTreeSet::new();
2450 if as_src {
2451 affected_srcs.insert(n);
2452 }
2453 if as_via {
2454 let via_edge_str = def.via_edge.as_deref().unwrap();
2456 let via_dir = def.via_dir.unwrap_or(core_storage::Direction::Out);
2457 let rev_dir = match via_dir {
2458 core_storage::Direction::Out => core_storage::Direction::In,
2459 core_storage::Direction::In => core_storage::Direction::Out,
2460 };
2461 if let (Some(via_etype), Some(s_sym)) = (g.syms.get(via_edge_str), src_sym) {
2462 for &src in g.topo.neighbors(via_etype, rev_dir, n).as_ref() {
2463 if g.labels.get(src as usize).copied() == Some(s_sym) {
2464 affected_srcs.insert(src);
2465 }
2466 }
2467 }
2468 }
2469 if as_dst {
2470 let desired_touching_n = compute_desired_via(def, ViaAnchor::Dst(n), g);
2472 for (src, _dst) in desired_touching_n.keys() {
2473 affected_srcs.insert(*src);
2474 }
2475 let et = g.syms.intern(&def.edge_type);
2477 let rid = self.rule_intern.get(rule_name).copied();
2478 let old_srcs: Vec<u32> = self
2479 .by_node
2480 .get(&n)
2481 .into_iter()
2482 .flatten()
2483 .filter(|(r, t, _s, d)| Some(*r) == rid && *t == et && *d == n)
2484 .map(|(_, _, s, _)| *s)
2485 .collect();
2486 affected_srcs.extend(old_srcs);
2487 }
2488
2489 let affected_srcs: Vec<u32> = affected_srcs.into_iter().collect();
2491
2492 if let Some(k) = def.max_edges {
2493 let mut prov = ProvSets {
2494 set: self.provenance.entry(rule_name.to_string()).or_default(),
2495 owned: &mut self.owned,
2496 by_node: &mut self.by_node,
2497 rule_intern: &mut self.rule_intern,
2498 intern_rule: &mut self.intern_rule,
2499 deltas: &mut self.pending_deltas,
2500 emit: self.emit_deltas,
2501 };
2502 for src in affected_srcs {
2503 let desired_src = compute_desired_via(def, ViaAnchor::Src(src), g);
2504 let top_k = filter_src_top_k(desired_src, k, g.ids);
2505 apply_per_src_top_k(def, src, top_k, &mut prov, g);
2506 }
2507 } else {
2508 let tripped = self.tripped.entry(rule_name.to_string()).or_default();
2509 let budget = edge_budget(def);
2510 for src in affected_srcs {
2513 let desired_src = compute_desired_via(def, ViaAnchor::Src(src), g);
2514 if !*tripped {
2515 let mut prov = ProvSets {
2516 set: self.provenance.entry(rule_name.to_string()).or_default(),
2517 owned: &mut self.owned,
2518 by_node: &mut self.by_node,
2519 rule_intern: &mut self.rule_intern,
2520 intern_rule: &mut self.intern_rule,
2521 deltas: &mut self.pending_deltas,
2522 emit: self.emit_deltas,
2523 };
2524 apply_desired(def, desired_src, Some(src), &mut prov, tripped, g);
2525 }
2526 let _ = budget;
2530 }
2531 }
2532 }
2533
2534 pub fn on_edge_changed(
2547 &mut self,
2548 etype_str: &str,
2549 src_id: u32,
2550 dst_id: u32,
2551 g: &mut GraphMut<'_>,
2552 ) {
2553 self.ensure_provenance_loaded_mut();
2555 if !self.indexes_populated && !self.rules.is_empty() {
2558 let hnsw = std::mem::take(
2559 &mut *self
2560 .retained_hnsw_blobs
2561 .lock()
2562 .expect("retained_hnsw_blobs lock poisoned"),
2563 );
2564 let ivf_bytes = self
2565 .retained_ivf_bytes
2566 .lock()
2567 .expect("retained_ivf_bytes lock poisoned")
2568 .take()
2569 .unwrap_or_default();
2570 let ivf = decode_ivf_bytes_to_export(&ivf_bytes);
2571 self.reindex_all_load_ivf(g.ids, g.syms, g.labels, g.props, ivf);
2572 self.load_hnsw_state(hnsw);
2573 }
2574
2575 let rule_names: Vec<String> = self.rules.keys().cloned().collect();
2576 for rule_name in rule_names {
2577 let def = self.rules[&rule_name].clone();
2578 let Some(ref via_edge) = def.via_edge else {
2579 continue; };
2581 if via_edge != etype_str {
2582 continue; }
2584
2585 let src_sym = match g.syms.get(&def.src_label) {
2587 Some(s) => s,
2588 None => continue,
2589 };
2590 let via_sym = match def.via_label.as_deref().and_then(|l| g.syms.get(l)) {
2591 Some(s) => s,
2592 None => continue,
2593 };
2594 let via_dir = def.via_dir.unwrap_or(core_storage::Direction::Out);
2598 let (rule_src, rule_via) = match via_dir {
2599 core_storage::Direction::Out => (src_id, dst_id),
2600 core_storage::Direction::In => (dst_id, src_id),
2601 };
2602
2603 if g.labels.get(rule_src as usize).copied() != Some(src_sym) {
2604 continue;
2605 }
2606 if g.labels.get(rule_via as usize).copied() != Some(via_sym) {
2607 continue;
2608 }
2609
2610 *self.fires.entry(rule_name.clone()).or_default() += 1;
2612 let desired_src = compute_desired_via(&def, ViaAnchor::Src(rule_src), g);
2613
2614 if let Some(k) = def.max_edges {
2615 let mut prov = ProvSets {
2616 set: self.provenance.entry(rule_name).or_default(),
2617 owned: &mut self.owned,
2618 by_node: &mut self.by_node,
2619 rule_intern: &mut self.rule_intern,
2620 intern_rule: &mut self.intern_rule,
2621 deltas: &mut self.pending_deltas,
2622 emit: self.emit_deltas,
2623 };
2624 let top_k = filter_src_top_k(desired_src, k, g.ids);
2625 apply_per_src_top_k(&def, rule_src, top_k, &mut prov, g);
2626 } else {
2627 let tripped = self.tripped.entry(rule_name.clone()).or_default();
2628 let mut prov = ProvSets {
2629 set: self.provenance.entry(rule_name).or_default(),
2630 owned: &mut self.owned,
2631 by_node: &mut self.by_node,
2632 rule_intern: &mut self.rule_intern,
2633 intern_rule: &mut self.intern_rule,
2634 deltas: &mut self.pending_deltas,
2635 emit: self.emit_deltas,
2636 };
2637 apply_desired(&def, desired_src, Some(rule_src), &mut prov, tripped, g);
2638 }
2639 }
2640 }
2641
2642 pub fn on_node_removed(&mut self, n: u32, g: &mut GraphMut<'_>) {
2650 self.ensure_provenance_loaded_mut();
2652 if !self.indexes_populated && !self.rules.is_empty() {
2657 let hnsw = std::mem::take(
2658 &mut *self
2659 .retained_hnsw_blobs
2660 .lock()
2661 .expect("retained_hnsw_blobs lock poisoned"),
2662 );
2663 let ivf_bytes = self
2664 .retained_ivf_bytes
2665 .lock()
2666 .expect("retained_ivf_bytes lock poisoned")
2667 .take()
2668 .unwrap_or_default();
2669 let ivf = decode_ivf_bytes_to_export(&ivf_bytes);
2670 self.reindex_all_load_ivf(g.ids, g.syms, g.labels, g.props, ivf);
2671 self.load_hnsw_state(hnsw);
2672 }
2673
2674 let n_label = g.labels.get(n as usize).copied();
2675 let rule_names: Vec<String> = self.rules.keys().cloned().collect();
2676
2677 for rule_name in rule_names {
2678 let def = self.rules[&rule_name].clone();
2679 let src_sym = g.syms.get(&def.src_label);
2680 let dst_sym = g.syms.get(&def.dst_label);
2681 let as_src = src_sym.is_some() && n_label == src_sym;
2682 let as_dst = dst_sym.is_some() && n_label == dst_sym;
2683
2684 {
2685 let cur_getter = |f: &str| g.props.get(n, f).map(|vr| vr.into_value());
2686 let idx = self.indexes.get_mut(&rule_name).unwrap();
2687 if as_src {
2688 let spec = src_lookup_spec_for(&def);
2689 idx.src_side.remove(&spec, n, &cur_getter);
2690 }
2691 if as_dst {
2692 let spec = candidate_spec_for(&def);
2693 idx.dst_side.remove(&spec, n, &cur_getter);
2694 }
2695 }
2696
2697 self.maybe_queue_ivf_rebuild(&rule_name, &def);
2698 }
2699
2700 let touching: Vec<(String, Triple)> = self
2701 .by_node
2702 .get(&n)
2703 .into_iter()
2704 .flatten()
2705 .map(|&(rid, t, s, d)| (self.intern_rule[rid as usize].clone(), (t, s, d)))
2706 .collect();
2707
2708 let topk_backfill: Vec<(String, u32)> = touching
2712 .iter()
2713 .filter_map(|(rule_name, triple)| {
2714 let &(_, s, d) = triple;
2715 let def = self.rules.get(rule_name)?;
2716 def.max_edges?; if d == n && s != n {
2718 Some((rule_name.clone(), s))
2719 } else {
2720 None
2721 }
2722 })
2723 .collect();
2724
2725 for (rule_name, triple) in touching {
2726 let (t, s, d) = triple;
2727 g.topo.remove_edge(t, s, d);
2728 g.edge_props.remove_edge(t, s, d);
2729 if let Some(set) = self.provenance.get_mut(&rule_name) {
2730 ProvSets {
2731 set,
2732 owned: &mut self.owned,
2733 by_node: &mut self.by_node,
2734 rule_intern: &mut self.rule_intern,
2735 intern_rule: &mut self.intern_rule,
2736 deltas: &mut self.pending_deltas,
2737 emit: self.emit_deltas,
2738 }
2739 .remove(&rule_name, triple, g.ids, g.syms);
2740 }
2741 }
2742
2743 for (rule_name, src) in topk_backfill {
2748 let def = self.rules[&rule_name].clone();
2749 let k = def.max_edges.unwrap(); let desired_src = compute_desired(&def, &self.indexes[&rule_name], src, true, g);
2751 let top_k = filter_src_top_k(desired_src, k, g.ids);
2752 let mut prov = ProvSets {
2753 set: self.provenance.entry(rule_name.clone()).or_default(),
2754 owned: &mut self.owned,
2755 by_node: &mut self.by_node,
2756 rule_intern: &mut self.rule_intern,
2757 intern_rule: &mut self.intern_rule,
2758 deltas: &mut self.pending_deltas,
2759 emit: self.emit_deltas,
2760 };
2761 apply_per_src_top_k(&def, src, top_k, &mut prov, g);
2762 }
2763 }
2764
2765 pub fn rebuild(&mut self, name: &str, g: &mut GraphMut<'_>) -> Result<(), String> {
2773 if !self.rules.contains_key(name) {
2774 return Err(format!("rule {:?} not found", name));
2775 }
2776 self.rebuild_needed.remove(name);
2777 let def = self.rules[name].clone();
2778
2779 *self.indexes.get_mut(name).unwrap() = RuleIndex::default();
2781
2782 if def.approximate {
2784 let idx = self.indexes.get_mut(name).unwrap();
2785 idx.src_side.init_hnsw(name);
2786 idx.dst_side.init_hnsw(name);
2787 }
2788
2789 let n_total = g.ids.len() as u32;
2790 for id in 0..n_total {
2791 let label_sym = match g.labels.get(id as usize).copied() {
2792 Some(s) if s != u32::MAX => s,
2793 _ => continue,
2794 };
2795 let idx = self.indexes.get_mut(name).unwrap();
2796 index_node_for_rule(id, label_sym, &def, idx, g.syms, g.props);
2797 }
2798
2799 if def.approximate {
2802 let idx = self.indexes.get_mut(name).unwrap();
2803 idx.src_side.fit_ivf_clusters(name);
2804 idx.dst_side.fit_ivf_clusters(name);
2805 }
2806
2807 let mut prov = ProvSets {
2811 set: self.provenance.get_mut(name).unwrap(),
2812 owned: &mut self.owned,
2813 by_node: &mut self.by_node,
2814 rule_intern: &mut self.rule_intern,
2815 intern_rule: &mut self.intern_rule,
2816 deltas: &mut self.pending_deltas,
2817 emit: self.emit_deltas,
2818 };
2819 if let Some(k) = def.max_edges {
2820 apply_streaming_rebuild_top_k(&def, k, &self.indexes[name], &mut prov, g);
2821 } else {
2822 let tripped = self.tripped.get_mut(name).unwrap();
2823 apply_streaming_rebuild(&def, &self.indexes[name], &mut prov, tripped, g);
2824 }
2825 let fires = self.fires.entry(name.to_string()).or_default();
2826 bump_fires_for_participants(&def, g, fires);
2827
2828 Ok(())
2829 }
2830
2831 #[cfg(test)]
2832 fn by_node_consistent(&self) -> bool {
2833 let (rebuilt, intern, names) = rebuild_by_node(&self.provenance);
2834 resolve_by_node(&self.by_node, &self.intern_rule) == resolve_by_node(&rebuilt, &names)
2835 && intern.len() == names.len()
2836 }
2837}
2838
2839#[cfg(test)]
2844mod tests {
2845 use super::*;
2846 use crate::def::{evaluate, NodeView, Predicate, RuleDef};
2847 use core_storage::{ColumnStore, Direction, EdgeProps, IdMap, Interner, Topology, Value};
2848
2849 struct Fx {
2850 ids: IdMap,
2851 syms: Interner,
2852 labels: Vec<u32>,
2853 props: ColumnStore,
2854 topo: Topology,
2855 eprops: EdgeProps,
2856 }
2857 impl Fx {
2858 fn new() -> Self {
2859 Fx {
2860 ids: IdMap::new(),
2861 syms: Interner::new(),
2862 labels: vec![],
2863 props: ColumnStore::new(),
2864 topo: Topology::new(),
2865 eprops: EdgeProps::new(),
2866 }
2867 }
2868 fn add(&mut self, label: &str, key: &str, props: Vec<(&str, Value)>) -> u32 {
2869 let id = self.ids.get_or_insert(key);
2870 let sym = self.syms.intern(label);
2871 self.labels.resize(id as usize + 1, u32::MAX);
2872 self.labels[id as usize] = sym;
2873 for (f, v) in props {
2874 self.props.set(id, f, v);
2875 }
2876 id
2877 }
2878 fn g(&mut self) -> GraphMut<'_> {
2879 GraphMut {
2880 ids: &self.ids,
2881 syms: &mut self.syms,
2882 labels: &self.labels,
2883 props: ColumnsView::owned(&self.props),
2884 topo: &mut self.topo,
2885 edge_props: &mut self.eprops,
2886 }
2887 }
2888 }
2889
2890 fn tags(items: &[&str]) -> Value {
2891 Value::List(items.iter().map(|s| Value::Str((*s).into())).collect())
2892 }
2893
2894 fn overlap_rule() -> RuleDef {
2895 RuleDef {
2896 name: "rel".into(),
2897 src_label: "A".into(),
2898 dst_label: "A".into(),
2899 predicate: Predicate::Overlap {
2900 field: "tags".into(),
2901 min: 0.4,
2902 },
2903 edge_type: "REL".into(),
2904 weight_prop: Some("score".into()),
2905 max_edges: None,
2906 approximate: false,
2907 via_label: None,
2908 via_edge: None,
2909 via_dir: None,
2910 }
2911 }
2912
2913 fn emb(xs: &[f64]) -> Value {
2914 Value::List(xs.iter().copied().map(Value::Float).collect())
2915 }
2916
2917 fn approx_vec_rule() -> RuleDef {
2918 RuleDef {
2919 name: "sim".into(),
2920 src_label: "V".into(),
2921 dst_label: "V".into(),
2922 predicate: Predicate::VectorSimilar {
2923 field: "emb".into(),
2924 min: 0.5,
2925 },
2926 edge_type: "SIM".into(),
2927 weight_prop: None,
2928 max_edges: None,
2929 approximate: true,
2930 via_label: None,
2931 via_edge: None,
2932 via_dir: None,
2933 }
2934 }
2935
2936 #[test]
2937 fn approximate_rule_rebuilds_after_drift_threshold() {
2938 with_ivf_drift_rebuild(1, || {
2939 let mut fx = Fx::new();
2940 let mut ids = Vec::new();
2941 for i in 0..6 {
2942 let x = i as f64 * 0.2;
2943 ids.push(fx.add("V", &format!("v{i}"), vec![("emb", emb(&[x, 1.0 - x]))]));
2944 }
2945 let mut eng = RuleEngine::new();
2946 {
2947 let mut g = fx.g();
2948 eng.create_rule(approx_vec_rule(), &mut g).unwrap();
2949 }
2950 assert!(eng.take_rebuild_needed().is_empty());
2951 {
2952 let mut g = fx.g();
2953 eng.on_node_removed(ids[0], &mut g);
2954 }
2955 assert!(
2956 eng.take_rebuild_needed().is_empty(),
2957 "drift=1 is not > threshold 1"
2958 );
2959 {
2960 let mut g = fx.g();
2961 eng.on_node_removed(ids[1], &mut g);
2962 }
2963 assert_eq!(eng.take_rebuild_needed(), vec!["sim".to_string()]);
2964 {
2965 let mut g = fx.g();
2966 eng.rebuild("sim", &mut g).unwrap();
2967 }
2968 assert!(
2969 eng.take_rebuild_needed().is_empty(),
2970 "rebuild must reset drift and not re-queue itself"
2971 );
2972 let drift = eng
2973 .export_ivf_state()
2974 .get("sim")
2975 .map(|(_, dst)| dst.2)
2976 .unwrap();
2977 assert_eq!(drift, 0, "rebuild resets dst-side IVF drift");
2978 });
2979 }
2980
2981 #[test]
2982 fn backfill_creates_edges_with_scores_and_delete_removes_exactly_them() {
2983 let mut fx = Fx::new();
2984 let a = fx.add("A", "a", vec![("tags", tags(&["x", "y"]))]);
2985 let b = fx.add("A", "b", vec![("tags", tags(&["x", "y"]))]);
2986 let _c = fx.add("A", "c", vec![("tags", tags(&["q"]))]);
2987 let et = fx.syms.intern("REL");
2989 fx.topo.add_edge(et, a, b);
2990 let mut eng = RuleEngine::new();
2991 let mut g = fx.g();
2992 eng.create_rule(overlap_rule(), &mut g).unwrap();
2993 assert!(g.topo.neighbors(et, Direction::Out, b).contains(&a));
2995 assert_eq!(
2996 g.edge_props.get(et, b, a, "score"),
2997 Some(&Value::Float(1.0))
2998 );
2999 assert!(!eng.is_owned(et, a, b));
3000 assert!(eng.is_owned(et, b, a));
3001 eng.delete_rule("rel", &mut g).unwrap();
3002 assert!(g.topo.neighbors(et, Direction::Out, a).contains(&b)); assert!(!g.topo.neighbors(et, Direction::Out, b).contains(&a)); assert_eq!(g.edge_props.get(et, b, a, "score"), None);
3005 }
3006
3007 #[test]
3008 fn incremental_update_adds_and_removes_edges() {
3009 let mut fx = Fx::new();
3010 let a = fx.add("A", "a", vec![("tags", tags(&["x", "y"]))]);
3011 let b = fx.add("A", "b", vec![("tags", tags(&["y", "z"]))]);
3012 let et = fx.syms.intern("REL");
3013 let mut eng = RuleEngine::new();
3014 {
3015 let mut g = fx.g();
3016 eng.create_rule(overlap_rule(), &mut g).unwrap(); assert_eq!(g.topo.edge_count(), 0);
3018 }
3019 let old = fx.props.get(b, "tags").cloned();
3021 fx.props.set(b, "tags", tags(&["x", "y"]));
3022 {
3023 let mut g = fx.g();
3024 eng.on_node_changed(b, Some(("tags", old)), &mut g);
3025 assert!(g.topo.neighbors(et, Direction::Out, a).contains(&b));
3026 assert!(g.topo.neighbors(et, Direction::Out, b).contains(&a));
3027 }
3028 let old = fx.props.get(b, "tags").cloned();
3030 fx.props.set(b, "tags", tags(&["qqq"]));
3031 let mut g = fx.g();
3032 eng.on_node_changed(b, Some(("tags", old)), &mut g);
3033 assert_eq!(g.topo.edge_count(), 0);
3034 assert_eq!(g.edge_props.get(et, a, b, "score"), None);
3035 }
3036
3037 #[test]
3038 fn key_match_new_node_links_and_rebuild_is_noop() {
3039 let mut fx = Fx::new();
3040 fx.add("C", "c1", vec![]);
3041 let mut eng = RuleEngine::new();
3042 {
3043 let mut g = fx.g();
3044 eng.create_rule(
3045 RuleDef {
3046 name: "fk".into(),
3047 src_label: "T".into(),
3048 dst_label: "C".into(),
3049 predicate: Predicate::KeyMatch {
3050 field: "cid".into(),
3051 },
3052 edge_type: "AT".into(),
3053 weight_prop: None,
3054 max_edges: None,
3055 approximate: false,
3056 via_label: None,
3057 via_edge: None,
3058 via_dir: None,
3059 },
3060 &mut g,
3061 )
3062 .unwrap();
3063 }
3064 let t = fx.add("T", "t1", vec![("cid", Value::Str("c1".into()))]);
3065 let (at, c1, count_before) = {
3066 let mut g = fx.g();
3067 eng.on_node_changed(t, None, &mut g);
3068 let at = g.syms.get("AT").unwrap();
3069 let c1 = g.ids.get("c1").unwrap();
3070 assert!(g.topo.neighbors(at, Direction::Out, t).contains(&c1));
3071 (at, c1, g.topo.edge_count())
3072 };
3073 let mut g = fx.g();
3074 eng.rebuild("fk", &mut g).unwrap();
3075 assert_eq!(g.topo.edge_count(), count_before); assert!(g.topo.neighbors(at, Direction::Out, t).contains(&c1));
3077 }
3078
3079 #[test]
3080 fn score_refresh_on_persisting_owned_edge() {
3081 let mut fx = Fx::new();
3084 let a = fx.add("A", "a", vec![("tags", tags(&["x", "y", "z"]))]);
3085 let b = fx.add("A", "b", vec![("tags", tags(&["x", "y", "q"]))]);
3086 let et = fx.syms.intern("SIM");
3087 let mut eng = RuleEngine::new();
3088 {
3089 let mut g = fx.g();
3090 eng.create_rule(
3091 RuleDef {
3092 name: "sim".into(),
3093 src_label: "A".into(),
3094 dst_label: "A".into(),
3095 predicate: Predicate::Overlap {
3096 field: "tags".into(),
3097 min: 0.2,
3098 },
3099 edge_type: "SIM".into(),
3100 weight_prop: Some("score".into()),
3101 max_edges: None,
3102 approximate: false,
3103 via_label: None,
3104 via_edge: None,
3105 via_dir: None,
3106 },
3107 &mut g,
3108 )
3109 .unwrap();
3110 assert!(g.topo.neighbors(et, Direction::Out, a).contains(&b));
3112 assert!(g.topo.neighbors(et, Direction::Out, b).contains(&a));
3113 assert!(eng.is_owned(et, a, b) || eng.is_owned(et, b, a));
3114 let check = |v: Option<&Value>| {
3115 if let Some(Value::Float(f)) = v {
3116 assert!(
3117 (f - 0.5).abs() < 1e-9,
3118 "initial score should be 0.5, got {f}"
3119 );
3120 }
3121 };
3122 check(g.edge_props.get(et, a, b, "score"));
3123 check(g.edge_props.get(et, b, a, "score"));
3124 }
3125 let old = fx.props.get(b, "tags").cloned();
3127 fx.props.set(b, "tags", tags(&["x", "y", "z"]));
3128 {
3129 let mut g = fx.g();
3130 eng.on_node_changed(b, Some(("tags", old)), &mut g);
3131 assert!(g.topo.neighbors(et, Direction::Out, a).contains(&b));
3133 assert!(g.topo.neighbors(et, Direction::Out, b).contains(&a));
3134 assert_eq!(
3136 g.edge_props.get(et, a, b, "score"),
3137 Some(&Value::Float(1.0)),
3138 "score on a→b must refresh to 1.0"
3139 );
3140 assert_eq!(
3141 g.edge_props.get(et, b, a, "score"),
3142 Some(&Value::Float(1.0)),
3143 "score on b→a must refresh to 1.0"
3144 );
3145 }
3146 }
3147
3148 #[test]
3149 fn dst_side_keymatch_links_when_c_node_inserted_after_t() {
3150 let mut fx = Fx::new();
3152 let t = fx.add("T", "t1", vec![("cid", Value::Str("c9".into()))]);
3154 let mut eng = RuleEngine::new();
3155 {
3156 let mut g = fx.g();
3157 eng.create_rule(
3158 RuleDef {
3159 name: "fk".into(),
3160 src_label: "T".into(),
3161 dst_label: "C".into(),
3162 predicate: Predicate::KeyMatch {
3163 field: "cid".into(),
3164 },
3165 edge_type: "AT".into(),
3166 weight_prop: None,
3167 max_edges: None,
3168 approximate: false,
3169 via_label: None,
3170 via_edge: None,
3171 via_dir: None,
3172 },
3173 &mut g,
3174 )
3175 .unwrap();
3176 let at = g.syms.intern("AT");
3178 assert_eq!(g.topo.edge_count(), 0, "no C node yet → no edge");
3179 let _ = at;
3181 }
3182 let c9 = fx.add("C", "c9", vec![]);
3184 {
3185 let mut g = fx.g();
3186 eng.on_node_changed(c9, None, &mut g);
3187 let at = g.syms.get("AT").unwrap();
3188 assert!(
3190 g.topo.neighbors(at, Direction::Out, t).contains(&c9),
3191 "T→C edge must appear when C node is inserted"
3192 );
3193 assert!(eng.is_owned(at, t, c9));
3194 }
3195 }
3196
3197 #[test]
3198 fn on_node_removed_retracts_both_sides_and_deindexes() {
3199 let mut fx = Fx::new();
3200 let a = fx.add("A", "a", vec![("tags", tags(&["x", "y"]))]);
3201 let b = fx.add("A", "b", vec![("tags", tags(&["x", "y"]))]);
3202 let et = fx.syms.intern("REL");
3203 let mut eng = RuleEngine::new();
3204 {
3205 let mut g = fx.g();
3206 eng.create_rule(overlap_rule(), &mut g).unwrap();
3207 assert!(g.topo.neighbors(et, Direction::Out, a).contains(&b));
3208 assert!(g.topo.neighbors(et, Direction::Out, b).contains(&a));
3209 }
3210 {
3211 let mut g = fx.g();
3212 eng.on_node_removed(a, &mut g);
3213 assert!(!g.topo.neighbors(et, Direction::Out, a).contains(&b));
3214 assert!(!g.topo.neighbors(et, Direction::Out, b).contains(&a));
3215 assert_eq!(g.edge_props.get(et, a, b, "score"), None);
3216 assert_eq!(g.edge_props.get(et, b, a, "score"), None);
3217 assert!(!eng.is_owned(et, a, b));
3218 assert!(!eng.is_owned(et, b, a));
3219 }
3220 let c = fx.add("A", "c", vec![("tags", tags(&["x", "y"]))]);
3222 {
3223 let mut g = fx.g();
3224 eng.on_node_changed(c, None, &mut g);
3225 assert!(g.topo.neighbors(et, Direction::Out, b).contains(&c));
3226 assert!(g.topo.neighbors(et, Direction::Out, c).contains(&b));
3227 assert!(!g.topo.neighbors(et, Direction::Out, c).contains(&a));
3228 assert!(!g.topo.neighbors(et, Direction::Out, a).contains(&c));
3229 }
3230 {
3232 let mut g = fx.g();
3233 eng.on_node_removed(a, &mut g);
3234 assert!(g.topo.neighbors(et, Direction::Out, b).contains(&c));
3235 }
3236 }
3237
3238 #[test]
3239 fn duplicate_name_and_unknown_delete_error() {
3240 let mut fx = Fx::new();
3241 let mut eng = RuleEngine::new();
3242 let mut g = fx.g();
3243 eng.create_rule(overlap_rule(), &mut g).unwrap();
3244 assert!(eng.create_rule(overlap_rule(), &mut g).is_err());
3245 assert!(eng.delete_rule("nope", &mut g).is_err());
3246 }
3247
3248 #[test]
3254 fn coowned_edge_type_survives_first_delete_gone_after_second() {
3255 let mut fx = Fx::new();
3256 let a = fx.add("A", "a", vec![("tags", tags(&["x", "y"]))]);
3257 let b = fx.add("A", "b", vec![("tags", tags(&["x", "y"]))]);
3258 let mut eng = RuleEngine::new();
3259 {
3260 let mut g = fx.g();
3261 eng.create_rule(
3263 RuleDef {
3264 name: "r1".into(),
3265 src_label: "A".into(),
3266 dst_label: "A".into(),
3267 predicate: Predicate::Overlap {
3268 field: "tags".into(),
3269 min: 0.1,
3270 },
3271 edge_type: "REL2".into(),
3272 weight_prop: None,
3273 max_edges: None,
3274 approximate: false,
3275 via_label: None,
3276 via_edge: None,
3277 via_dir: None,
3278 },
3279 &mut g,
3280 )
3281 .unwrap();
3282 eng.create_rule(
3284 RuleDef {
3285 name: "r2".into(),
3286 src_label: "A".into(),
3287 dst_label: "A".into(),
3288 predicate: Predicate::Overlap {
3289 field: "tags".into(),
3290 min: 0.2,
3291 },
3292 edge_type: "REL2".into(),
3293 weight_prop: None,
3294 max_edges: None,
3295 approximate: false,
3296 via_label: None,
3297 via_edge: None,
3298 via_dir: None,
3299 },
3300 &mut g,
3301 )
3302 .unwrap();
3303
3304 let et = g.syms.intern("REL2");
3305 assert!(
3307 g.topo.neighbors(et, Direction::Out, a).contains(&b),
3308 "a→b must exist after both rules created"
3309 );
3310 assert!(
3311 g.topo.neighbors(et, Direction::Out, b).contains(&a),
3312 "b→a must exist after both rules created"
3313 );
3314
3315 eng.delete_rule("r1", &mut g).unwrap();
3317 assert!(
3318 g.topo.neighbors(et, Direction::Out, a).contains(&b),
3319 "a→b must survive R1 deletion (R2 rebuilds and claims it)"
3320 );
3321 assert!(
3322 g.topo.neighbors(et, Direction::Out, b).contains(&a),
3323 "b→a must survive R1 deletion (R2 rebuilds and claims it)"
3324 );
3325 assert!(
3327 eng.is_owned(et, a, b),
3328 "a→b must be owned by R2 after rebuild"
3329 );
3330 assert!(
3331 eng.is_owned(et, b, a),
3332 "b→a must be owned by R2 after rebuild"
3333 );
3334
3335 eng.delete_rule("r2", &mut g).unwrap();
3337 assert!(
3338 !g.topo.neighbors(et, Direction::Out, a).contains(&b),
3339 "a→b must be gone after both rules deleted"
3340 );
3341 assert!(
3342 !g.topo.neighbors(et, Direction::Out, b).contains(&a),
3343 "b→a must be gone after both rules deleted"
3344 );
3345 }
3346 }
3347
3348 fn topk_eq_rule(k: u64) -> RuleDef {
3350 RuleDef {
3351 name: "eq".into(),
3352 src_label: "N".into(),
3353 dst_label: "N".into(),
3354 predicate: Predicate::FieldEqual { field: "k".into() },
3355 edge_type: "EQ".into(),
3356 weight_prop: None,
3357 max_edges: Some(k),
3358 approximate: false,
3359 via_label: None,
3360 via_edge: None,
3361 via_dir: None,
3362 }
3363 }
3364
3365 fn prov_pairs(eng: &RuleEngine, name: &str) -> BTreeSet<(u32, u32)> {
3366 eng.provenance()
3367 .get(name)
3368 .map(|s| s.iter().map(|&(_, a, b)| (a, b)).collect())
3369 .unwrap_or_default()
3370 }
3371
3372 #[test]
3376 fn topk_k1_keeps_best_scored_dst() {
3377 let mut fx = Fx::new();
3378 let mut eng = RuleEngine::new();
3379 {
3380 let mut g = fx.g();
3381 eng.create_rule(topk_eq_rule(1), &mut g).unwrap();
3382 }
3383 let mut ids = Vec::new();
3385 for i in 0..4usize {
3386 let id = fx.add(
3387 "N",
3388 &format!("n{i}"),
3389 vec![("k", Value::Str("const".into()))],
3390 );
3391 ids.push(id);
3392 let mut g = fx.g();
3393 eng.on_node_changed(id, None, &mut g);
3394 }
3395 let et = fx.syms.get("EQ").unwrap();
3396 let expected_dsts = [ids[1], ids[0], ids[0], ids[0]];
3402 for (i, (&src, &expected_dst)) in ids.iter().zip(expected_dsts.iter()).enumerate() {
3403 let out: Vec<u32> = fx.topo.neighbors(et, Direction::Out, src).to_vec();
3404 assert_eq!(
3405 out,
3406 vec![expected_dst],
3407 "src n{i} should point only to the best dst"
3408 );
3409 }
3410 assert_eq!(eng.provenance()["eq"].len(), 4);
3411 assert!(!eng.is_tripped("eq"), "top-k rules never trip");
3412 }
3413
3414 #[test]
3417 fn topk_insert_evict() {
3418 let mut fx = Fx::new();
3422 let rule = RuleDef {
3423 name: "nw".into(),
3424 src_label: "S".into(),
3425 dst_label: "D".into(),
3426 predicate: Predicate::NumericWithin {
3427 field: "v".into(),
3428 tolerance: 10.0,
3429 },
3430 edge_type: "NEAR".into(),
3431 weight_prop: Some("score".into()),
3432 max_edges: Some(1),
3433 approximate: false,
3434 via_label: None,
3435 via_edge: None,
3436 via_dir: None,
3437 };
3438 let mut eng = RuleEngine::new();
3439 {
3440 let mut g = fx.g();
3441 eng.create_rule(rule, &mut g).unwrap();
3442 }
3443
3444 let s0 = fx.add("S", "s0", vec![("v", Value::Float(0.0))]);
3446 let d_far = fx.add("D", "d_far", vec![("v", Value::Float(9.0))]);
3448 {
3449 let mut g = fx.g();
3450 eng.on_node_changed(s0, None, &mut g);
3451 eng.on_node_changed(d_far, None, &mut g);
3452 }
3453 let et = fx.syms.get("NEAR").unwrap();
3454 assert!(fx.topo.neighbors(et, Direction::Out, s0).contains(&d_far));
3456 assert_eq!(eng.provenance()["nw"].len(), 1);
3457
3458 let d_close = fx.add("D", "d_close", vec![("v", Value::Float(1.0))]);
3460 {
3461 let mut g = fx.g();
3462 eng.on_node_changed(d_close, None, &mut g);
3463 }
3464 let out: Vec<u32> = fx.topo.neighbors(et, Direction::Out, s0).to_vec();
3466 assert_eq!(out, vec![d_close], "d_close should evict d_far");
3467 assert!(!fx.topo.neighbors(et, Direction::Out, s0).contains(&d_far));
3468 assert_eq!(eng.provenance()["nw"].len(), 1);
3469 assert!(eng.by_node_consistent());
3470 }
3471
3472 #[test]
3474 fn topk_retract_backfill() {
3475 let mut fx = Fx::new();
3476 let rule = RuleDef {
3477 name: "nw".into(),
3478 src_label: "S".into(),
3479 dst_label: "D".into(),
3480 predicate: Predicate::NumericWithin {
3481 field: "v".into(),
3482 tolerance: 10.0,
3483 },
3484 edge_type: "NEAR".into(),
3485 weight_prop: Some("score".into()),
3486 max_edges: Some(1),
3487 approximate: false,
3488 via_label: None,
3489 via_edge: None,
3490 via_dir: None,
3491 };
3492 let mut eng = RuleEngine::new();
3493
3494 let s0 = fx.add("S", "s0", vec![("v", Value::Float(0.0))]);
3495 let d_close = fx.add("D", "d_close", vec![("v", Value::Float(1.0))]); let d_far = fx.add("D", "d_far", vec![("v", Value::Float(8.0))]); {
3498 let mut g = fx.g();
3499 eng.create_rule(rule, &mut g).unwrap();
3500 }
3501 let et = fx.syms.get("NEAR").unwrap();
3502 assert!(fx.topo.neighbors(et, Direction::Out, s0).contains(&d_close));
3504 assert!(!fx.topo.neighbors(et, Direction::Out, s0).contains(&d_far));
3505 assert_eq!(eng.provenance()["nw"].len(), 1);
3506
3507 let old = fx.props.get(d_close, "v").cloned();
3509 fx.props.set(d_close, "v", Value::Float(50.0));
3510 {
3511 let mut g = fx.g();
3512 eng.on_node_changed(d_close, Some(("v", old)), &mut g);
3513 }
3514 assert!(!fx.topo.neighbors(et, Direction::Out, s0).contains(&d_close));
3516 assert!(
3517 fx.topo.neighbors(et, Direction::Out, s0).contains(&d_far),
3518 "d_far should backfill after d_close retracted"
3519 );
3520 assert_eq!(eng.provenance()["nw"].len(), 1);
3521 assert!(eng.by_node_consistent());
3522 }
3523
3524 #[test]
3526 fn topk_tie_broken_by_dst_key() {
3527 let mut fx = Fx::new();
3529 let mut eng = RuleEngine::new();
3530 {
3531 let mut g = fx.g();
3532 eng.create_rule(topk_eq_rule(2), &mut g).unwrap();
3533 }
3534 for name in ["a", "b", "c", "d", "e"] {
3537 let id = fx.add("N", name, vec![("k", Value::Str("x".into()))]);
3538 let mut g = fx.g();
3539 eng.on_node_changed(id, None, &mut g);
3540 }
3541 let et = fx.syms.get("EQ").unwrap();
3542 let get_id = |key: &str| fx.ids.get(key).unwrap();
3543 let a = get_id("a");
3545 let b = get_id("b");
3546 let c = get_id("c");
3547 let out_a: BTreeSet<u32> = fx
3548 .topo
3549 .neighbors(et, Direction::Out, a)
3550 .iter()
3551 .copied()
3552 .collect();
3553 assert!(out_a.contains(&b), "a→b (b is best key after a)");
3554 assert!(out_a.contains(&c), "a→c (c is 2nd best key)");
3555 assert_eq!(out_a.len(), 2);
3556 let e = get_id("e");
3558 let out_e: BTreeSet<u32> = fx
3559 .topo
3560 .neighbors(et, Direction::Out, e)
3561 .iter()
3562 .copied()
3563 .collect();
3564 assert!(out_e.contains(&a), "e→a");
3565 assert!(out_e.contains(&b), "e→b");
3566 assert_eq!(out_e.len(), 2);
3567 assert!(eng.by_node_consistent());
3568 }
3569
3570 #[test]
3572 fn topk_k_larger_than_candidate_count() {
3573 let mut fx = Fx::new();
3574 let mut eng = RuleEngine::new();
3575 {
3576 let mut g = fx.g();
3577 eng.create_rule(topk_eq_rule(100), &mut g).unwrap();
3579 }
3580 for i in 0..4usize {
3581 let id = fx.add("N", &format!("n{i}"), vec![("k", Value::Str("c".into()))]);
3582 let mut g = fx.g();
3583 eng.on_node_changed(id, None, &mut g);
3584 }
3585 assert_eq!(eng.provenance()["eq"].len(), 12);
3587 assert!(!eng.is_tripped("eq"));
3588 }
3589
3590 #[test]
3593 fn topk_rebuild_exact() {
3594 let mut fx = Fx::new();
3595 let mut eng = RuleEngine::new();
3596 {
3597 let mut g = fx.g();
3598 eng.create_rule(topk_eq_rule(1), &mut g).unwrap();
3599 }
3600 let _a = fx.add("N", "a", vec![("k", Value::Str("x".into()))]);
3602 let _b = fx.add("N", "b", vec![("k", Value::Str("x".into()))]);
3603 let _c = fx.add("N", "c", vec![("k", Value::Str("x".into()))]);
3604 {
3605 let mut g = fx.g();
3606 eng.on_node_changed(_a, None, &mut g);
3607 eng.on_node_changed(_b, None, &mut g);
3608 eng.on_node_changed(_c, None, &mut g);
3609 }
3610 assert_eq!(eng.provenance()["eq"].len(), 3);
3611
3612 {
3614 let mut g = fx.g();
3615 eng.rebuild("eq", &mut g).unwrap();
3616 }
3617 assert_eq!(eng.provenance()["eq"].len(), 3);
3618 assert!(!eng.is_tripped("eq"));
3619 assert!(eng.by_node_consistent());
3620 }
3621
3622 #[test]
3624 fn topk_by_node_consistent() {
3625 let mut fx = Fx::new();
3626 let mut eng = RuleEngine::new();
3627 {
3628 let mut g = fx.g();
3629 eng.create_rule(topk_eq_rule(2), &mut g).unwrap();
3630 }
3631 for i in 0..5usize {
3632 let id = fx.add(
3633 "N",
3634 &format!("n{i}"),
3635 vec![("k", Value::Str("const".into()))],
3636 );
3637 let mut g = fx.g();
3638 eng.on_node_changed(id, None, &mut g);
3639 }
3640 assert!(eng.by_node_consistent(), "consistent after insertions");
3641
3642 let id2 = fx.ids.get("n2").unwrap();
3644 let old = fx.props.get(id2, "k").cloned();
3645 fx.props.set(id2, "k", Value::Str("other".into()));
3646 {
3647 let mut g = fx.g();
3648 eng.on_node_changed(id2, Some(("k", old)), &mut g);
3649 }
3650 assert!(eng.by_node_consistent(), "consistent after eviction");
3651
3652 {
3653 let mut g = fx.g();
3654 eng.rebuild("eq", &mut g).unwrap();
3655 }
3656 assert!(eng.by_node_consistent(), "consistent after rebuild");
3657 }
3658
3659 fn numeric_rule() -> RuleDef {
3660 RuleDef {
3661 name: "nw".into(),
3662 src_label: "C".into(),
3663 dst_label: "C".into(),
3664 predicate: Predicate::NumericWithin {
3665 field: "year".into(),
3666 tolerance: 2.0,
3667 },
3668 edge_type: "NEAR".into(),
3669 weight_prop: Some("score".into()),
3670 max_edges: None,
3671 approximate: false,
3672 via_label: None,
3673 via_edge: None,
3674 via_dir: None,
3675 }
3676 }
3677
3678 fn geo_rule() -> RuleDef {
3679 RuleDef {
3680 name: "geo".into(),
3681 src_label: "City".into(),
3682 dst_label: "City".into(),
3683 predicate: Predicate::GeoRadius {
3684 field: "loc".into(),
3685 km: 400.0,
3686 },
3687 edge_type: "NEAR_GEO".into(),
3688 weight_prop: Some("score".into()),
3689 max_edges: None,
3690 approximate: false,
3691 via_label: None,
3692 via_edge: None,
3693 via_dir: None,
3694 }
3695 }
3696
3697 fn vec_rule() -> RuleDef {
3698 RuleDef {
3699 name: "vec".into(),
3700 src_label: "Doc".into(),
3701 dst_label: "Doc".into(),
3702 predicate: Predicate::VectorSimilar {
3703 field: "emb".into(),
3704 min: 0.9,
3705 },
3706 edge_type: "SIM".into(),
3707 weight_prop: Some("score".into()),
3708 max_edges: None,
3709 approximate: false,
3710 via_label: None,
3711 via_edge: None,
3712 via_dir: None,
3713 }
3714 }
3715
3716 fn pair_edges(topo: &Topology, et: u32, a: u32, b: u32) -> bool {
3717 topo.neighbors(et, Direction::Out, a).contains(&b)
3718 && topo.neighbors(et, Direction::Out, b).contains(&a)
3719 }
3720
3721 #[test]
3722 fn numeric_within_incremental_crosses_bucket_and_clears_old_index() {
3723 let mut fx = Fx::new();
3724 let a = fx.add("C", "a", vec![("year", Value::Float(10.0))]);
3725 let b = fx.add("C", "b", vec![("year", Value::Float(12.0))]);
3726 let et = fx.syms.intern("NEAR");
3727 let mut eng = RuleEngine::new();
3728 {
3729 let mut g = fx.g();
3730 eng.create_rule(numeric_rule(), &mut g).unwrap();
3731 assert!(pair_edges(g.topo, et, a, b));
3733 }
3734
3735 let old = fx.props.get(b, "year").cloned();
3738 fx.props.set(b, "year", Value::Float(16.1));
3739 {
3740 let mut g = fx.g();
3741 eng.on_node_changed(b, Some(("year", old)), &mut g);
3742 assert!(!pair_edges(g.topo, et, a, b));
3743 assert_eq!(g.topo.edge_count(), 0);
3744 }
3745 let def = numeric_rule();
3746 let spec = candidate_spec_for(&def);
3747 let old_map: std::collections::HashMap<_, _> =
3748 [("year".to_string(), Value::Float(12.0))].into();
3749 let old_get = |f: &str| old_map.get(f).cloned();
3750 let src_hits = eng.indexes["nw"].src_side.candidates(&spec, &old_get);
3751 let dst_hits = eng.indexes["nw"].dst_side.candidates(&spec, &old_get);
3752 assert!(!src_hits.contains(&b), "old src bucket must drop b");
3753 assert!(!dst_hits.contains(&b), "old dst bucket must drop b");
3754 assert!(src_hits.contains(&a));
3755
3756 let old = fx.props.get(b, "year").cloned();
3758 fx.props.set(b, "year", Value::Float(11.9));
3759 let mut g = fx.g();
3760 eng.on_node_changed(b, Some(("year", old)), &mut g);
3761 assert!(pair_edges(g.topo, et, a, b));
3762 }
3763
3764 fn loc_val(lat: f64, lon: f64) -> Value {
3765 Value::List(vec![Value::Float(lat), Value::Float(lon)])
3766 }
3767
3768 fn emb_val(vals: &[f64]) -> Value {
3769 Value::List(vals.iter().copied().map(Value::Float).collect())
3770 }
3771
3772 #[test]
3773 fn rebuild_is_noop_for_numeric_geo_and_vector() {
3774 let mut fx = Fx::new();
3775 let ca = fx.add("C", "ca", vec![("year", Value::Int(1998))]);
3776 let cb = fx.add("C", "cb", vec![("year", Value::Float(2000.0))]);
3777 let pa = fx.add("City", "paris", vec![("loc", loc_val(48.8566, 2.3522))]);
3778 let lo = fx.add("City", "london", vec![("loc", loc_val(51.5074, -0.1278))]);
3779 let da = fx.add("Doc", "d1", vec![("emb", emb_val(&[1.0, 0.0]))]);
3780 let db = fx.add("Doc", "d2", vec![("emb", emb_val(&[1.0, 0.0]))]);
3781
3782 let mut eng = RuleEngine::new();
3783 {
3784 let mut g = fx.g();
3785 eng.create_rule(numeric_rule(), &mut g).unwrap();
3786 eng.create_rule(geo_rule(), &mut g).unwrap();
3787 eng.create_rule(vec_rule(), &mut g).unwrap();
3788 }
3789
3790 let (near, ngeo, sim) = (
3791 fx.syms.get("NEAR").unwrap(),
3792 fx.syms.get("NEAR_GEO").unwrap(),
3793 fx.syms.get("SIM").unwrap(),
3794 );
3795 assert!(pair_edges(&fx.topo, near, ca, cb));
3796 assert!(pair_edges(&fx.topo, ngeo, pa, lo));
3797 assert!(pair_edges(&fx.topo, sim, da, db));
3798 let before = fx.topo.edge_count();
3799
3800 {
3801 let mut g = fx.g();
3802 eng.rebuild("nw", &mut g).unwrap();
3803 eng.rebuild("geo", &mut g).unwrap();
3804 eng.rebuild("vec", &mut g).unwrap();
3805 }
3806 assert_eq!(fx.topo.edge_count(), before);
3807 assert!(pair_edges(&fx.topo, near, ca, cb));
3808 assert!(pair_edges(&fx.topo, ngeo, pa, lo));
3809 assert!(pair_edges(&fx.topo, sim, da, db));
3810 }
3811
3812 fn fk_rule() -> RuleDef {
3813 RuleDef {
3814 name: "works_at".into(),
3815 src_label: "T".into(),
3816 dst_label: "C".into(),
3817 predicate: Predicate::KeyMatch {
3818 field: "cid".into(),
3819 },
3820 edge_type: "AT".into(),
3821 weight_prop: None,
3822 max_edges: None,
3823 approximate: false,
3824 via_label: None,
3825 via_edge: None,
3826 via_dir: None,
3827 }
3828 }
3829
3830 #[test]
3831 fn by_node_matches_rebuild_after_mutation_storm() {
3832 let mut fx = Fx::new();
3833 let hub = fx.add("C", "hub", vec![]);
3834 let other = fx.add("C", "other", vec![]);
3835 let mut people = Vec::new();
3836 for i in 0..40 {
3837 let cid = if i < 30 { "hub" } else { "other" };
3838 people.push(fx.add(
3839 "T",
3840 &format!("t{i}"),
3841 vec![("cid", Value::Str(cid.into())), ("tags", tags(&["x", "y"]))],
3842 ));
3843 }
3844 let mut overlap = overlap_rule();
3845 overlap.src_label = "T".into();
3846 overlap.dst_label = "T".into();
3847 let mut eng = RuleEngine::new();
3848 {
3849 let mut g = fx.g();
3850 eng.create_rule(fk_rule(), &mut g).unwrap();
3851 eng.create_rule(overlap, &mut g).unwrap();
3852 }
3853 assert!(eng.by_node_consistent());
3854 assert_eq!(eng.provenance_touching_len(hub), 30);
3855
3856 for (i, &id) in people.iter().enumerate().take(15) {
3858 let old = fx.props.get(id, "cid").cloned();
3859 fx.props.set(id, "cid", Value::Str("other".into()));
3860 let mut g = fx.g();
3861 eng.on_node_changed(id, Some(("cid", old)), &mut g);
3862 assert!(
3863 eng.by_node_consistent(),
3864 "inconsistent after cid update {i}"
3865 );
3866 }
3867 for &id in people.iter().take(8) {
3868 let old = fx.props.get(id, "tags").cloned();
3869 fx.props.set(id, "tags", tags(&["q"]));
3870 let mut g = fx.g();
3871 eng.on_node_changed(id, Some(("tags", old)), &mut g);
3872 }
3873 assert!(eng.by_node_consistent());
3874
3875 {
3877 let mut g = fx.g();
3878 eng.on_node_removed(people[0], &mut g);
3879 }
3880 fx.labels[people[0] as usize] = u32::MAX;
3881 assert!(eng.by_node_consistent());
3882 assert_eq!(eng.provenance_touching_len(people[0]), 0);
3883
3884 {
3885 let mut g = fx.g();
3886 eng.rebuild("works_at", &mut g).unwrap();
3887 eng.rebuild("rel", &mut g).unwrap();
3888 }
3889 assert!(eng.by_node_consistent());
3890
3891 {
3892 let mut g = fx.g();
3893 eng.delete_rule("rel", &mut g).unwrap();
3894 }
3895 assert!(eng.by_node_consistent());
3896 assert_eq!(eng.provenance_touching(people[1]).count(), 1);
3897
3898 let (defs, prov, tripped, fires) = eng.to_persist();
3900 let restored = RuleEngine::from_persist(defs, prov, tripped, fires);
3901 assert!(restored.by_node_consistent());
3902 assert_eq!(
3903 restored.provenance_touching_len(hub),
3904 eng.provenance_touching_len(hub)
3905 );
3906 assert_eq!(
3907 restored.provenance_touching_len(other),
3908 eng.provenance_touching_len(other)
3909 );
3910 }
3911
3912 #[test]
3913 fn provenance_touching_high_degree_hub() {
3914 let mut fx = Fx::new();
3915 let hub = fx.add("C", "hub", vec![]);
3916 let mut first = None;
3917 for i in 0..256 {
3918 let id = fx.add(
3919 "T",
3920 &format!("t{i}"),
3921 vec![("cid", Value::Str("hub".into()))],
3922 );
3923 if first.is_none() {
3924 first = Some(id);
3925 }
3926 }
3927 let first = first.unwrap();
3928 let mut eng = RuleEngine::new();
3929 {
3930 let mut g = fx.g();
3931 eng.create_rule(fk_rule(), &mut g).unwrap();
3932 }
3933 assert!(eng.by_node_consistent());
3934 assert_eq!(eng.provenance_touching_len(hub), 256);
3935 assert_eq!(eng.provenance_touching_len(first), 1);
3936 let hits: Vec<_> = eng.provenance_touching(first).collect();
3937 assert_eq!(hits.len(), 1);
3938 assert_eq!(hits[0].0, "works_at");
3939 assert_eq!(hits[0].2, first);
3940 assert_eq!(hits[0].3, hub);
3941 }
3942
3943 #[test]
3951 fn by_node_consistent_across_inserts_and_rebuild() {
3952 let mut fx = Fx::new();
3953 let mut eng = RuleEngine::new();
3954 let rule = RuleDef {
3955 name: "eq".into(),
3956 src_label: "N".into(),
3957 dst_label: "N".into(),
3958 predicate: Predicate::FieldEqual { field: "k".into() },
3959 edge_type: "EQ".into(),
3960 weight_prop: None,
3961 max_edges: None, approximate: false,
3963 via_label: None,
3964 via_edge: None,
3965 via_dir: None,
3966 };
3967 {
3968 let mut g = fx.g();
3969 eng.create_rule(rule, &mut g).unwrap();
3970 }
3971 let mut ids = Vec::new();
3972 for i in 0..6 {
3973 let id = fx.add(
3974 "N",
3975 &format!("n{i}"),
3976 vec![("k", Value::Str("const".into()))],
3977 );
3978 ids.push(id);
3979 let mut g = fx.g();
3980 eng.on_node_changed(id, None, &mut g);
3981 }
3982 assert_eq!(eng.provenance()["eq"].len(), 30);
3984 assert!(!eng.is_tripped("eq"));
3985 assert!(eng.by_node_consistent(), "consistent after insertions");
3986
3987 let old = fx.props.get(ids[3], "k").cloned();
3989 fx.props.set(ids[3], "k", Value::Str("other".into()));
3990 {
3991 let mut g = fx.g();
3992 eng.on_node_changed(ids[3], Some(("k", old)), &mut g);
3993 }
3994 assert!(eng.by_node_consistent(), "consistent after property change");
3995
3996 {
3997 let mut g = fx.g();
3998 eng.rebuild("eq", &mut g).unwrap();
3999 }
4000 assert!(!eng.is_tripped("eq"));
4001 assert!(eng.by_node_consistent(), "consistent after rebuild");
4002 }
4003
4004 fn mix64(mut x: u64) -> u64 {
4005 x = x.wrapping_add(0x9E3779B97F4A7C15);
4006 x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
4007 x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB);
4008 x ^ (x >> 31)
4009 }
4010
4011 fn rand_emb(seed: u64, i: u32, dim: usize) -> Value {
4012 let vals: Vec<f64> = (0..dim)
4013 .map(|d| {
4014 let bits = mix64(seed ^ ((i as u64 + 1).wrapping_mul(0x100000001)) ^ (d as u64));
4015 let mut f = (bits as f64) / (u64::MAX as f64) * 2.0 - 1.0;
4016 if f == 0.0 {
4017 f = 1.0;
4018 }
4019 f
4020 })
4021 .collect();
4022 emb_val(&vals)
4023 }
4024
4025 fn seed_docs(n: u32, seed: u64) -> (Fx, Vec<u32>) {
4026 let dims = [2usize, 3, 4, 8];
4027 let mut fx = Fx::new();
4028 let mut ids = Vec::new();
4029 for i in 0..n {
4030 let dim = dims[(i as usize) % dims.len()];
4031 ids.push(fx.add(
4032 "Doc",
4033 &format!("d{i}"),
4034 vec![("emb", rand_emb(seed, i, dim))],
4035 ));
4036 }
4037 (fx, ids)
4038 }
4039
4040 #[test]
4043 fn vector_dim_reject_matches_unfiltered_and_oracle() {
4044 const N: u32 = 500;
4045 const SEED: u64 = 0xC0FF_EE00_D15C;
4046 let def = vec_rule();
4047
4048 let (mut fx_on, ids) = seed_docs(N, SEED);
4049 let mut eng_on = RuleEngine::new();
4050 {
4051 let mut g = fx_on.g();
4052 eng_on.create_rule(def.clone(), &mut g).unwrap();
4053 }
4054 let on = prov_pairs(&eng_on, "vec");
4055 assert!(!on.is_empty(), "seeded set must produce some edges");
4056
4057 let (mut fx_off, _) = seed_docs(N, SEED);
4058 let mut eng_off = RuleEngine::new();
4059 {
4060 let mut g = fx_off.g();
4061 with_vector_dim_reject(false, || {
4062 eng_off.create_rule(def.clone(), &mut g).unwrap();
4063 });
4064 }
4065 assert_eq!(on, prov_pairs(&eng_off, "vec"), "filter vs no-filter");
4066
4067 let mut brute = BTreeSet::new();
4068 for &s in &ids {
4069 for &d in &ids {
4070 if s == d {
4071 continue;
4072 }
4073 let skey = fx_on.ids.key_of(s).unwrap();
4074 let dkey = fx_on.ids.key_of(d).unwrap();
4075 let sget = |f: &str| fx_on.props.get(s, f).cloned();
4076 let dget = |f: &str| fx_on.props.get(d, f).cloned();
4077 if evaluate(
4078 &def.predicate,
4079 &NodeView {
4080 key: skey,
4081 props: &sget,
4082 },
4083 &NodeView {
4084 key: dkey,
4085 props: &dget,
4086 },
4087 )
4088 .is_some()
4089 {
4090 brute.insert((s, d));
4091 }
4092 }
4093 }
4094 assert_eq!(on, brute, "filter vs brute-force evaluate");
4095 }
4096
4097 #[test]
4100 fn vector_dim_change_updates_cache_and_matches_fresh_build() {
4101 let mut fx = Fx::new();
4102 let a = fx.add("Doc", "a", vec![("emb", emb_val(&[1.0, 0.0]))]);
4103 let b = fx.add("Doc", "b", vec![("emb", emb_val(&[1.0, 0.0]))]);
4104 let c = fx.add("Doc", "c", vec![("emb", emb_val(&[1.0, 0.0, 0.0]))]);
4105 let mut eng = RuleEngine::new();
4106 {
4107 let mut g = fx.g();
4108 eng.create_rule(vec_rule(), &mut g).unwrap();
4109 }
4110 assert_eq!(eng.indexes["vec"].src_side.vec_dim(a), Some(2));
4111 assert_eq!(eng.indexes["vec"].src_side.vec_dim(c), Some(3));
4112 assert_eq!(prov_pairs(&eng, "vec"), BTreeSet::from([(a, b), (b, a)]));
4113
4114 let old = fx.props.get(b, "emb").cloned();
4115 fx.props.set(b, "emb", emb_val(&[1.0, 0.0, 0.0]));
4116 {
4117 let mut g = fx.g();
4118 eng.on_node_changed(b, Some(("emb", old)), &mut g);
4119 }
4120 assert_eq!(eng.indexes["vec"].src_side.vec_dim(b), Some(3));
4121 assert_eq!(eng.indexes["vec"].dst_side.vec_dim(b), Some(3));
4122 let after = prov_pairs(&eng, "vec");
4123 assert_eq!(after, BTreeSet::from([(b, c), (c, b)]));
4124
4125 let mut fresh_fx = Fx::new();
4127 let fa = fresh_fx.add("Doc", "a", vec![("emb", emb_val(&[1.0, 0.0]))]);
4128 let fb = fresh_fx.add("Doc", "b", vec![("emb", emb_val(&[1.0, 0.0, 0.0]))]);
4129 let fc = fresh_fx.add("Doc", "c", vec![("emb", emb_val(&[1.0, 0.0, 0.0]))]);
4130 let mut fresh = RuleEngine::new();
4131 {
4132 let mut g = fresh_fx.g();
4133 fresh.create_rule(vec_rule(), &mut g).unwrap();
4134 }
4135 assert_eq!(
4136 prov_pairs(&fresh, "vec"),
4137 BTreeSet::from([(fb, fc), (fc, fb)])
4138 );
4139 assert_eq!(fresh.indexes["vec"].src_side.vec_dim(fb), Some(3));
4140 assert_eq!(fresh.indexes["vec"].src_side.vec_dim(fa), Some(2));
4141 }
4142
4143 #[test]
4163 fn streaming_topk_order_identity_property_test() {
4164 fn reference_topk(rule: &RuleDef, k: u64, fx: &mut Fx) -> BTreeSet<(u32, u32)> {
4167 let mut idx = RuleIndex::default();
4168 for id in 0..fx.ids.len() as u32 {
4169 let label_sym = match fx.labels.get(id as usize).copied() {
4170 Some(s) if s != u32::MAX => s,
4171 _ => continue,
4172 };
4173 index_node_for_rule(
4174 id,
4175 label_sym,
4176 rule,
4177 &mut idx,
4178 &fx.syms,
4179 ColumnsView::owned(&fx.props),
4180 );
4181 }
4182 let src_sym = fx.syms.get(&rule.src_label);
4183 let mut out = BTreeSet::new();
4184 let ids_snap: Vec<u32> = (0..fx.ids.len() as u32).collect();
4185 for id in ids_snap {
4186 let label_sym = match fx.labels.get(id as usize).copied() {
4187 Some(s) if s != u32::MAX => s,
4188 _ => continue,
4189 };
4190 if src_sym != Some(label_sym) {
4191 continue;
4192 }
4193 let g = GraphMut {
4194 ids: &fx.ids,
4195 syms: &mut fx.syms,
4196 labels: &fx.labels,
4197 props: ColumnsView::owned(&fx.props),
4198 topo: &mut fx.topo,
4199 edge_props: &mut fx.eprops,
4200 };
4201 let per_src = compute_desired(rule, &idx, id, true, &g);
4202 let mut candidates: Vec<((u32, u32), f64)> = per_src.into_iter().collect();
4204 candidates.sort_by(|&((_, da), sa), &((_, db), sb)| {
4205 sb.total_cmp(&sa).then_with(|| {
4206 let ka = fx.ids.key_of(da).unwrap_or("");
4207 let kb = fx.ids.key_of(db).unwrap_or("");
4208 ka.cmp(kb)
4209 })
4210 });
4211 candidates.truncate(k as usize);
4212 out.extend(candidates.into_iter().map(|(k, _)| k));
4213 }
4214 out
4215 }
4216
4217 fn streaming_pairs(rule: RuleDef, fx: &mut Fx) -> BTreeSet<(u32, u32)> {
4219 let name = rule.name.clone();
4220 let mut eng = RuleEngine::new();
4221 eng.create_rule(rule, &mut fx.g()).unwrap();
4222 eng.provenance()
4223 .get(&name)
4224 .map(|s| s.iter().map(|&(_, a, b)| (a, b)).collect())
4225 .unwrap_or_default()
4226 }
4227
4228 for seed in [0u64, 1, 42, 0xDEAD_BEEF, 0x1234_5678, 99, 12_648_430, 7] {
4233 for k in [1u64, 2, 3, 5] {
4234 let rule = RuleDef {
4235 name: "eq".into(),
4236 src_label: "N".into(),
4237 dst_label: "N".into(),
4238 predicate: Predicate::FieldEqual { field: "k".into() },
4239 edge_type: "EQ".into(),
4240 weight_prop: None,
4241 max_edges: Some(k),
4242 approximate: false,
4243 via_label: None,
4244 via_edge: None,
4245 via_dir: None,
4246 };
4247
4248 let build = || {
4249 let mut fx = Fx::new();
4250 for i in 0..12u32 {
4251 let h = mix64(seed ^ (i as u64 + 1));
4252 let val = match h % 3 {
4253 0 => "a",
4254 1 => "b",
4255 _ => "c",
4256 };
4257 fx.add(
4258 "N",
4259 &format!("n{i:02}"),
4260 vec![("k", Value::Str(val.into()))],
4261 );
4262 }
4263 fx
4264 };
4265
4266 let expected = reference_topk(&rule, k, &mut build());
4267 let actual = streaming_pairs(rule, &mut build());
4268
4269 assert_eq!(
4270 expected, actual,
4271 "FieldEqual seed={seed} k={k}: streaming top-k must match brute-force top-k"
4272 );
4273 }
4274 }
4275
4276 for seed in [0u64, 1, 42, 7] {
4281 for k in [1u64, 2, 4] {
4282 let rule = RuleDef {
4283 name: "nw".into(),
4284 src_label: "S".into(),
4285 dst_label: "D".into(),
4286 predicate: Predicate::NumericWithin {
4287 field: "v".into(),
4288 tolerance: 10.0,
4289 },
4290 edge_type: "NEAR".into(),
4291 weight_prop: Some("score".into()),
4292 max_edges: Some(k),
4293 approximate: false,
4294 via_label: None,
4295 via_edge: None,
4296 via_dir: None,
4297 };
4298
4299 let build = || {
4300 let mut fx = Fx::new();
4301 for i in 0..6u32 {
4302 let h = mix64(seed ^ (i as u64 + 1));
4303 let v = (h % 20) as f64;
4304 fx.add("S", &format!("s{i}"), vec![("v", Value::Float(v))]);
4305 }
4306 for i in 0..8u32 {
4307 let h = mix64(seed ^ (i as u64 + 101));
4308 let v = (h % 20) as f64;
4309 fx.add("D", &format!("d{i}"), vec![("v", Value::Float(v))]);
4310 }
4311 fx
4312 };
4313
4314 let expected = reference_topk(&rule, k, &mut build());
4315 let actual = streaming_pairs(rule, &mut build());
4316
4317 assert_eq!(
4318 expected, actual,
4319 "NumericWithin seed={seed} k={k}: streaming top-k must match brute-force top-k"
4320 );
4321 }
4322 }
4323
4324 for seed in [0u64, 1, 42, 7] {
4331 for k in [1u64, 2] {
4332 let rule = RuleDef {
4333 name: "fk".into(),
4334 src_label: "T".into(),
4335 dst_label: "C".into(),
4336 predicate: Predicate::KeyMatch {
4337 field: "cid".into(),
4338 },
4339 edge_type: "AT".into(),
4340 weight_prop: None,
4341 max_edges: Some(k),
4342 approximate: false,
4343 via_label: None,
4344 via_edge: None,
4345 via_dir: None,
4346 };
4347
4348 let build = || {
4349 let mut fx = Fx::new();
4350 for i in 0..4u32 {
4352 fx.add("C", &format!("c{i}"), vec![]);
4353 }
4354 for i in 0..8u32 {
4356 let h = mix64(seed ^ (i as u64 + 1));
4357 let cid = format!("c{}", h % 4);
4358 fx.add("T", &format!("t{i}"), vec![("cid", Value::Str(cid))]);
4359 }
4360 fx
4361 };
4362
4363 let expected = reference_topk(&rule, k, &mut build());
4364 let actual = streaming_pairs(rule, &mut build());
4365
4366 assert_eq!(
4367 expected, actual,
4368 "KeyMatch seed={seed} k={k}: streaming top-k must match brute-force top-k"
4369 );
4370 }
4371 }
4372
4373 {
4379 let cluster_a: &[(&str, f64, f64)] = &[
4381 ("va0", 1.0_f64, 0.0_f64),
4382 ("va1", 0.98_f64, 0.199_f64), ("va2", 0.97_f64, 0.243_f64), ];
4385 let cluster_b: &[(&str, f64, f64)] = &[
4386 ("vb0", 0.0_f64, 1.0_f64),
4387 ("vb1", 0.1_f64, 0.995_f64),
4388 ("vb2", 0.05_f64, 0.999_f64),
4389 ];
4390 for k in [1u64, 2] {
4391 let rule = RuleDef {
4392 name: "vsim".into(),
4393 src_label: "V".into(),
4394 dst_label: "V".into(),
4395 predicate: Predicate::VectorSimilar {
4396 field: "emb".into(),
4397 min: 0.9,
4398 },
4399 edge_type: "VSIM".into(),
4400 weight_prop: Some("score".into()),
4401 max_edges: Some(k),
4402 approximate: false,
4403 via_label: None,
4404 via_edge: None,
4405 via_dir: None,
4406 };
4407
4408 let build = || {
4409 let mut fx = Fx::new();
4410 let mut add_v = |key: &str, x: f64, y: f64| {
4411 let norm = (x * x + y * y).sqrt();
4412 let v = Value::List(vec![Value::Float(x / norm), Value::Float(y / norm)]);
4413 fx.add("V", key, vec![("emb", v)]);
4414 };
4415 for &(k, x, y) in cluster_a.iter().chain(cluster_b.iter()) {
4416 add_v(k, x, y);
4417 }
4418 fx
4419 };
4420
4421 let expected = reference_topk(&rule, k, &mut build());
4422 let actual = streaming_pairs(rule, &mut build());
4423
4424 assert_eq!(
4425 expected, actual,
4426 "VectorSimilar/ScanAll k={k}: streaming top-k must match brute-force top-k"
4427 );
4428 }
4429 }
4430 }
4431
4432 #[test]
4458 #[ignore]
4459 fn streaming_peak_transient_bound() {
4460 use std::sync::{
4461 atomic::{AtomicBool, AtomicU64, Ordering},
4462 Arc,
4463 };
4464
4465 fn peak_rss_during<F: FnOnce()>(f: F) -> u64 {
4468 let done = Arc::new(AtomicBool::new(false));
4469 let peak = Arc::new(AtomicU64::new(0));
4470 let done2 = done.clone();
4471 let peak2 = peak.clone();
4472 let pid = std::process::id().to_string();
4473
4474 let handle = std::thread::spawn(move || {
4475 while !done2.load(Ordering::Relaxed) {
4476 let rss = std::process::Command::new("ps")
4477 .args(["-o", "rss=", "-p", &pid])
4478 .output()
4479 .ok()
4480 .and_then(|o| String::from_utf8(o.stdout).ok())
4481 .and_then(|s| s.trim().parse::<u64>().ok())
4482 .unwrap_or(0)
4483 * 1024;
4484 peak2.fetch_max(rss, Ordering::Relaxed);
4485 std::thread::sleep(std::time::Duration::from_millis(1));
4486 }
4487 });
4488
4489 f();
4490
4491 done.store(true, Ordering::Relaxed);
4492 let _ = handle.join();
4493 peak.load(Ordering::Relaxed)
4494 }
4495
4496 let mut fx = Fx::new();
4500 for i in 0..500u32 {
4501 fx.add(
4502 "Talent",
4503 &format!("t{i}"),
4504 vec![("k", Value::Str("same".into()))],
4505 );
4506 }
4507 for i in 0..500u32 {
4508 fx.add(
4509 "Company",
4510 &format!("c{i}"),
4511 vec![("k", Value::Str("same".into()))],
4512 );
4513 }
4514 let rule = RuleDef {
4515 name: "eq_tc".into(),
4516 src_label: "Talent".into(),
4517 dst_label: "Company".into(),
4518 predicate: Predicate::FieldEqual { field: "k".into() },
4519 edge_type: "EQ".into(),
4520 weight_prop: None,
4521 max_edges: Some(2), approximate: false,
4523 via_label: None,
4524 via_edge: None,
4525 via_dir: None,
4526 };
4527
4528 let pid = std::process::id().to_string();
4530 let baseline = std::process::Command::new("ps")
4531 .args(["-o", "rss=", "-p", &pid])
4532 .output()
4533 .ok()
4534 .and_then(|o| String::from_utf8(o.stdout).ok())
4535 .and_then(|s| s.trim().parse::<u64>().ok())
4536 .unwrap_or(0)
4537 * 1024;
4538
4539 let mut eng = RuleEngine::new();
4540 let peak = peak_rss_during(|| {
4541 eng.create_rule(rule, &mut fx.g()).unwrap();
4542 });
4543
4544 let peak_delta = peak.saturating_sub(baseline);
4545
4546 assert!(
4550 peak_delta < 3 * 1024 * 1024,
4551 "peak transient delta {} bytes ({} KiB) exceeded 3 MiB; \
4552 streaming path may be building the full pairs map",
4553 peak_delta,
4554 peak_delta / 1024
4555 );
4556 assert_eq!(eng.provenance()["eq_tc"].len(), 1_000); assert!(!eng.is_tripped("eq_tc")); eprintln!(
4559 "streaming_peak_transient_bound: baseline={baseline} peak={peak} \
4560 delta={peak_delta} bytes ({} KiB)",
4561 peak_delta / 1024
4562 );
4563 }
4564
4565 fn near_threshold_pair(dim: usize, min: f64) -> (Vec<f64>, Vec<f64>) {
4572 let cos_target = min + 1e-6; let sin_small = (1.0 - cos_target * cos_target).sqrt();
4576 let mut a = vec![0.0f64; dim];
4577 a[0] = 1.0;
4578 let mut b = vec![0.0f64; dim];
4579 b[0] = cos_target;
4580 if dim > 1 {
4581 b[1] = sin_small;
4582 }
4583 (a, b)
4584 }
4585
4586 fn emb_val2(xs: &[f64]) -> Value {
4587 Value::List(xs.iter().copied().map(Value::Float).collect())
4588 }
4589
4590 fn make_early_exit_fixture(seed: u64, min: f64) -> (Fx, Vec<u32>, usize, usize) {
4594 let dims = [2usize, 4, 8, 16];
4595 let n = 100u32;
4596 let mut fx = Fx::new();
4597 let mut ids = Vec::new();
4598 for i in 0..n {
4599 let dim = dims[(i as usize) % dims.len()];
4600 let emb = rand_emb(seed, i, dim);
4601 ids.push(fx.add("Doc", &format!("d{i}"), vec![("emb", emb)]));
4602 }
4603 let (va, vb) = near_threshold_pair(8, min);
4605 let nt_a = fx.add("Doc", "nt_a", vec![("emb", emb_val2(&va))]);
4606 let nt_b = fx.add("Doc", "nt_b", vec![("emb", emb_val2(&vb))]);
4607 ids.push(nt_a);
4608 ids.push(nt_b);
4609 (fx, ids, nt_a as usize, nt_b as usize)
4610 }
4611
4612 #[test]
4616 fn vector_early_exit_identity_proof() {
4617 const SEED: u64 = 0xEA_4E_5A;
4618 const MIN: f64 = 0.85;
4619
4620 let def = RuleDef {
4621 name: "vec".into(),
4622 src_label: "Doc".into(),
4623 dst_label: "Doc".into(),
4624 predicate: Predicate::VectorSimilar {
4625 field: "emb".into(),
4626 min: MIN,
4627 },
4628 edge_type: "SIM".into(),
4629 weight_prop: Some("score".into()),
4630 max_edges: None,
4631 approximate: false,
4632 via_label: None,
4633 via_edge: None,
4634 via_dir: None,
4635 };
4636
4637 let (mut fx_on, ids, nt_a, nt_b) = make_early_exit_fixture(SEED, MIN);
4639 let (mut fx_off, _, _, _) = make_early_exit_fixture(SEED, MIN);
4640 let (fx_oracle, _, _, _) = make_early_exit_fixture(SEED, MIN);
4641
4642 let nt_a = nt_a as u32;
4643 let nt_b = nt_b as u32;
4644
4645 let mut eng_on = RuleEngine::new();
4647 {
4648 let mut g = fx_on.g();
4649 eng_on.create_rule(def.clone(), &mut g).unwrap();
4650 }
4651 let edges_on = prov_pairs(&eng_on, "vec");
4652 assert!(!edges_on.is_empty(), "should produce some edges");
4653
4654 assert!(
4656 edges_on.contains(&(nt_a, nt_b)),
4657 "near-threshold pair nt_a→nt_b must match with early-exit ON"
4658 );
4659 assert!(
4660 edges_on.contains(&(nt_b, nt_a)),
4661 "near-threshold pair nt_b→nt_a must match with early-exit ON"
4662 );
4663
4664 let mut eng_off = RuleEngine::new();
4666 {
4667 let mut g = fx_off.g();
4668 with_vector_early_exit(false, || {
4669 eng_off.create_rule(def.clone(), &mut g).unwrap();
4670 });
4671 }
4672 let edges_off = prov_pairs(&eng_off, "vec");
4673 assert_eq!(
4674 edges_on, edges_off,
4675 "early-exit ON vs OFF must produce identical edges"
4676 );
4677
4678 let mut oracle = BTreeSet::new();
4680 for &s in &ids {
4681 for &d in &ids {
4682 if s == d {
4683 continue;
4684 }
4685 let skey = fx_oracle.ids.key_of(s).unwrap();
4686 let dkey = fx_oracle.ids.key_of(d).unwrap();
4687 let sg = |f: &str| fx_oracle.props.get(s, f).cloned();
4688 let dg = |f: &str| fx_oracle.props.get(d, f).cloned();
4689 if evaluate(
4690 &def.predicate,
4691 &NodeView {
4692 key: skey,
4693 props: &sg,
4694 },
4695 &NodeView {
4696 key: dkey,
4697 props: &dg,
4698 },
4699 )
4700 .is_some()
4701 {
4702 oracle.insert((s, d));
4703 }
4704 }
4705 }
4706 assert_eq!(
4707 edges_on, oracle,
4708 "early-exit ON vs brute-force oracle must be identical"
4709 );
4710 }
4711
4712 #[test]
4715 fn vector_early_exit_checkpoint_coherence() {
4716 let mut fx = Fx::new();
4717 let a = fx.add("Doc", "a", vec![("emb", emb_val(&[1.0, 0.0, 0.0, 0.0]))]);
4719 let b = fx.add("Doc", "b", vec![("emb", emb_val(&[1.0, 0.0, 0.0, 0.0]))]);
4720 let c = fx.add(
4722 "Doc",
4723 "c",
4724 vec![("emb", emb_val(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]))],
4725 );
4726 let def = RuleDef {
4727 name: "vec".into(),
4728 src_label: "Doc".into(),
4729 dst_label: "Doc".into(),
4730 predicate: Predicate::VectorSimilar {
4731 field: "emb".into(),
4732 min: 0.9,
4733 },
4734 edge_type: "SIM".into(),
4735 weight_prop: None,
4736 max_edges: None,
4737 approximate: false,
4738 via_label: None,
4739 via_edge: None,
4740 via_dir: None,
4741 };
4742
4743 let mut eng = RuleEngine::new();
4744 {
4745 let mut g = fx.g();
4746 eng.create_rule(def.clone(), &mut g).unwrap();
4747 }
4748
4749 assert!(
4751 eng.indexes["vec"].src_side.vec_ckpts(a).is_some(),
4752 "a must have src checkpoints"
4753 );
4754 assert!(
4755 eng.indexes["vec"].dst_side.vec_ckpts(b).is_some(),
4756 "b must have dst checkpoints"
4757 );
4758 assert!(
4759 eng.indexes["vec"].src_side.vec_ckpts(c).is_some(),
4760 "c must have src checkpoints (dim=6)"
4761 );
4762
4763 let ckpts_a = *eng.indexes["vec"].src_side.vec_ckpts(a).unwrap();
4765 let norm_a = eng.indexes["vec"].src_side.vec_meta(a).unwrap().1;
4766 assert!(
4767 (ckpts_a[0] - norm_a).abs() < 1e-12,
4768 "ckpts[0] must equal the full L2 norm"
4769 );
4770
4771 assert_eq!(prov_pairs(&eng, "vec"), BTreeSet::from([(a, b), (b, a)]));
4773
4774 let old_b = fx.props.get(b, "emb").cloned();
4776 fx.props
4777 .set(b, "emb", emb_val(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]));
4778 {
4779 let mut g = fx.g();
4780 eng.on_node_changed(b, Some(("emb", old_b)), &mut g);
4781 }
4782 assert_eq!(eng.indexes["vec"].src_side.vec_dim(b), Some(6));
4784 assert_eq!(eng.indexes["vec"].dst_side.vec_dim(b), Some(6));
4785 assert!(eng.indexes["vec"].src_side.vec_ckpts(b).is_some());
4787 assert_eq!(prov_pairs(&eng, "vec"), BTreeSet::from([(b, c), (c, b)]));
4789
4790 let wrong_live = vec![2.0f64, 0.0, 0.0, 0.0, 0.0, 0.0]; let gate_result = eng.indexes["vec"].src_side.fresh_ckpts_for(b, &wrong_live);
4794 assert!(
4795 gate_result.is_none(),
4796 "freshness gate must reject a mismatched-norm live vector"
4797 );
4798
4799 let correct_live = vec![1.0f64, 0.0, 0.0, 0.0, 0.0, 0.0];
4801 let gate_result = eng.indexes["vec"]
4802 .src_side
4803 .fresh_ckpts_for(b, &correct_live);
4804 assert!(
4805 gate_result.is_some(),
4806 "freshness gate must accept the matching live vector"
4807 );
4808 }
4809
4810 #[test]
4819 fn vector_early_exit_razor_dim1536() {
4820 const MIN: f64 = 0.85;
4821 const DIM: usize = 1536;
4822 let target = MIN + 5e-13;
4824 let inv_sqrt = 1.0 / (DIM as f64).sqrt();
4825
4826 let a: Vec<f64> = vec![inv_sqrt; DIM];
4828
4829 let perp_scale = (1.0 - target * target).sqrt() / (2.0f64).sqrt();
4835 let mut b: Vec<f64> = vec![target * inv_sqrt; DIM];
4836 b[0] += perp_scale;
4837 b[1] -= perp_scale;
4838
4839 let def = RuleDef {
4840 name: "razor".into(),
4841 src_label: "Doc".into(),
4842 dst_label: "Doc".into(),
4843 predicate: Predicate::VectorSimilar {
4844 field: "emb".into(),
4845 min: MIN,
4846 },
4847 edge_type: "SIM".into(),
4848 weight_prop: None,
4849 max_edges: None,
4850 approximate: false,
4851 via_label: None,
4852 via_edge: None,
4853 via_dir: None,
4854 };
4855
4856 let build_fx = || {
4858 let mut fx = Fx::new();
4859 let na = fx.add("Doc", "razor_a", vec![("emb", emb_val2(&a))]);
4860 let nb = fx.add("Doc", "razor_b", vec![("emb", emb_val2(&b))]);
4861 (fx, na, nb)
4862 };
4863
4864 let (mut fx_on, na, nb) = build_fx();
4865 let (mut fx_off, _, _) = build_fx();
4866 let (fx_oracle, _, _) = build_fx();
4867
4868 let mut eng_on = RuleEngine::new();
4870 {
4871 let mut g = fx_on.g();
4872 eng_on.create_rule(def.clone(), &mut g).unwrap();
4873 }
4874 let edges_on = prov_pairs(&eng_on, "razor");
4875 assert!(
4876 edges_on.contains(&(na, nb)),
4877 "razor pair razor_a→razor_b must be present with early-exit ON (cos={target:.15}, min={MIN})"
4878 );
4879 assert!(
4880 edges_on.contains(&(nb, na)),
4881 "razor pair razor_b→razor_a must be present with early-exit ON"
4882 );
4883
4884 let mut eng_off = RuleEngine::new();
4886 {
4887 let mut g = fx_off.g();
4888 with_vector_early_exit(false, || {
4889 eng_off.create_rule(def.clone(), &mut g).unwrap();
4890 });
4891 }
4892 let edges_off = prov_pairs(&eng_off, "razor");
4893 assert_eq!(
4894 edges_on, edges_off,
4895 "razor dim=1536: early-exit ON vs OFF must produce identical edges"
4896 );
4897
4898 let ids = [na, nb];
4900 let mut oracle = BTreeSet::new();
4901 for &s in &ids {
4902 for &d in &ids {
4903 if s == d {
4904 continue;
4905 }
4906 let skey = fx_oracle.ids.key_of(s).unwrap();
4907 let dkey = fx_oracle.ids.key_of(d).unwrap();
4908 let sg = |f: &str| fx_oracle.props.get(s, f).cloned();
4909 let dg = |f: &str| fx_oracle.props.get(d, f).cloned();
4910 if evaluate(
4911 &def.predicate,
4912 &NodeView {
4913 key: skey,
4914 props: &sg,
4915 },
4916 &NodeView {
4917 key: dkey,
4918 props: &dg,
4919 },
4920 )
4921 .is_some()
4922 {
4923 oracle.insert((s, d));
4924 }
4925 }
4926 }
4927 assert_eq!(
4928 edges_on, oracle,
4929 "razor dim=1536: early-exit ON vs brute-force oracle must be identical"
4930 );
4931 }
4932
4933 fn field_equal_rule(
4977 src_label: &str,
4978 dst_label: &str,
4979 field: &str,
4980 edge_type: &str,
4981 max_edges: Option<u64>,
4982 ) -> RuleDef {
4983 RuleDef {
4984 name: format!("{src_label}_{dst_label}_{field}"),
4985 src_label: src_label.into(),
4986 dst_label: dst_label.into(),
4987 predicate: Predicate::FieldEqual {
4988 field: field.into(),
4989 },
4990 edge_type: edge_type.into(),
4991 weight_prop: None,
4992 max_edges,
4993 approximate: false,
4994 via_label: None,
4995 via_edge: None,
4996 via_dir: None,
4997 }
4998 }
4999
5000 #[test]
5001 fn backfill_does_not_materialize_the_cross_product() {
5002 use std::sync::atomic::Ordering;
5003 let mut fx = Fx::new();
5008 for i in 0..400u32 {
5009 fx.add(
5010 "Person",
5011 &format!("p{i}"),
5012 vec![("city", Value::Str("austin".into()))],
5013 );
5014 }
5015 for i in 0..400u32 {
5016 fx.add(
5017 "Org",
5018 &format!("o{i}"),
5019 vec![("city", Value::Str("austin".into()))],
5020 );
5021 }
5022
5023 let mut eng = RuleEngine::new();
5024 PEAK_DESIRED_PAIRS.store(0, Ordering::Relaxed);
5025 {
5026 let mut g = fx.g();
5027 eng.create_rule(
5028 field_equal_rule("Person", "Org", "city", "IN_CITY", Some(5)),
5029 &mut g,
5030 )
5031 .unwrap();
5032 }
5033
5034 let edges = fx.topo.edge_count();
5035 assert_eq!(
5036 edges,
5037 400 * 5,
5038 "per-source top-k must yield exactly k per source"
5039 );
5040
5041 let peak = PEAK_DESIRED_PAIRS.load(Ordering::Relaxed);
5042 assert!(
5043 peak <= 400 * 5 * 4, "backfill must not materialize the full cross-product; peak was {peak}"
5045 );
5046 }
5047
5048 #[test]
5064 fn global_budget_backfill_stays_per_source_bounded() {
5065 use std::sync::atomic::Ordering;
5066 let mut fx = Fx::new();
5069 for i in 0..400u32 {
5070 fx.add(
5071 "Person",
5072 &format!("p{i}"),
5073 vec![("city", Value::Str("austin".into()))],
5074 );
5075 }
5076 for i in 0..400u32 {
5077 fx.add(
5078 "Org",
5079 &format!("o{i}"),
5080 vec![("city", Value::Str("austin".into()))],
5081 );
5082 }
5083
5084 let mut eng = RuleEngine::new();
5085 PEAK_DESIRED_PAIRS.store(0, Ordering::Relaxed);
5086 {
5087 let mut g = fx.g();
5088 eng.create_rule(
5089 field_equal_rule("Person", "Org", "city", "IN_CITY", None),
5090 &mut g,
5091 )
5092 .unwrap();
5093 }
5094
5095 let edges = fx.topo.edge_count();
5098 assert_eq!(
5099 edges,
5100 400 * 400,
5101 "none-path must apply all pairs when under budget; got {edges}"
5102 );
5103
5104 let peak = PEAK_DESIRED_PAIRS.load(Ordering::Relaxed);
5108 assert!(
5109 peak <= 400 * 4, "none-path backfill must not accumulate a global desired-map; peak was {peak}"
5111 );
5112 }
5113}