1use crate::edge::Edge;
16use crate::error::{GraphError, Result};
17use crate::node::Node;
18use crate::types::PropertyValue;
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum PropertyType {
25 Boolean,
26 Integer,
27 Float,
29 String,
30 Vector,
32 Array,
34 Map,
35 Any,
37}
38
39impl PropertyType {
40 pub fn accepts(&self, value: &PropertyValue) -> bool {
42 match self {
43 PropertyType::Any => true,
44 PropertyType::Boolean => matches!(value, PropertyValue::Boolean(_)),
45 PropertyType::Integer => matches!(value, PropertyValue::Integer(_)),
46 PropertyType::Float => {
48 matches!(value, PropertyValue::Float(_) | PropertyValue::Integer(_))
49 }
50 PropertyType::String => matches!(value, PropertyValue::String(_)),
51 PropertyType::Vector => extract_vector(value).is_some(),
52 PropertyType::Array => {
53 matches!(value, PropertyValue::Array(_) | PropertyValue::List(_))
54 }
55 PropertyType::Map => matches!(value, PropertyValue::Map(_)),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63pub enum DistanceMetric {
64 Cosine,
65 DotProduct,
66 Euclidean,
67}
68
69impl DistanceMetric {
70 pub fn score(&self, a: &[f32], b: &[f32]) -> f32 {
74 self.score_pre(a, b, self.query_norm(a))
75 }
76
77 #[inline]
80 pub fn query_norm(&self, q: &[f32]) -> f32 {
81 match self {
82 DistanceMetric::Cosine => dot(q, q).sqrt(),
83 _ => 1.0,
84 }
85 }
86
87 #[inline]
90 pub fn score_pre(&self, query: &[f32], candidate: &[f32], query_norm: f32) -> f32 {
91 match self {
92 DistanceMetric::DotProduct => dot(query, candidate),
93 DistanceMetric::Cosine => {
94 let n = query.len().min(candidate.len());
98 let mut qc = 0.0f32;
99 let mut cc = 0.0f32;
100 for i in 0..n {
101 let c = candidate[i];
102 qc += query[i] * c;
103 cc += c * c;
104 }
105 let cn = cc.sqrt();
106 if query_norm == 0.0 || cn == 0.0 {
107 0.0
108 } else {
109 qc / (query_norm * cn)
110 }
111 }
112 DistanceMetric::Euclidean => {
113 let n = query.len().min(candidate.len());
114 let mut sum = 0.0f32;
115 for i in 0..n {
116 let d = query[i] - candidate[i];
117 sum += d * d;
118 }
119 -sum.sqrt()
120 }
121 }
122 }
123}
124
125#[inline]
129pub fn score_property(
130 metric: DistanceMetric,
131 query: &[f32],
132 query_norm: f32,
133 value: &PropertyValue,
134) -> Option<f32> {
135 match value {
136 PropertyValue::FloatArray(v) => {
138 if v.len() == query.len() {
139 Some(metric.score_pre(query, v, query_norm))
140 } else {
141 None
142 }
143 }
144 PropertyValue::Array(_) | PropertyValue::List(_) => {
146 let v = extract_vector(value)?;
147 if v.len() == query.len() {
148 Some(metric.score_pre(query, &v, query_norm))
149 } else {
150 None
151 }
152 }
153 _ => None,
154 }
155}
156
157#[inline]
158fn dot(a: &[f32], b: &[f32]) -> f32 {
159 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
164}
165
166pub fn extract_vector(value: &PropertyValue) -> Option<Vec<f32>> {
168 match value {
169 PropertyValue::FloatArray(v) => Some(v.clone()),
170 PropertyValue::Array(items) | PropertyValue::List(items) => {
171 let mut out = Vec::with_capacity(items.len());
172 for it in items {
173 match it {
174 PropertyValue::Float(f) => out.push(*f as f32),
175 PropertyValue::Integer(i) => out.push(*i as f32),
176 _ => return None,
177 }
178 }
179 if out.is_empty() {
180 None
181 } else {
182 Some(out)
183 }
184 }
185 _ => None,
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct PropertySchema {
192 pub name: String,
193 pub ptype: PropertyType,
194 pub required: bool,
196 pub indexed: bool,
198}
199
200impl PropertySchema {
201 pub fn new(name: impl Into<String>, ptype: PropertyType) -> Self {
202 Self {
203 name: name.into(),
204 ptype,
205 required: false,
206 indexed: false,
207 }
208 }
209 pub fn required(mut self) -> Self {
210 self.required = true;
211 self
212 }
213 pub fn indexed(mut self) -> Self {
214 self.indexed = true;
215 self
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct NodeSchema {
222 pub label: String,
223 pub properties: Vec<PropertySchema>,
224 pub strict: bool,
226}
227
228impl NodeSchema {
229 pub fn new(label: impl Into<String>) -> Self {
230 Self {
231 label: label.into(),
232 properties: Vec::new(),
233 strict: false,
234 }
235 }
236 pub fn property(mut self, p: PropertySchema) -> Self {
237 self.properties.push(p);
238 self
239 }
240 pub fn strict(mut self) -> Self {
241 self.strict = true;
242 self
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct EdgeSchema {
249 pub edge_type: String,
250 pub from_label: String,
251 pub to_label: String,
252 pub properties: Vec<PropertySchema>,
253}
254
255impl EdgeSchema {
256 pub fn new(
257 edge_type: impl Into<String>,
258 from_label: impl Into<String>,
259 to_label: impl Into<String>,
260 ) -> Self {
261 Self {
262 edge_type: edge_type.into(),
263 from_label: from_label.into(),
264 to_label: to_label.into(),
265 properties: Vec::new(),
266 }
267 }
268 pub fn property(mut self, p: PropertySchema) -> Self {
269 self.properties.push(p);
270 self
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct VectorSchema {
277 pub name: String,
279 pub label: String,
281 pub property: String,
283 pub dimensions: usize,
284 pub metric: DistanceMetric,
285}
286
287impl VectorSchema {
288 pub fn new(
289 name: impl Into<String>,
290 label: impl Into<String>,
291 property: impl Into<String>,
292 dimensions: usize,
293 metric: DistanceMetric,
294 ) -> Self {
295 Self {
296 name: name.into(),
297 label: label.into(),
298 property: property.into(),
299 dimensions,
300 metric,
301 }
302 }
303}
304
305#[derive(Debug, Clone, Default, Serialize, Deserialize)]
307pub struct GraphSchema {
308 nodes: HashMap<String, NodeSchema>,
309 edges: HashMap<String, EdgeSchema>,
310 vectors: HashMap<String, VectorSchema>,
311}
312
313impl GraphSchema {
314 pub fn new() -> Self {
315 Self::default()
316 }
317
318 pub fn add_node(&mut self, schema: NodeSchema) -> &mut Self {
319 self.nodes.insert(schema.label.clone(), schema);
320 self
321 }
322 pub fn add_edge(&mut self, schema: EdgeSchema) -> &mut Self {
323 self.edges.insert(schema.edge_type.clone(), schema);
324 self
325 }
326 pub fn add_vector(&mut self, schema: VectorSchema) -> &mut Self {
327 self.vectors.insert(schema.name.clone(), schema);
328 self
329 }
330
331 pub fn node(&self, label: &str) -> Option<&NodeSchema> {
332 self.nodes.get(label)
333 }
334 pub fn edge(&self, edge_type: &str) -> Option<&EdgeSchema> {
335 self.edges.get(edge_type)
336 }
337 pub fn vector(&self, name: &str) -> Option<&VectorSchema> {
338 self.vectors.get(name)
339 }
340
341 pub fn node_schemas_sorted(&self) -> Vec<&NodeSchema> {
343 let mut v: Vec<&NodeSchema> = self.nodes.values().collect();
344 v.sort_by(|a, b| a.label.cmp(&b.label));
345 v
346 }
347 pub fn edge_schemas_sorted(&self) -> Vec<&EdgeSchema> {
349 let mut v: Vec<&EdgeSchema> = self.edges.values().collect();
350 v.sort_by(|a, b| a.edge_type.cmp(&b.edge_type));
351 v
352 }
353 pub fn vector_schemas_sorted(&self) -> Vec<&VectorSchema> {
355 let mut v: Vec<&VectorSchema> = self.vectors.values().collect();
356 v.sort_by(|a, b| a.name.cmp(&b.name));
357 v
358 }
359
360 pub fn validate_self(&self) -> Result<()> {
364 for e in self.edges.values() {
365 if !self.nodes.contains_key(&e.from_label) {
366 return Err(GraphError::SchemaViolation(format!(
367 "edge '{}' references undeclared from-label '{}'",
368 e.edge_type, e.from_label
369 )));
370 }
371 if !self.nodes.contains_key(&e.to_label) {
372 return Err(GraphError::SchemaViolation(format!(
373 "edge '{}' references undeclared to-label '{}'",
374 e.edge_type, e.to_label
375 )));
376 }
377 }
378 for v in self.vectors.values() {
379 if !self.nodes.contains_key(&v.label) {
380 return Err(GraphError::SchemaViolation(format!(
381 "vector '{}' bound to undeclared label '{}'",
382 v.name, v.label
383 )));
384 }
385 }
386 Ok(())
387 }
388
389 pub fn validate_node(&self, node: &Node) -> Result<()> {
392 let mut allowed: Vec<&str> = Vec::new();
394 let mut any_strict = false;
395 let mut matched_any = false;
396
397 for label in &node.labels {
398 let Some(ns) = self.nodes.get(&label.name) else {
399 continue;
400 };
401 matched_any = true;
402 any_strict |= ns.strict;
403 for p in &ns.properties {
404 allowed.push(p.name.as_str());
405 match node.properties.get(&p.name) {
406 None if p.required => {
407 return Err(GraphError::SchemaViolation(format!(
408 "node '{}' (:{}) missing required property '{}'",
409 node.id, label.name, p.name
410 )));
411 }
412 Some(v) if !p.ptype.accepts(v) => {
413 return Err(GraphError::SchemaViolation(format!(
414 "node '{}' (:{}) property '{}' has wrong type (expected {:?})",
415 node.id, label.name, p.name, p.ptype
416 )));
417 }
418 _ => {}
419 }
420 }
421 }
422
423 if matched_any && any_strict {
424 for key in node.properties.keys() {
425 if !allowed.iter().any(|a| a == key) {
426 return Err(GraphError::SchemaViolation(format!(
427 "node '{}' has undeclared property '{}' (strict schema)",
428 node.id, key
429 )));
430 }
431 }
432 }
433 Ok(())
434 }
435
436 pub fn validate_edge(
440 &self,
441 edge: &Edge,
442 from_labels: &[String],
443 to_labels: &[String],
444 ) -> Result<()> {
445 let Some(es) = self.edges.get(&edge.edge_type) else {
446 return Ok(());
447 };
448 if !from_labels.iter().any(|l| l == &es.from_label) {
449 return Err(GraphError::SchemaViolation(format!(
450 "edge '{}' requires from-label '{}', got {:?}",
451 edge.edge_type, es.from_label, from_labels
452 )));
453 }
454 if !to_labels.iter().any(|l| l == &es.to_label) {
455 return Err(GraphError::SchemaViolation(format!(
456 "edge '{}' requires to-label '{}', got {:?}",
457 edge.edge_type, es.to_label, to_labels
458 )));
459 }
460 for p in &es.properties {
461 match edge.properties.get(&p.name) {
462 None if p.required => {
463 return Err(GraphError::SchemaViolation(format!(
464 "edge '{}' missing required property '{}'",
465 edge.edge_type, p.name
466 )));
467 }
468 Some(v) if !p.ptype.accepts(v) => {
469 return Err(GraphError::SchemaViolation(format!(
470 "edge '{}' property '{}' has wrong type (expected {:?})",
471 edge.edge_type, p.name, p.ptype
472 )));
473 }
474 _ => {}
475 }
476 }
477 Ok(())
478 }
479
480 pub fn validate_vector_dims(&self, vector_type: &str, query: &[f32]) -> Result<&VectorSchema> {
482 let vs = self.vectors.get(vector_type).ok_or_else(|| {
483 GraphError::SchemaViolation(format!("unknown vector type '{}'", vector_type))
484 })?;
485 if query.len() != vs.dimensions {
486 return Err(GraphError::SchemaViolation(format!(
487 "vector type '{}' expects dimension {}, got {}",
488 vector_type,
489 vs.dimensions,
490 query.len()
491 )));
492 }
493 Ok(vs)
494 }
495}
496
497pub fn reciprocal_rank_fusion(rankings: &[Vec<String>], k_const: f32) -> Vec<(String, f32)> {
502 let mut scores: HashMap<String, f32> = HashMap::new();
503 for ranking in rankings {
504 for (rank, id) in ranking.iter().enumerate() {
505 let contribution = 1.0 / (k_const + (rank as f32 + 1.0));
506 *scores.entry(id.clone()).or_insert(0.0) += contribution;
507 }
508 }
509 let mut fused: Vec<(String, f32)> = scores.into_iter().collect();
510 fused.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
511 fused
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::node::NodeBuilder;
518 use crate::types::Label;
519
520 fn person_schema() -> GraphSchema {
521 let mut s = GraphSchema::new();
522 s.add_node(
523 NodeSchema::new("Person")
524 .property(
525 PropertySchema::new("name", PropertyType::String)
526 .required()
527 .indexed(),
528 )
529 .property(PropertySchema::new("age", PropertyType::Integer))
530 .property(PropertySchema::new("embedding", PropertyType::Vector)),
531 );
532 s.add_node(NodeSchema::new("Company"));
533 s.add_edge(EdgeSchema::new("WORKS_AT", "Person", "Company"));
534 s.add_vector(VectorSchema::new(
535 "PersonEmb",
536 "Person",
537 "embedding",
538 3,
539 DistanceMetric::Cosine,
540 ));
541 s
542 }
543
544 #[test]
545 fn self_validation_catches_dangling_refs() {
546 let mut s = GraphSchema::new();
547 s.add_edge(EdgeSchema::new("KNOWS", "Person", "Person"));
548 assert!(s.validate_self().is_err());
549 s.add_node(NodeSchema::new("Person"));
550 assert!(s.validate_self().is_ok());
551 }
552
553 #[test]
554 fn node_validation_required_and_types() {
555 let s = person_schema();
556 let ok = NodeBuilder::new()
558 .label("Person")
559 .property("name", "Alice")
560 .property("age", 30i64)
561 .build();
562 assert!(s.validate_node(&ok).is_ok());
563 let missing = NodeBuilder::new()
565 .label("Person")
566 .property("age", 30i64)
567 .build();
568 assert!(s.validate_node(&missing).is_err());
569 let wrong = NodeBuilder::new()
571 .label("Person")
572 .property("name", "Bob")
573 .property("age", "old")
574 .build();
575 assert!(s.validate_node(&wrong).is_err());
576 let other = NodeBuilder::new()
578 .label("Alien")
579 .property("planet", "Mars")
580 .build();
581 assert!(s.validate_node(&other).is_ok());
582 }
583
584 #[test]
585 fn strict_node_rejects_undeclared_props() {
586 let mut s = GraphSchema::new();
587 s.add_node(
588 NodeSchema::new("Tag")
589 .property(PropertySchema::new("name", PropertyType::String))
590 .strict(),
591 );
592 let bad = NodeBuilder::new()
593 .label("Tag")
594 .property("name", "x")
595 .property("extra", 1i64)
596 .build();
597 assert!(s.validate_node(&bad).is_err());
598 }
599
600 #[test]
601 fn edge_validation_checks_endpoint_labels() {
602 let s = person_schema();
603 let e = Edge::create("p1".into(), "c1".into(), "WORKS_AT");
604 assert!(s
605 .validate_edge(&e, &["Person".into()], &["Company".into()])
606 .is_ok());
607 assert!(s
609 .validate_edge(&e, &["Company".into()], &["Company".into()])
610 .is_err());
611 let e2 = Edge::create("p1".into(), "p2".into(), "LIKES");
613 assert!(s
614 .validate_edge(&e2, &["Person".into()], &["Person".into()])
615 .is_ok());
616 }
617
618 #[test]
619 fn vector_dim_validation() {
620 let s = person_schema();
621 assert!(s
622 .validate_vector_dims("PersonEmb", &[1.0, 2.0, 3.0])
623 .is_ok());
624 assert!(s.validate_vector_dims("PersonEmb", &[1.0, 2.0]).is_err());
625 assert!(s.validate_vector_dims("Missing", &[1.0, 2.0, 3.0]).is_err());
626 }
627
628 #[test]
629 fn distance_metrics_rank_higher_is_better() {
630 let q = [1.0f32, 0.0, 0.0];
631 let near = [0.9f32, 0.1, 0.0];
632 let far = [0.0f32, 1.0, 0.0];
633 for m in [
634 DistanceMetric::Cosine,
635 DistanceMetric::DotProduct,
636 DistanceMetric::Euclidean,
637 ] {
638 assert!(m.score(&q, &near) > m.score(&q, &far), "{:?}", m);
639 }
640 }
641
642 #[test]
643 fn extract_vector_handles_shapes() {
644 assert_eq!(
645 extract_vector(&PropertyValue::FloatArray(vec![1.0, 2.0])),
646 Some(vec![1.0, 2.0])
647 );
648 assert_eq!(
649 extract_vector(&PropertyValue::Array(vec![
650 PropertyValue::Integer(1),
651 PropertyValue::Float(2.0)
652 ])),
653 Some(vec![1.0, 2.0])
654 );
655 assert_eq!(extract_vector(&PropertyValue::String("x".into())), None);
656 }
657
658 #[test]
659 fn rrf_fuses_and_ranks() {
660 let a = vec!["x".to_string(), "y".to_string(), "z".to_string()];
661 let b = vec!["y".to_string(), "x".to_string()];
662 let fused = reciprocal_rank_fusion(&[a, b], 60.0);
663 assert_eq!(fused.len(), 3);
665 assert_eq!(fused[2].0, "z");
666 }
667
668 #[test]
669 fn multi_label_node_validation() {
670 let mut s = GraphSchema::new();
671 s.add_node(
672 NodeSchema::new("A")
673 .property(PropertySchema::new("a", PropertyType::Integer).required()),
674 );
675 s.add_node(
676 NodeSchema::new("B")
677 .property(PropertySchema::new("b", PropertyType::String).required()),
678 );
679 let n = Node::new(
680 "n1".into(),
681 vec![Label::new("A"), Label::new("B")],
682 [
683 ("a".to_string(), PropertyValue::Integer(1)),
684 ("b".to_string(), PropertyValue::String("x".into())),
685 ]
686 .into_iter()
687 .collect(),
688 );
689 assert!(s.validate_node(&n).is_ok());
690 }
691}