use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum NodeType {
Concept,
Entity,
Tag,
Document,
}
impl std::fmt::Display for NodeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NodeType::Concept => write!(f, "Concept"),
NodeType::Entity => write!(f, "Entity"),
NodeType::Tag => write!(f, "Tag"),
NodeType::Document => write!(f, "Document"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum EdgeType {
SimilarTo,
CoOccursWith,
BelongsTo,
ExtractedFrom,
}
impl std::fmt::Display for EdgeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EdgeType::SimilarTo => write!(f, "SIMILAR_TO"),
EdgeType::CoOccursWith => write!(f, "CO_OCCURS_WITH"),
EdgeType::BelongsTo => write!(f, "BELONGS_TO"),
EdgeType::ExtractedFrom => write!(f, "EXTRACTED_FROM"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNode {
pub id: Uuid,
pub rei_id: Uuid,
pub text: String,
pub node_type: NodeType,
pub weight: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub embedding: Option<Vec<f32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_doc_id: Option<Uuid>,
#[serde(default)]
pub metadata: serde_json::Value,
}
impl GraphNode {
pub fn concept(rei_id: Uuid, text: String, weight: f32, source_doc_id: Option<Uuid>) -> Self {
Self {
id: Uuid::new_v4(),
rei_id,
text,
node_type: NodeType::Concept,
weight,
embedding: None,
source_doc_id,
metadata: serde_json::Value::Object(Default::default()),
}
}
pub fn tag(rei_id: Uuid, name: String) -> Self {
Self {
id: Uuid::new_v4(),
rei_id,
text: name,
node_type: NodeType::Tag,
weight: 1.0,
embedding: None,
source_doc_id: None,
metadata: serde_json::Value::Object(Default::default()),
}
}
pub fn document(rei_id: Uuid, doc_id: Uuid, title: String) -> Self {
Self {
id: doc_id,
rei_id,
text: title,
node_type: NodeType::Document,
weight: 1.0,
embedding: None,
source_doc_id: None,
metadata: serde_json::Value::Object(Default::default()),
}
}
pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
self.embedding = Some(embedding);
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
pub id: Uuid,
pub from_id: Uuid,
pub to_id: Uuid,
pub edge_type: EdgeType,
pub strength: f32,
#[serde(default)]
pub metadata: serde_json::Value,
}
impl GraphEdge {
pub fn new(from_id: Uuid, to_id: Uuid, edge_type: EdgeType, strength: f32) -> Self {
Self {
id: Uuid::new_v4(),
from_id,
to_id,
edge_type,
strength: strength.clamp(0.0, 1.0),
metadata: serde_json::Value::Object(Default::default()),
}
}
pub fn similar_to(from_id: Uuid, to_id: Uuid, strength: f32) -> Self {
Self::new(from_id, to_id, EdgeType::SimilarTo, strength)
}
pub fn co_occurs_with(from_id: Uuid, to_id: Uuid, strength: f32) -> Self {
Self::new(from_id, to_id, EdgeType::CoOccursWith, strength)
}
pub fn belongs_to(node_id: Uuid, tag_id: Uuid) -> Self {
Self::new(node_id, tag_id, EdgeType::BelongsTo, 1.0)
}
pub fn extracted_from(node_id: Uuid, doc_id: Uuid) -> Self {
Self::new(node_id, doc_id, EdgeType::ExtractedFrom, 1.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphPath {
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
pub total_weight: f32,
}
impl GraphPath {
pub fn new(nodes: Vec<GraphNode>, edges: Vec<GraphEdge>) -> Self {
let total_weight = edges.iter().map(|e| e.strength).product();
Self {
nodes,
edges,
total_weight,
}
}
pub fn start(&self) -> Option<&GraphNode> {
self.nodes.first()
}
pub fn end(&self) -> Option<&GraphNode> {
self.nodes.last()
}
pub fn len(&self) -> usize {
self.edges.len()
}
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_concept_node() {
let rei_id = Uuid::new_v4();
let doc_id = Uuid::new_v4();
let node = GraphNode::concept(rei_id, "important concept".to_string(), 1.0, Some(doc_id));
assert_eq!(node.rei_id, rei_id);
assert_eq!(node.text, "important concept");
assert_eq!(node.node_type, NodeType::Concept);
assert_eq!(node.weight, 1.0);
assert_eq!(node.source_doc_id, Some(doc_id));
}
#[test]
fn test_create_edge() {
let from_id = Uuid::new_v4();
let to_id = Uuid::new_v4();
let edge = GraphEdge::similar_to(from_id, to_id, 0.85);
assert_eq!(edge.from_id, from_id);
assert_eq!(edge.to_id, to_id);
assert_eq!(edge.edge_type, EdgeType::SimilarTo);
assert_eq!(edge.strength, 0.85);
}
#[test]
fn test_edge_strength_clamping() {
let from_id = Uuid::new_v4();
let to_id = Uuid::new_v4();
let edge_high = GraphEdge::new(from_id, to_id, EdgeType::SimilarTo, 1.5);
assert_eq!(edge_high.strength, 1.0);
let edge_low = GraphEdge::new(from_id, to_id, EdgeType::SimilarTo, -0.5);
assert_eq!(edge_low.strength, 0.0);
}
#[test]
fn test_graph_path() {
let rei_id = Uuid::new_v4();
let node1 = GraphNode::concept(rei_id, "A".to_string(), 1.0, None);
let node2 = GraphNode::concept(rei_id, "B".to_string(), 1.0, None);
let edge = GraphEdge::similar_to(node1.id, node2.id, 0.9);
let path = GraphPath::new(vec![node1, node2], vec![edge]);
assert_eq!(path.len(), 1);
assert_eq!(path.total_weight, 0.9);
assert_eq!(path.start().unwrap().text, "A");
assert_eq!(path.end().unwrap().text, "B");
}
}