1use core_storage::{list_tokens, Value, ValueKey};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeSet;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub struct RuleDef {
7 pub name: String,
8 pub src_label: String,
9 pub dst_label: String,
10 pub predicate: Predicate,
11 pub edge_type: String,
12 pub weight_prop: Option<String>,
13 pub max_edges: Option<u64>,
21 #[serde(default)]
37 pub approximate: bool,
38 #[serde(default)]
56 pub via_label: Option<String>,
57 #[serde(default)]
61 pub via_edge: Option<String>,
62 #[serde(default)]
67 pub via_dir: Option<core_storage::Direction>,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum Predicate {
86 KeyMatch {
87 field: String,
88 },
89 FieldEqual {
90 field: String,
91 },
92 Overlap {
93 field: String,
94 min: f64,
95 },
96 All(Vec<Predicate>),
97 NumericWithin {
99 field: String,
100 tolerance: f64,
101 },
102 GeoRadius {
103 field: String,
104 km: f64,
105 },
106 VectorSimilar {
107 field: String,
108 min: f64,
109 },
110 Any(Vec<Predicate>),
114}
115
116pub struct NodeView<'a> {
117 pub key: &'a str,
118 pub props: &'a dyn Fn(&str) -> Option<Value>,
119}
120
121impl RuleDef {
122 pub fn validate(&self) -> Result<(), String> {
123 for (what, s) in [
124 ("name", &self.name),
125 ("src_label", &self.src_label),
126 ("dst_label", &self.dst_label),
127 ("edge_type", &self.edge_type),
128 ] {
129 if s.is_empty() {
130 return Err(format!("{what} must not be empty"));
131 }
132 }
133 validate_pred(&self.predicate)?;
134 let depth = predicate_nesting_depth(&self.predicate);
135 if depth > MAX_PREDICATE_NESTING_DEPTH {
136 return Err(format!(
137 "predicate nesting depth {depth} exceeds cap of \
138 {MAX_PREDICATE_NESTING_DEPTH}"
139 ));
140 }
141 if self.approximate && !predicate_is_vector_similar_rooted(&self.predicate) {
142 return Err(
143 "approximate=true requires a VectorSimilar-rooted predicate \
144 (VectorSimilar, or All whose first element is VectorSimilar)"
145 .into(),
146 );
147 }
148 if self.via_label.is_some() && self.approximate {
149 return Err("via-hop rules do not support approximate: true".into());
150 }
151 match (&self.via_label, &self.via_edge) {
153 (Some(_), None) | (None, Some(_)) => {
154 return Err("via_label and via_edge must both be set or both absent".into());
155 }
156 (Some(l), Some(e)) => {
157 if l.is_empty() {
158 return Err("via_label must not be empty".into());
159 }
160 if e.is_empty() {
161 return Err("via_edge must not be empty".into());
162 }
163 }
164 (None, None) => {}
165 }
166 Ok(())
167 }
168
169 pub fn watched_fields(&self) -> BTreeSet<String> {
170 let mut out = BTreeSet::new();
171 collect_fields(&self.predicate, &mut out);
172 out
173 }
174}
175
176pub const MAX_PREDICATE_NESTING_DEPTH: usize = 4;
183
184pub const DEFAULT_SCORED_TOP_K: u64 = 32;
186
187pub const DEFAULT_KEYMATCH_TOP_K: u64 = 1;
189
190pub fn is_keymatch_rooted(p: &Predicate) -> bool {
193 match p {
194 Predicate::KeyMatch { .. } => true,
195 Predicate::All(parts) => !parts.is_empty() && is_keymatch_rooted(&parts[0]),
196 Predicate::Any(_) => false,
197 _ => false,
198 }
199}
200
201pub fn default_max_edges(predicate: &Predicate) -> u64 {
203 if is_keymatch_rooted(predicate) {
204 DEFAULT_KEYMATCH_TOP_K
205 } else {
206 DEFAULT_SCORED_TOP_K
207 }
208}
209
210pub fn predicate_is_vector_similar_rooted(p: &Predicate) -> bool {
214 match p {
215 Predicate::VectorSimilar { .. } => true,
216 Predicate::All(parts) => {
217 !parts.is_empty() && matches!(parts[0], Predicate::VectorSimilar { .. })
218 }
219 Predicate::Any(_) => false,
220 _ => false,
221 }
222}
223
224fn predicate_nesting_depth(p: &Predicate) -> usize {
230 match p {
231 Predicate::All(parts) | Predicate::Any(parts) => {
232 1 + parts.iter().map(predicate_nesting_depth).max().unwrap_or(0)
233 }
234 _ => 0,
235 }
236}
237
238fn validate_pred(p: &Predicate) -> Result<(), String> {
239 match p {
240 Predicate::KeyMatch { field } | Predicate::FieldEqual { field } => {
241 if field.is_empty() {
242 Err("field must not be empty".into())
243 } else {
244 Ok(())
245 }
246 }
247 Predicate::Overlap { field, min } => {
248 if field.is_empty() {
249 Err("field must not be empty".into())
250 } else if !(*min > 0.0 && *min <= 1.0) {
251 Err(format!("overlap min must be in (0,1], got {min}"))
252 } else {
253 Ok(())
254 }
255 }
256 Predicate::NumericWithin { field, tolerance } => {
257 if field.is_empty() {
258 Err("field must not be empty".into())
259 } else if !(tolerance.is_finite() && *tolerance >= 0.0) {
260 Err(format!(
261 "numeric_within tolerance must be finite and >= 0, got {tolerance}"
262 ))
263 } else {
264 Ok(())
265 }
266 }
267 Predicate::GeoRadius { field, km } => {
268 if field.is_empty() {
269 Err("field must not be empty".into())
270 } else if !(km.is_finite() && *km > 0.0) {
271 Err(format!("geo_radius km must be finite and > 0, got {km}"))
272 } else {
273 Ok(())
274 }
275 }
276 Predicate::VectorSimilar { field, min } => {
277 if field.is_empty() {
278 Err("field must not be empty".into())
279 } else if !(*min > 0.0 && *min <= 1.0) {
280 Err(format!("vector_similar min must be in (0,1], got {min}"))
281 } else {
282 Ok(())
283 }
284 }
285 Predicate::All(parts) => {
286 if parts.is_empty() {
287 return Err("all() must have at least one predicate".into());
288 }
289 parts.iter().try_for_each(validate_pred)
290 }
291 Predicate::Any(parts) => {
292 if parts.is_empty() {
293 return Err("any() must have at least one predicate".into());
294 }
295 parts.iter().try_for_each(validate_pred)
296 }
297 }
298}
299
300fn collect_fields(p: &Predicate, out: &mut BTreeSet<String>) {
301 match p {
302 Predicate::KeyMatch { field }
303 | Predicate::FieldEqual { field }
304 | Predicate::Overlap { field, .. }
305 | Predicate::NumericWithin { field, .. }
306 | Predicate::GeoRadius { field, .. }
307 | Predicate::VectorSimilar { field, .. } => {
308 out.insert(field.clone());
309 }
310 Predicate::All(parts) | Predicate::Any(parts) => {
311 parts.iter().for_each(|q| collect_fields(q, out))
312 }
313 }
314}
315
316pub fn evaluate(pred: &Predicate, src: &NodeView, dst: &NodeView) -> Option<f64> {
317 match pred {
318 Predicate::KeyMatch { field } => match (src.props)(field)? {
319 Value::Str(s) if s == dst.key => Some(1.0),
320 _ => None,
321 },
322 Predicate::FieldEqual { field } => {
323 let a = ValueKey::from_value(&(src.props)(field)?)?;
324 let b = ValueKey::from_value(&(dst.props)(field)?)?;
325 (a == b).then_some(1.0)
326 }
327 Predicate::Overlap { field, min } => {
328 let a = list_tokens(&(src.props)(field)?)?;
329 let b = list_tokens(&(dst.props)(field)?)?;
330 let inter = a.intersection(&b).count();
331 let union = a.union(&b).count();
332 if union == 0 || inter == 0 {
333 return None;
334 }
335 let j = inter as f64 / union as f64;
336 (j >= *min).then_some(j)
337 }
338 Predicate::All(parts) => {
339 if parts.is_empty() {
341 return None;
342 }
343 let mut score = f64::INFINITY;
344 for part in parts {
345 score = score.min(evaluate(part, src, dst)?);
346 }
347 Some(score)
348 }
349 Predicate::Any(parts) => {
350 let mut best: Option<f64> = None;
354 for part in parts {
355 if let Some(s) = evaluate(part, src, dst) {
356 best = Some(match best {
357 None => s,
358 Some(prev) => prev.max(s),
359 });
360 }
361 }
362 best
363 }
364 Predicate::NumericWithin { field, tolerance } => {
365 if !tolerance.is_finite() || *tolerance < 0.0 {
369 return None;
370 }
371 let a = as_finite_f64(&(src.props)(field)?)?;
372 let b = as_finite_f64(&(dst.props)(field)?)?;
373 let delta = (a - b).abs();
374 if *tolerance == 0.0 {
375 return (delta == 0.0).then_some(1.0);
376 }
377 (delta <= *tolerance).then_some(1.0 - delta / *tolerance)
378 }
379 Predicate::GeoRadius { field, km } => {
380 if !km.is_finite() || *km <= 0.0 {
381 return None;
382 }
383 let (alat, alon) = as_latlon(&(src.props)(field)?)?;
384 let (blat, blon) = as_latlon(&(dst.props)(field)?)?;
385 let d = haversine_km(alat, alon, blat, blon);
386 if !d.is_finite() {
387 return None;
388 }
389 (d <= *km).then_some(1.0 - d / *km)
390 }
391 Predicate::VectorSimilar { field, min } => {
392 let a = as_numeric_list(&(src.props)(field)?)?;
393 let b = as_numeric_list(&(dst.props)(field)?)?;
394 if a.len() != b.len() {
395 return None;
396 }
397 let cos = cosine(&a, &b)?.min(1.0);
398 (cos >= *min).then_some(cos)
399 }
400 }
401}
402
403fn as_finite_f64(v: &Value) -> Option<f64> {
404 match v {
405 Value::Int(i) => Some(*i as f64),
406 Value::Float(f) if f.is_finite() => Some(*f),
407 _ => None,
408 }
409}
410
411fn as_latlon(v: &Value) -> Option<(f64, f64)> {
412 let Value::List(items) = v else {
413 return None;
414 };
415 if items.len() != 2 {
416 return None;
417 }
418 let lat = as_finite_f64(&items[0])?;
419 let lon = as_finite_f64(&items[1])?;
420 if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lon) {
421 Some((lat, lon))
422 } else {
423 None
424 }
425}
426
427fn as_numeric_list(v: &Value) -> Option<Vec<f64>> {
428 let Value::List(items) = v else {
429 return None;
430 };
431 if items.is_empty() {
432 return None;
433 }
434 items.iter().map(as_finite_f64).collect()
435}
436
437const EARTH_RADIUS_KM: f64 = 6371.0088;
439
440fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
441 let phi1 = lat1.to_radians();
442 let phi2 = lat2.to_radians();
443 let dphi = (lat2 - lat1).to_radians();
444 let dlam = (lon2 - lon1).to_radians();
445 let a = ((dphi / 2.0).sin().powi(2) + phi1.cos() * phi2.cos() * (dlam / 2.0).sin().powi(2))
446 .clamp(0.0, 1.0);
447 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
448 EARTH_RADIUS_KM * c
449}
450
451fn cosine(a: &[f64], b: &[f64]) -> Option<f64> {
452 let mut dot = 0.0;
453 let mut na2 = 0.0;
454 let mut nb2 = 0.0;
455 for (x, y) in a.iter().zip(b.iter()) {
456 dot += *x * *y;
457 na2 += *x * *x;
458 nb2 += *y * *y;
459 }
460 let na = na2.sqrt();
461 let nb = nb2.sqrt();
462 if !(na > 0.0 && nb > 0.0) {
463 return None;
464 }
465 let cos = dot / (na * nb);
466 cos.is_finite().then_some(cos)
467}
468
469pub fn cosine_early_exit(
505 a: &[f64],
506 b: &[f64],
507 ckpts_a: &[f64; 8],
508 ckpts_b: &[f64; 8],
509 norm_a: f64,
510 norm_b: f64,
511 min: f64,
512) -> Option<f64> {
513 let dim = a.len();
514 if dim == 0 || dim != b.len() {
515 return None; }
517 let denom = norm_a * norm_b;
518 if !denom.is_finite() || denom == 0.0 {
519 return None;
520 }
521
522 let eps = dim as f64 * f64::EPSILON * 4.0;
528 let mut dot = 0.0f64;
529
530 for ci in 0..8usize {
531 let chunk_start = ci * dim / 8;
532 let chunk_end = if ci < 7 { (ci + 1) * dim / 8 } else { dim };
533 for k in chunk_start..chunk_end {
534 dot += a[k] * b[k];
535 }
536 if ci < 7 {
540 let bound = ckpts_a[ci + 1] * ckpts_b[ci + 1];
541 let cos_max = (dot + bound) / denom;
542 if cos_max.is_finite() && cos_max < min - eps {
543 return None;
544 }
545 }
546 }
547
548 let cos = (dot / denom).min(1.0);
550 if cos.is_finite() && cos >= min {
551 Some(cos)
552 } else {
553 None
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use core_storage::Value;
561 use std::collections::HashMap;
562
563 macro_rules! eval {
569 ($p:expr, ($sk:expr, $sm:ident) => ($dk:expr, $dm:ident)) => {{
570 let sp = |f: &str| $sm.get(f).cloned();
571 let dp = |f: &str| $dm.get(f).cloned();
572 evaluate(
573 $p,
574 &NodeView {
575 key: $sk,
576 props: &sp,
577 },
578 &NodeView {
579 key: $dk,
580 props: &dp,
581 },
582 )
583 }};
584 }
585
586 #[test]
587 fn key_match_links_fk_to_key() {
588 let s: HashMap<_, _> = [("cid".to_string(), Value::Str("c1".into()))].into();
589 let d: HashMap<String, Value> = HashMap::new();
590 let p = Predicate::KeyMatch {
591 field: "cid".into(),
592 };
593 assert_eq!(eval!(&p, ("t1", s) => ("c1", d)), Some(1.0));
594 assert_eq!(eval!(&p, ("t1", s) => ("c2", d)), None);
595 assert_eq!(eval!(&p, ("t1", d) => ("c1", d)), None); }
597
598 #[test]
599 fn field_equal_needs_both_scalars_equal() {
600 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
601 let b = a.clone();
602 let c: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
603 let p = Predicate::FieldEqual {
604 field: "ind".into(),
605 };
606 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
607 assert_eq!(eval!(&p, ("a", a) => ("c", c)), None);
608 }
609
610 #[test]
611 fn overlap_is_jaccard_with_threshold() {
612 let mk =
613 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
614 let a: HashMap<_, _> = [("tags".to_string(), mk(&["x", "y"]))].into();
615 let b: HashMap<_, _> = [("tags".to_string(), mk(&["y", "z"]))].into();
616 let p = Predicate::Overlap {
617 field: "tags".into(),
618 min: 0.3,
619 };
620 let score = eval!(&p, ("a", a) => ("b", b)).unwrap();
622 assert!((score - 1.0 / 3.0).abs() < 1e-9);
623 let strict = Predicate::Overlap {
624 field: "tags".into(),
625 min: 0.5,
626 };
627 assert_eq!(eval!(&strict, ("a", a) => ("b", b)), None);
628 let e: HashMap<_, _> = [("tags".to_string(), mk(&[]))].into();
630 assert_eq!(eval!(&p, ("a", e) => ("b", b)), None);
631 }
632
633 #[test]
634 fn all_takes_min_score_and_requires_every_part() {
635 let mk =
636 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
637 let a: HashMap<_, _> = [
638 ("ind".to_string(), Value::Str("arch".into())),
639 ("tags".to_string(), mk(&["x", "y"])),
640 ]
641 .into();
642 let b: HashMap<_, _> = [
643 ("ind".to_string(), Value::Str("arch".into())),
644 ("tags".to_string(), mk(&["y"])),
645 ]
646 .into();
647 let p = Predicate::All(vec![
648 Predicate::FieldEqual {
649 field: "ind".into(),
650 },
651 Predicate::Overlap {
652 field: "tags".into(),
653 min: 0.4,
654 },
655 ]);
656 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
657 assert!((s - 0.5).abs() < 1e-9); }
659
660 #[test]
661 fn validation_rejects_bad_rules_and_collects_watched_fields() {
662 let ok = RuleDef {
663 name: "r".into(),
664 src_label: "A".into(),
665 dst_label: "B".into(),
666 predicate: Predicate::All(vec![
667 Predicate::KeyMatch { field: "fk".into() },
668 Predicate::Overlap {
669 field: "tags".into(),
670 min: 0.5,
671 },
672 ]),
673 edge_type: "E".into(),
674 weight_prop: Some("score".into()),
675 max_edges: None,
676 approximate: false,
677 via_label: None,
678 via_edge: None,
679 via_dir: None,
680 };
681 assert!(ok.validate().is_ok());
682 assert_eq!(
683 ok.watched_fields().into_iter().collect::<Vec<_>>(),
684 vec!["fk".to_string(), "tags".to_string()]
685 );
686 let mut bad = ok.clone();
687 bad.predicate = Predicate::Overlap {
688 field: "t".into(),
689 min: 0.0,
690 };
691 assert!(bad.validate().is_err()); let mut bad2 = ok.clone();
693 bad2.edge_type = String::new();
694 assert!(bad2.validate().is_err());
695 let mut bad3 = ok;
696 bad3.predicate = Predicate::All(vec![]);
697 assert!(bad3.validate().is_err());
698 }
699
700 #[test]
701 fn evaluate_empty_all_returns_none() {
702 let empty: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
703 let sp = |f: &str| empty.get(f).cloned();
704 let dp = |f: &str| empty.get(f).cloned();
705 let src = NodeView {
706 key: "a",
707 props: &sp,
708 };
709 let dst = NodeView {
710 key: "b",
711 props: &dp,
712 };
713 assert_eq!(evaluate(&Predicate::All(vec![]), &src, &dst), None);
714 }
715
716 #[test]
717 fn numeric_within_int_float_cross_type() {
718 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
719 let b: HashMap<_, _> = [("year".to_string(), Value::Float(2000.0))].into();
720 let tight = Predicate::NumericWithin {
721 field: "year".into(),
722 tolerance: 2.0,
723 };
724 assert_eq!(eval!(&tight, ("a", a) => ("b", b)), Some(0.0));
726 let loose = Predicate::NumericWithin {
727 field: "year".into(),
728 tolerance: 3.0,
729 };
730 let score = eval!(&loose, ("a", a) => ("b", b)).unwrap();
731 assert!((score - 1.0 / 3.0).abs() < 1e-9);
732 }
733
734 #[test]
735 fn numeric_within_missing_or_non_numeric_is_none() {
736 let num: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
737 let missing: HashMap<String, Value> = HashMap::new();
738 let text: HashMap<_, _> = [("year".to_string(), Value::Str("1998".into()))].into();
739 let p = Predicate::NumericWithin {
740 field: "year".into(),
741 tolerance: 2.0,
742 };
743 assert_eq!(eval!(&p, ("a", num) => ("b", missing)), None);
744 assert_eq!(eval!(&p, ("a", missing) => ("b", num)), None);
745 assert_eq!(eval!(&p, ("a", num) => ("b", text)), None);
746 }
747
748 #[test]
749 fn numeric_within_tol_zero_requires_exact() {
750 let a: HashMap<_, _> = [("year".to_string(), Value::Int(1998))].into();
751 let same: HashMap<_, _> = [("year".to_string(), Value::Float(1998.0))].into();
752 let other: HashMap<_, _> = [("year".to_string(), Value::Int(1999))].into();
753 let p = Predicate::NumericWithin {
754 field: "year".into(),
755 tolerance: 0.0,
756 };
757 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
758 assert_eq!(eval!(&p, ("a", a) => ("b", other)), None);
759 }
760
761 #[test]
762 fn numeric_within_non_finite_is_none() {
763 let a: HashMap<_, _> = [("year".to_string(), Value::Float(f64::NAN))].into();
764 let b: HashMap<_, _> = [("year".to_string(), Value::Float(1.0))].into();
765 let inf: HashMap<_, _> = [("year".to_string(), Value::Float(f64::INFINITY))].into();
766 let p = Predicate::NumericWithin {
767 field: "year".into(),
768 tolerance: 2.0,
769 };
770 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
771 assert_eq!(eval!(&p, ("a", inf) => ("b", b)), None);
772 }
773
774 fn geo_pair(
775 src: (f64, f64),
776 dst: (f64, f64),
777 ) -> (HashMap<String, Value>, HashMap<String, Value>) {
778 let mk = |lat: f64, lon: f64| {
779 let mut m = HashMap::new();
780 m.insert(
781 "loc".to_string(),
782 Value::List(vec![Value::Float(lat), Value::Float(lon)]),
783 );
784 m
785 };
786 (mk(src.0, src.1), mk(dst.0, dst.1))
787 }
788
789 #[test]
790 fn geo_radius_paris_london() {
791 let (paris, london) = geo_pair((48.8566, 2.3522), (51.5074, -0.1278));
793 let inside = Predicate::GeoRadius {
794 field: "loc".into(),
795 km: 400.0,
796 };
797 let score = eval!(&inside, ("p", paris) => ("l", london)).unwrap();
798 assert!((score - 0.14125).abs() < 0.001);
800 let outside = Predicate::GeoRadius {
801 field: "loc".into(),
802 km: 300.0,
803 };
804 assert_eq!(eval!(&outside, ("p", paris) => ("l", london)), None);
805 }
806
807 #[test]
808 fn geo_radius_identical_coordinates_score_one() {
809 let (a, b) = geo_pair((48.8566, 2.3522), (48.8566, 2.3522));
810 let p = Predicate::GeoRadius {
811 field: "loc".into(),
812 km: 400.0,
813 };
814 assert_eq!(eval!(&p, ("a", a) => ("b", b)), Some(1.0));
815 }
816
817 #[test]
818 fn geo_radius_malformed_is_none() {
819 let paris: HashMap<_, _> = [(
820 "loc".to_string(),
821 Value::List(vec![Value::Float(48.8566), Value::Float(2.3522)]),
822 )]
823 .into();
824 let one: HashMap<_, _> =
825 [("loc".to_string(), Value::List(vec![Value::Float(48.8566)]))].into();
826 let three: HashMap<_, _> = [(
827 "loc".to_string(),
828 Value::List(vec![
829 Value::Float(48.8566),
830 Value::Float(2.3522),
831 Value::Float(0.0),
832 ]),
833 )]
834 .into();
835 let string_el: HashMap<_, _> = [(
836 "loc".to_string(),
837 Value::List(vec![Value::Str("48.8566".into()), Value::Float(2.3522)]),
838 )]
839 .into();
840 let lat91: HashMap<_, _> = [(
841 "loc".to_string(),
842 Value::List(vec![Value::Float(91.0), Value::Float(0.0)]),
843 )]
844 .into();
845 let p = Predicate::GeoRadius {
846 field: "loc".into(),
847 km: 400.0,
848 };
849 assert_eq!(eval!(&p, ("a", paris) => ("b", one)), None);
850 assert_eq!(eval!(&p, ("a", paris) => ("b", three)), None);
851 assert_eq!(eval!(&p, ("a", paris) => ("b", string_el)), None);
852 assert_eq!(eval!(&p, ("a", paris) => ("b", lat91)), None);
853 }
854
855 fn vec_field(vals: &[f64]) -> HashMap<String, Value> {
856 [(
857 "emb".to_string(),
858 Value::List(vals.iter().copied().map(Value::Float).collect()),
859 )]
860 .into()
861 }
862
863 #[test]
864 fn vector_similar_cosine_and_rejects() {
865 let a = vec_field(&[1.0, 0.0]);
866 let same = vec_field(&[1.0, 0.0]);
867 let ortho = vec_field(&[0.0, 1.0]);
868 let p = Predicate::VectorSimilar {
869 field: "emb".into(),
870 min: 0.5,
871 };
872 assert_eq!(eval!(&p, ("a", a) => ("b", same)), Some(1.0));
873 assert_eq!(eval!(&p, ("a", a) => ("b", ortho)), None); let u = vec_field(&[1.0, 2.0]);
876 let scaled = vec_field(&[2.0, 4.0]);
877 let score = eval!(&p, ("a", u) => ("b", scaled)).unwrap();
878 assert!((1.0 - score).abs() < 1e-9); let dim3 = vec_field(&[1.0, 0.0, 0.0]);
881 assert_eq!(eval!(&p, ("a", a) => ("b", dim3)), None);
882 let zero = vec_field(&[0.0, 0.0]);
883 assert_eq!(eval!(&p, ("a", a) => ("b", zero)), None);
884 }
885
886 #[test]
887 fn approximate_only_valid_with_vector_similar_rooted_predicate() {
888 let ok_vec = RuleDef {
890 name: "av".into(),
891 src_label: "V".into(),
892 dst_label: "V".into(),
893 predicate: Predicate::VectorSimilar {
894 field: "emb".into(),
895 min: 0.9,
896 },
897 edge_type: "VEC".into(),
898 weight_prop: None,
899 max_edges: None,
900 approximate: true,
901 via_label: None,
902 via_edge: None,
903 via_dir: None,
904 };
905 assert!(ok_vec.validate().is_ok());
906
907 let ok_all = RuleDef {
909 name: "av2".into(),
910 src_label: "V".into(),
911 dst_label: "V".into(),
912 predicate: Predicate::All(vec![
913 Predicate::VectorSimilar {
914 field: "emb".into(),
915 min: 0.9,
916 },
917 Predicate::FieldEqual {
918 field: "kind".into(),
919 },
920 ]),
921 edge_type: "VEC2".into(),
922 weight_prop: None,
923 max_edges: None,
924 approximate: true,
925 via_label: None,
926 via_edge: None,
927 via_dir: None,
928 };
929 assert!(ok_all.validate().is_ok());
930
931 let bad_fe = RuleDef {
933 name: "bfe".into(),
934 src_label: "A".into(),
935 dst_label: "A".into(),
936 predicate: Predicate::FieldEqual { field: "f".into() },
937 edge_type: "FE".into(),
938 weight_prop: None,
939 max_edges: None,
940 approximate: true,
941 via_label: None,
942 via_edge: None,
943 via_dir: None,
944 };
945 assert!(bad_fe.validate().is_err());
946
947 let bad_ov = RuleDef {
949 name: "bov".into(),
950 src_label: "A".into(),
951 dst_label: "A".into(),
952 predicate: Predicate::Overlap {
953 field: "tags".into(),
954 min: 0.5,
955 },
956 edge_type: "OV".into(),
957 weight_prop: None,
958 max_edges: None,
959 approximate: true,
960 via_label: None,
961 via_edge: None,
962 via_dir: None,
963 };
964 assert!(bad_ov.validate().is_err());
965
966 let bad_all_order = RuleDef {
968 name: "bao".into(),
969 src_label: "A".into(),
970 dst_label: "A".into(),
971 predicate: Predicate::All(vec![
972 Predicate::FieldEqual { field: "f".into() },
973 Predicate::VectorSimilar {
974 field: "emb".into(),
975 min: 0.9,
976 },
977 ]),
978 edge_type: "E".into(),
979 weight_prop: None,
980 max_edges: None,
981 approximate: true,
982 via_label: None,
983 via_edge: None,
984 via_dir: None,
985 };
986 assert!(bad_all_order.validate().is_err());
987 }
988
989 #[test]
990 fn validate_rejects_via_with_approximate() {
991 let bad = RuleDef {
993 name: "vbad".into(),
994 src_label: "A".into(),
995 dst_label: "B".into(),
996 predicate: Predicate::VectorSimilar {
997 field: "emb".into(),
998 min: 0.9,
999 },
1000 edge_type: "VEC".into(),
1001 weight_prop: None,
1002 max_edges: None,
1003 approximate: true,
1004 via_label: Some("Mid".into()),
1005 via_edge: Some("hop".into()),
1006 via_dir: None,
1007 };
1008 let err = bad.validate().unwrap_err();
1009 assert_eq!(err, "via-hop rules do not support approximate: true");
1010
1011 let ok = RuleDef {
1013 approximate: false,
1014 ..bad.clone()
1015 };
1016 assert!(ok.validate().is_ok());
1017 }
1018
1019 #[test]
1020 fn all_composes_field_equal_and_numeric_within() {
1021 let a: HashMap<_, _> = [
1022 ("ind".to_string(), Value::Str("arch".into())),
1023 ("year".to_string(), Value::Int(1998)),
1024 ]
1025 .into();
1026 let b: HashMap<_, _> = [
1027 ("ind".to_string(), Value::Str("arch".into())),
1028 ("year".to_string(), Value::Float(2000.0)),
1029 ]
1030 .into();
1031 let p = Predicate::All(vec![
1032 Predicate::FieldEqual {
1033 field: "ind".into(),
1034 },
1035 Predicate::NumericWithin {
1036 field: "year".into(),
1037 tolerance: 3.0,
1038 },
1039 ]);
1040 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1041 assert!((s - 1.0 / 3.0).abs() < 1e-9); }
1043
1044 fn sample_rule(pred: Predicate) -> RuleDef {
1045 RuleDef {
1046 name: "r".into(),
1047 src_label: "A".into(),
1048 dst_label: "B".into(),
1049 predicate: pred,
1050 edge_type: "E".into(),
1051 weight_prop: None,
1052 max_edges: None,
1053 approximate: false,
1054 via_label: None,
1055 via_edge: None,
1056 via_dir: None,
1057 }
1058 }
1059
1060 #[test]
1066 fn any_takes_max_score_and_requires_at_least_one_branch() {
1067 let mk =
1068 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1069 let a: HashMap<_, _> = [
1074 ("ind".to_string(), Value::Str("arch".into())),
1075 ("tags".to_string(), mk(&["x", "y"])),
1076 ]
1077 .into();
1078 let b: HashMap<_, _> = [
1079 ("ind".to_string(), Value::Str("law".into())),
1080 ("tags".to_string(), mk(&["y", "z"])),
1081 ]
1082 .into();
1083 let p = Predicate::Any(vec![
1084 Predicate::FieldEqual {
1085 field: "ind".into(),
1086 },
1087 Predicate::Overlap {
1088 field: "tags".into(),
1089 min: 0.3,
1090 },
1091 ]);
1092 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1093 assert!(
1094 (s - 1.0 / 3.0).abs() < 1e-9,
1095 "score must be max(None, 1/3) = 1/3, got {s}"
1096 );
1097 }
1098
1099 #[test]
1101 fn any_score_is_max_when_both_branches_match() {
1102 let a: HashMap<_, _> = [
1107 ("ind".to_string(), Value::Str("arch".into())),
1108 ("year".to_string(), Value::Int(2000)),
1109 ]
1110 .into();
1111 let b: HashMap<_, _> = [
1112 ("ind".to_string(), Value::Str("arch".into())),
1113 ("year".to_string(), Value::Float(2001.0)),
1114 ]
1115 .into();
1116 let p = Predicate::Any(vec![
1117 Predicate::FieldEqual {
1118 field: "ind".into(),
1119 },
1120 Predicate::NumericWithin {
1121 field: "year".into(),
1122 tolerance: 3.0,
1123 },
1124 ]);
1125 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1126 assert!(
1127 (s - 1.0).abs() < 1e-9,
1128 "score must be max(1.0, 2/3) = 1.0, got {s}"
1129 );
1130 }
1131
1132 #[test]
1134 fn any_returns_none_when_all_branches_fail() {
1135 let a: HashMap<_, _> = [("ind".to_string(), Value::Str("arch".into()))].into();
1136 let b: HashMap<_, _> = [("ind".to_string(), Value::Str("law".into()))].into();
1137 let p = Predicate::Any(vec![
1138 Predicate::FieldEqual {
1139 field: "ind".into(),
1140 },
1141 Predicate::FieldEqual {
1142 field: "ind".into(),
1143 },
1144 ]);
1145 assert_eq!(eval!(&p, ("a", a) => ("b", b)), None);
1146 }
1147
1148 #[test]
1151 fn nested_all_of_any_uses_min_over_max() {
1152 let mk =
1153 |items: &[&str]| Value::List(items.iter().map(|s| Value::Str((*s).into())).collect());
1154 let a: HashMap<_, _> = [
1162 ("ind".to_string(), Value::Str("arch".into())),
1163 ("tags".to_string(), mk(&["x", "y"])),
1164 ("year".to_string(), Value::Int(2000)),
1165 ]
1166 .into();
1167 let b: HashMap<_, _> = [
1168 ("ind".to_string(), Value::Str("arch".into())),
1169 ("tags".to_string(), mk(&["y", "z"])),
1170 ("year".to_string(), Value::Float(2001.0)),
1171 ]
1172 .into();
1173 let p = Predicate::All(vec![
1174 Predicate::FieldEqual {
1175 field: "ind".into(),
1176 },
1177 Predicate::Any(vec![
1178 Predicate::Overlap {
1179 field: "tags".into(),
1180 min: 0.3,
1181 },
1182 Predicate::NumericWithin {
1183 field: "year".into(),
1184 tolerance: 3.0,
1185 },
1186 ]),
1187 ]);
1188 let s = eval!(&p, ("a", a) => ("b", b)).unwrap();
1189 assert!(
1190 (s - 2.0 / 3.0).abs() < 1e-9,
1191 "expected min(1.0, max(1/3, 2/3)) = 2/3, got {s}"
1192 );
1193 }
1194
1195 #[test]
1198 fn nested_any_of_all_uses_max_over_min() {
1199 let p = Predicate::Any(vec![
1201 Predicate::All(vec![
1202 Predicate::FieldEqual {
1203 field: "gen".into(),
1204 },
1205 Predicate::NumericWithin {
1206 field: "yr".into(),
1207 tolerance: 4.0,
1208 },
1209 ]),
1210 Predicate::NumericWithin {
1211 field: "yr2".into(),
1212 tolerance: 10.0,
1213 },
1214 ]);
1215
1216 let a: HashMap<_, _> = [
1223 ("gen".to_string(), Value::Str("pop".into())),
1224 ("yr".to_string(), Value::Int(2000)),
1225 ("yr2".to_string(), Value::Int(2000)),
1226 ]
1227 .into();
1228 let b: HashMap<_, _> = [
1229 ("gen".to_string(), Value::Str("pop".into())),
1230 ("yr".to_string(), Value::Float(2001.0)),
1231 ("yr2".to_string(), Value::Float(2005.0)),
1232 ]
1233 .into();
1234 let s_a = eval!(&p, ("a", a) => ("b", b)).unwrap();
1235 assert!(
1236 (s_a - 0.75).abs() < 1e-9,
1237 "Any-of-All scenario A: max(min(1.0,0.75), 0.5) must be 0.75, got {s_a}"
1238 );
1239
1240 let a2: HashMap<_, _> = [
1245 ("gen".to_string(), Value::Str("pop".into())),
1246 ("yr".to_string(), Value::Int(2000)),
1247 ("yr2".to_string(), Value::Int(2000)),
1248 ]
1249 .into();
1250 let c: HashMap<_, _> = [
1251 ("gen".to_string(), Value::Str("rock".into())),
1252 ("yr".to_string(), Value::Float(2001.0)),
1253 ("yr2".to_string(), Value::Float(2001.0)),
1254 ]
1255 .into();
1256 let s_b = eval!(&p, ("a", a2) => ("c", c)).unwrap();
1257 assert!(
1258 (s_b - 0.9).abs() < 1e-9,
1259 "Any-of-All scenario B: max(None, 0.9) must be 0.9, got {s_b}"
1260 );
1261 }
1262
1263 #[test]
1265 fn any_validation_errors() {
1266 let empty = sample_rule(Predicate::Any(vec![]));
1268 let err = empty.validate().unwrap_err();
1269 assert_eq!(err, "any() must have at least one predicate");
1270
1271 fn any_chain(depth: usize) -> Predicate {
1273 if depth == 0 {
1274 Predicate::FieldEqual { field: "f".into() }
1275 } else {
1276 Predicate::Any(vec![any_chain(depth - 1)])
1277 }
1278 }
1279
1280 assert!(
1282 sample_rule(any_chain(4)).validate().is_ok(),
1283 "depth 4 must be valid (at cap)"
1284 );
1285 let too_deep = sample_rule(any_chain(5));
1287 let err = too_deep.validate().unwrap_err();
1288 assert!(
1289 err.contains("nesting depth"),
1290 "error must mention 'nesting depth', got: {err}"
1291 );
1292
1293 let bad_inner = sample_rule(Predicate::Any(vec![Predicate::All(vec![])]));
1295 assert!(bad_inner.validate().is_err());
1296 }
1297
1298 #[test]
1300 fn any_watched_fields_collected() {
1301 let p = Predicate::Any(vec![
1302 Predicate::FieldEqual {
1303 field: "ind".into(),
1304 },
1305 Predicate::NumericWithin {
1306 field: "year".into(),
1307 tolerance: 1.0,
1308 },
1309 ]);
1310 let r = sample_rule(p);
1311 assert!(r.validate().is_ok());
1312 let fields: Vec<_> = r.watched_fields().into_iter().collect();
1313 assert_eq!(fields, vec!["ind".to_string(), "year".to_string()]);
1314 }
1315
1316 #[test]
1317 fn new_predicates_validate_and_watch_fields() {
1318 let num = sample_rule(Predicate::NumericWithin {
1319 field: "year".into(),
1320 tolerance: 2.0,
1321 });
1322 assert!(num.validate().is_ok());
1323 assert_eq!(
1324 num.watched_fields().into_iter().collect::<Vec<_>>(),
1325 vec!["year".to_string()]
1326 );
1327 let geo = sample_rule(Predicate::GeoRadius {
1328 field: "loc".into(),
1329 km: 400.0,
1330 });
1331 assert!(geo.validate().is_ok());
1332 let vecp = sample_rule(Predicate::VectorSimilar {
1333 field: "emb".into(),
1334 min: 0.9,
1335 });
1336 assert!(vecp.validate().is_ok());
1337
1338 let mut bad = num.clone();
1339 bad.predicate = Predicate::NumericWithin {
1340 field: "year".into(),
1341 tolerance: -1.0,
1342 };
1343 assert!(bad.validate().is_err());
1344 bad.predicate = Predicate::NumericWithin {
1345 field: "year".into(),
1346 tolerance: f64::NAN,
1347 };
1348 assert!(bad.validate().is_err());
1349
1350 let mut bad_geo = geo;
1351 bad_geo.predicate = Predicate::GeoRadius {
1352 field: "loc".into(),
1353 km: 0.0,
1354 };
1355 assert!(bad_geo.validate().is_err());
1356 bad_geo.predicate = Predicate::GeoRadius {
1357 field: "loc".into(),
1358 km: f64::NAN,
1359 };
1360 assert!(bad_geo.validate().is_err());
1361
1362 let mut bad_vec = vecp;
1363 bad_vec.predicate = Predicate::VectorSimilar {
1364 field: "emb".into(),
1365 min: 0.0,
1366 };
1367 assert!(bad_vec.validate().is_err());
1368 bad_vec.predicate = Predicate::VectorSimilar {
1369 field: "emb".into(),
1370 min: 1.5,
1371 };
1372 assert!(bad_vec.validate().is_err());
1373 }
1374
1375 #[test]
1376 fn default_max_edges_keymatch_is_1_else_32() {
1377 assert_eq!(DEFAULT_SCORED_TOP_K, 32);
1378 assert_eq!(DEFAULT_KEYMATCH_TOP_K, 1);
1379 assert_eq!(
1380 default_max_edges(&Predicate::KeyMatch { field: "fk".into() }),
1381 DEFAULT_KEYMATCH_TOP_K
1382 );
1383 assert_eq!(
1384 default_max_edges(&Predicate::All(vec![Predicate::KeyMatch {
1385 field: "fk".into()
1386 }])),
1387 DEFAULT_KEYMATCH_TOP_K
1388 );
1389 assert_eq!(
1390 default_max_edges(&Predicate::All(vec![Predicate::All(vec![
1391 Predicate::KeyMatch { field: "fk".into() }
1392 ])])),
1393 DEFAULT_KEYMATCH_TOP_K
1394 );
1395 assert_eq!(
1396 default_max_edges(&Predicate::Overlap {
1397 field: "tags".into(),
1398 min: 0.5,
1399 }),
1400 DEFAULT_SCORED_TOP_K
1401 );
1402 assert_eq!(
1403 default_max_edges(&Predicate::Any(vec![Predicate::KeyMatch {
1404 field: "fk".into()
1405 }])),
1406 DEFAULT_SCORED_TOP_K
1407 );
1408 assert!(!is_keymatch_rooted(&Predicate::FieldEqual {
1409 field: "f".into()
1410 }));
1411 }
1412}
1413
1414#[cfg(test)]
1415mod wire_pins {
1416 use super::*;
1417
1418 fn pin(pred: Predicate) -> RuleDef {
1419 RuleDef {
1420 name: "r".into(),
1421 src_label: "A".into(),
1422 dst_label: "B".into(),
1423 predicate: pred,
1424 edge_type: "E".into(),
1425 weight_prop: None,
1426 max_edges: None,
1427 approximate: false,
1428 via_label: None,
1429 via_edge: None,
1430 via_dir: None,
1431 }
1432 }
1433
1434 fn pin_approx(pred: Predicate) -> RuleDef {
1435 RuleDef {
1436 name: "r".into(),
1437 src_label: "A".into(),
1438 dst_label: "B".into(),
1439 predicate: pred,
1440 edge_type: "E".into(),
1441 weight_prop: None,
1442 max_edges: None,
1443 approximate: true,
1444 via_label: None,
1445 via_edge: None,
1446 via_dir: None,
1447 }
1448 }
1449
1450 #[test]
1451 fn old_predicate_variants_keep_encoding() {
1452 assert_eq!(
1459 bincode::serialize(&pin(Predicate::KeyMatch { field: "fk".into() })).unwrap(),
1460 vec![
1461 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1462 66, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 102, 107, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0,
1463 0, 0, 0, 0
1464 ]
1465 );
1466 assert_eq!(
1467 bincode::serialize(&pin(Predicate::FieldEqual {
1468 field: "ind".into()
1469 }))
1470 .unwrap(),
1471 vec![
1472 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1473 66, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 105, 110, 100, 1, 0, 0, 0, 0, 0, 0, 0, 69,
1474 0, 0, 0, 0, 0, 0
1475 ]
1476 );
1477 assert_eq!(
1478 bincode::serialize(&pin(Predicate::Overlap {
1479 field: "tags".into(),
1480 min: 0.5,
1481 }))
1482 .unwrap(),
1483 vec![
1484 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1485 66, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1486 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1487 ]
1488 );
1489 assert_eq!(
1490 bincode::serialize(&pin(Predicate::All(vec![
1491 Predicate::KeyMatch { field: "fk".into() },
1492 Predicate::Overlap {
1493 field: "tags".into(),
1494 min: 0.5,
1495 },
1496 ])))
1497 .unwrap(),
1498 vec![
1499 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1500 66, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 102,
1501 107, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 116, 97, 103, 115, 0, 0, 0, 0, 0, 0, 224,
1502 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1503 ]
1504 );
1505 }
1506
1507 #[test]
1508 fn new_predicate_variants_have_pinned_encoding() {
1509 assert_eq!(
1511 bincode::serialize(&pin(Predicate::NumericWithin {
1512 field: "year".into(),
1513 tolerance: 2.0,
1514 }))
1515 .unwrap(),
1516 vec![
1517 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1518 66, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 121, 101, 97, 114, 0, 0, 0, 0, 0, 0, 0, 64,
1519 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1520 ]
1521 );
1522 assert_eq!(
1523 bincode::serialize(&pin(Predicate::GeoRadius {
1524 field: "loc".into(),
1525 km: 400.0,
1526 }))
1527 .unwrap(),
1528 vec![
1529 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1530 66, 5, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 108, 111, 99, 0, 0, 0, 0, 0, 0, 121, 64, 1,
1531 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1532 ]
1533 );
1534 assert_eq!(
1535 bincode::serialize(&pin(Predicate::VectorSimilar {
1536 field: "emb".into(),
1537 min: 0.9,
1538 }))
1539 .unwrap(),
1540 vec![
1541 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1542 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1543 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0
1544 ]
1545 );
1546 }
1547
1548 #[test]
1549 fn any_variant_is_appended_at_discriminant_7() {
1550 let any_fe = pin(Predicate::Any(vec![Predicate::FieldEqual {
1566 field: "f".into(),
1567 }]));
1568 assert_eq!(
1569 bincode::serialize(&any_fe).unwrap(),
1570 vec![
1571 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1572 66, 7, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 102, 1,
1573 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 0, 0, 0, 0,
1574 ],
1575 "Any([FieldEqual{{f}}]) exact-bytes pin failed — discriminant or field layout changed"
1576 );
1577 let decoded: RuleDef = bincode::deserialize(&bincode::serialize(&any_fe).unwrap()).unwrap();
1579 assert_eq!(decoded, any_fe, "Any must round-trip via bincode");
1580 let vs = pin(Predicate::VectorSimilar {
1583 field: "emb".into(),
1584 min: 0.9,
1585 });
1586 let vs_bytes = bincode::serialize(&vs).unwrap();
1587 let vs_decoded: RuleDef = bincode::deserialize(&vs_bytes).unwrap();
1588 assert_eq!(
1589 vs_decoded.predicate,
1590 Predicate::VectorSimilar {
1591 field: "emb".into(),
1592 min: 0.9
1593 },
1594 "VectorSimilar record must still decode after via fields appended"
1595 );
1596 }
1597
1598 #[test]
1599 fn approximate_variant_has_pinned_encoding() {
1600 assert_eq!(
1604 bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1605 field: "emb".into(),
1606 min: 0.9,
1607 }))
1608 .unwrap(),
1609 vec![
1610 1, 0, 0, 0, 0, 0, 0, 0, 114, 1, 0, 0, 0, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 0, 0, 0,
1611 66, 6, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 101, 109, 98, 205, 204, 204, 204, 204, 204,
1612 236, 63, 1, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 1, 0, 0, 0
1613 ]
1614 );
1615 let exact = bincode::serialize(&pin(Predicate::VectorSimilar {
1618 field: "emb".into(),
1619 min: 0.9,
1620 }))
1621 .unwrap();
1622 let approx = bincode::serialize(&pin_approx(Predicate::VectorSimilar {
1623 field: "emb".into(),
1624 min: 0.9,
1625 }))
1626 .unwrap();
1627 assert_eq!(exact.len(), approx.len());
1628 let n = exact.len();
1629 assert_eq!(&exact[..n - 4], &approx[..n - 4]);
1631 assert_eq!(exact[n - 4], 0u8, "exact: approximate=false");
1633 assert_eq!(approx[n - 4], 1u8, "approx: approximate=true");
1634 assert_eq!(&exact[n - 3..], &[0u8, 0, 0]);
1636 assert_eq!(&approx[n - 3..], &[0u8, 0, 0]);
1637 }
1638}