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