ruvector_graph_wasm/
lib.rs

1//! WebAssembly bindings for RuVector Graph Database
2//!
3//! This module provides high-performance browser bindings for a Neo4j-inspired graph database
4//! built on RuVector's hypergraph infrastructure.
5//!
6//! Features:
7//! - Node and edge CRUD operations
8//! - Hyperedge support for n-ary relationships
9//! - Basic Cypher query support
10//! - Web Workers support for parallel operations
11//! - Async query execution with streaming results
12//! - IndexedDB persistence (planned)
13
14use js_sys::{Array, Object, Promise, Reflect};
15use parking_lot::Mutex;
16use ruvector_core::advanced::hypergraph::{
17    Hyperedge as CoreHyperedge, HypergraphIndex, TemporalGranularity, TemporalHyperedge,
18};
19use ruvector_core::types::DistanceMetric;
20use serde_wasm_bindgen::{from_value, to_value};
21use std::collections::HashMap;
22use std::sync::Arc;
23use uuid::Uuid;
24use wasm_bindgen::prelude::*;
25use web_sys::console;
26
27pub mod async_ops;
28pub mod types;
29
30use types::{
31    js_object_to_hashmap, Edge, EdgeId, GraphError, Hyperedge, HyperedgeId, JsEdge, JsHyperedge,
32    JsNode, Node, NodeId, QueryResult,
33};
34
35/// Initialize panic hook for better error messages
36#[wasm_bindgen(start)]
37pub fn init() {
38    console_error_panic_hook::set_once();
39    tracing_wasm::set_as_global_default();
40}
41
42/// Main GraphDB class for browser usage
43#[wasm_bindgen]
44pub struct GraphDB {
45    nodes: Arc<Mutex<HashMap<NodeId, Node>>>,
46    edges: Arc<Mutex<HashMap<EdgeId, Edge>>>,
47    hypergraph: Arc<Mutex<HypergraphIndex>>,
48    hyperedges: Arc<Mutex<HashMap<HyperedgeId, Hyperedge>>>,
49    // Index structures for efficient queries
50    labels_index: Arc<Mutex<HashMap<String, Vec<NodeId>>>>,
51    edge_types_index: Arc<Mutex<HashMap<String, Vec<EdgeId>>>>,
52    node_edges_out: Arc<Mutex<HashMap<NodeId, Vec<EdgeId>>>>,
53    node_edges_in: Arc<Mutex<HashMap<NodeId, Vec<EdgeId>>>>,
54    distance_metric: DistanceMetric,
55}
56
57#[wasm_bindgen]
58impl GraphDB {
59    /// Create a new GraphDB instance
60    ///
61    /// # Arguments
62    /// * `metric` - Distance metric for hypergraph embeddings ("euclidean", "cosine", "dotproduct", "manhattan")
63    #[wasm_bindgen(constructor)]
64    pub fn new(metric: Option<String>) -> Result<GraphDB, JsValue> {
65        let distance_metric = match metric.as_deref() {
66            Some("euclidean") => DistanceMetric::Euclidean,
67            Some("cosine") => DistanceMetric::Cosine,
68            Some("dotproduct") => DistanceMetric::DotProduct,
69            Some("manhattan") => DistanceMetric::Manhattan,
70            None => DistanceMetric::Cosine,
71            Some(other) => return Err(JsValue::from_str(&format!("Unknown metric: {}", other))),
72        };
73
74        Ok(GraphDB {
75            nodes: Arc::new(Mutex::new(HashMap::new())),
76            edges: Arc::new(Mutex::new(HashMap::new())),
77            hypergraph: Arc::new(Mutex::new(HypergraphIndex::new(distance_metric))),
78            hyperedges: Arc::new(Mutex::new(HashMap::new())),
79            labels_index: Arc::new(Mutex::new(HashMap::new())),
80            edge_types_index: Arc::new(Mutex::new(HashMap::new())),
81            node_edges_out: Arc::new(Mutex::new(HashMap::new())),
82            node_edges_in: Arc::new(Mutex::new(HashMap::new())),
83            distance_metric,
84        })
85    }
86
87    /// Execute a Cypher query (basic implementation)
88    ///
89    /// # Arguments
90    /// * `cypher` - Cypher query string
91    ///
92    /// # Returns
93    /// Promise<QueryResult> with matching nodes, edges, and hyperedges
94    #[wasm_bindgen]
95    pub async fn query(&self, cypher: String) -> Result<QueryResult, JsValue> {
96        console::log_1(&format!("Executing Cypher: {}", cypher).into());
97
98        // Parse and execute basic Cypher queries
99        // This is a simplified implementation - a full Cypher parser would be more complex
100        let result = self
101            .execute_cypher(&cypher)
102            .map_err(|e| JsValue::from(GraphError::from(e)))?;
103
104        Ok(result)
105    }
106
107    /// Create a new node
108    ///
109    /// # Arguments
110    /// * `labels` - Array of label strings
111    /// * `properties` - JavaScript object with node properties
112    ///
113    /// # Returns
114    /// Node ID
115    #[wasm_bindgen(js_name = createNode)]
116    pub fn create_node(&self, labels: Vec<String>, properties: JsValue) -> Result<String, JsValue> {
117        let id = Uuid::new_v4().to_string();
118        let props = js_object_to_hashmap(properties).map_err(|e| JsValue::from_str(&e))?;
119
120        // Extract embedding if present
121        let embedding = props
122            .get("embedding")
123            .and_then(|v| serde_json::from_value::<Vec<f32>>(v.clone()).ok());
124
125        let node = Node {
126            id: id.clone(),
127            labels: labels.clone(),
128            properties: props,
129            embedding: embedding.clone(),
130        };
131
132        // Store node
133        self.nodes.lock().insert(id.clone(), node);
134
135        // Update label index
136        let mut labels_index = self.labels_index.lock();
137        for label in &labels {
138            labels_index
139                .entry(label.clone())
140                .or_insert_with(Vec::new)
141                .push(id.clone());
142        }
143
144        // Add to hypergraph if embedding exists
145        if let Some(emb) = embedding {
146            self.hypergraph.lock().add_entity(id.clone(), emb);
147        }
148
149        // Initialize edge lists
150        self.node_edges_out.lock().insert(id.clone(), Vec::new());
151        self.node_edges_in.lock().insert(id.clone(), Vec::new());
152
153        Ok(id)
154    }
155
156    /// Create a new edge (relationship)
157    ///
158    /// # Arguments
159    /// * `from` - Source node ID
160    /// * `to` - Target node ID
161    /// * `edge_type` - Relationship type
162    /// * `properties` - JavaScript object with edge properties
163    ///
164    /// # Returns
165    /// Edge ID
166    #[wasm_bindgen(js_name = createEdge)]
167    pub fn create_edge(
168        &self,
169        from: String,
170        to: String,
171        edge_type: String,
172        properties: JsValue,
173    ) -> Result<String, JsValue> {
174        // Verify nodes exist
175        let nodes = self.nodes.lock();
176        if !nodes.contains_key(&from) {
177            return Err(JsValue::from_str(&format!("Node {} not found", from)));
178        }
179        if !nodes.contains_key(&to) {
180            return Err(JsValue::from_str(&format!("Node {} not found", to)));
181        }
182        drop(nodes);
183
184        let id = Uuid::new_v4().to_string();
185        let props = js_object_to_hashmap(properties).map_err(|e| JsValue::from_str(&e))?;
186
187        let edge = Edge {
188            id: id.clone(),
189            from: from.clone(),
190            to: to.clone(),
191            edge_type: edge_type.clone(),
192            properties: props,
193        };
194
195        // Store edge
196        self.edges.lock().insert(id.clone(), edge);
197
198        // Update indices
199        self.edge_types_index
200            .lock()
201            .entry(edge_type)
202            .or_insert_with(Vec::new)
203            .push(id.clone());
204
205        self.node_edges_out
206            .lock()
207            .entry(from)
208            .or_insert_with(Vec::new)
209            .push(id.clone());
210
211        self.node_edges_in
212            .lock()
213            .entry(to)
214            .or_insert_with(Vec::new)
215            .push(id.clone());
216
217        Ok(id)
218    }
219
220    /// Create a hyperedge (n-ary relationship)
221    ///
222    /// # Arguments
223    /// * `nodes` - Array of node IDs
224    /// * `description` - Natural language description of the relationship
225    /// * `embedding` - Optional embedding vector (auto-generated if not provided)
226    /// * `confidence` - Optional confidence score (0.0-1.0, defaults to 1.0)
227    ///
228    /// # Returns
229    /// Hyperedge ID
230    #[wasm_bindgen(js_name = createHyperedge)]
231    pub fn create_hyperedge(
232        &self,
233        nodes: Vec<String>,
234        description: String,
235        embedding: Option<Vec<f32>>,
236        confidence: Option<f32>,
237    ) -> Result<String, JsValue> {
238        // Verify all nodes exist
239        let nodes_map = self.nodes.lock();
240        for node_id in &nodes {
241            if !nodes_map.contains_key(node_id) {
242                return Err(JsValue::from_str(&format!("Node {} not found", node_id)));
243            }
244        }
245        drop(nodes_map);
246
247        let id = Uuid::new_v4().to_string();
248
249        // Generate embedding if not provided (use average of node embeddings)
250        let emb = if let Some(e) = embedding {
251            e
252        } else {
253            self.generate_hyperedge_embedding(&nodes)?
254        };
255
256        let conf = confidence.unwrap_or(1.0).clamp(0.0, 1.0);
257
258        let hyperedge = Hyperedge {
259            id: id.clone(),
260            nodes: nodes.clone(),
261            description: description.clone(),
262            embedding: emb.clone(),
263            confidence: conf,
264            properties: HashMap::new(),
265        };
266
267        // Create core hyperedge
268        let core_hyperedge = CoreHyperedge {
269            id: id.clone(),
270            nodes: nodes.clone(),
271            description,
272            embedding: emb,
273            confidence: conf,
274            metadata: HashMap::new(),
275        };
276
277        // Add to hypergraph index
278        self.hypergraph
279            .lock()
280            .add_hyperedge(core_hyperedge)
281            .map_err(|e| JsValue::from_str(&format!("Failed to add hyperedge: {}", e)))?;
282
283        // Store hyperedge
284        self.hyperedges.lock().insert(id.clone(), hyperedge);
285
286        Ok(id)
287    }
288
289    /// Get a node by ID
290    ///
291    /// # Arguments
292    /// * `id` - Node ID
293    ///
294    /// # Returns
295    /// JsNode or null if not found
296    #[wasm_bindgen(js_name = getNode)]
297    pub fn get_node(&self, id: String) -> Option<JsNode> {
298        self.nodes.lock().get(&id).map(|n| n.to_js())
299    }
300
301    /// Get an edge by ID
302    #[wasm_bindgen(js_name = getEdge)]
303    pub fn get_edge(&self, id: String) -> Option<JsEdge> {
304        self.edges.lock().get(&id).map(|e| e.to_js())
305    }
306
307    /// Get a hyperedge by ID
308    #[wasm_bindgen(js_name = getHyperedge)]
309    pub fn get_hyperedge(&self, id: String) -> Option<JsHyperedge> {
310        self.hyperedges.lock().get(&id).map(|h| h.to_js())
311    }
312
313    /// Delete a node by ID
314    ///
315    /// # Arguments
316    /// * `id` - Node ID
317    ///
318    /// # Returns
319    /// True if deleted, false if not found
320    #[wasm_bindgen(js_name = deleteNode)]
321    pub fn delete_node(&self, id: String) -> bool {
322        // Remove from nodes
323        let removed = self.nodes.lock().remove(&id).is_some();
324
325        if removed {
326            // Clean up indices
327            let mut labels_index = self.labels_index.lock();
328            for (_, node_ids) in labels_index.iter_mut() {
329                node_ids.retain(|nid| nid != &id);
330            }
331
332            // Remove associated edges
333            if let Some(out_edges) = self.node_edges_out.lock().remove(&id) {
334                for edge_id in out_edges {
335                    self.edges.lock().remove(&edge_id);
336                }
337            }
338            if let Some(in_edges) = self.node_edges_in.lock().remove(&id) {
339                for edge_id in in_edges {
340                    self.edges.lock().remove(&edge_id);
341                }
342            }
343        }
344
345        removed
346    }
347
348    /// Delete an edge by ID
349    #[wasm_bindgen(js_name = deleteEdge)]
350    pub fn delete_edge(&self, id: String) -> bool {
351        if let Some(edge) = self.edges.lock().remove(&id) {
352            // Clean up indices
353            if let Some(edges) = self.node_edges_out.lock().get_mut(&edge.from) {
354                edges.retain(|eid| eid != &id);
355            }
356            if let Some(edges) = self.node_edges_in.lock().get_mut(&edge.to) {
357                edges.retain(|eid| eid != &id);
358            }
359            true
360        } else {
361            false
362        }
363    }
364
365    /// Import Cypher statements
366    ///
367    /// # Arguments
368    /// * `statements` - Array of Cypher CREATE statements
369    ///
370    /// # Returns
371    /// Number of statements executed
372    #[wasm_bindgen(js_name = importCypher)]
373    pub async fn import_cypher(&self, statements: Vec<String>) -> Result<usize, JsValue> {
374        let mut count = 0;
375        for statement in statements {
376            self.execute_cypher(&statement)
377                .map_err(|e| JsValue::from_str(&e))?;
378            count += 1;
379        }
380        Ok(count)
381    }
382
383    /// Export database as Cypher CREATE statements
384    ///
385    /// # Returns
386    /// String containing Cypher statements
387    #[wasm_bindgen(js_name = exportCypher)]
388    pub fn export_cypher(&self) -> String {
389        let mut cypher = String::new();
390
391        // Export nodes
392        for (id, node) in self.nodes.lock().iter() {
393            let labels = if node.labels.is_empty() {
394                String::new()
395            } else {
396                format!(":{}", node.labels.join(":"))
397            };
398
399            let props = if node.properties.is_empty() {
400                String::new()
401            } else {
402                format!(
403                    " {{{}}}",
404                    node.properties
405                        .iter()
406                        .map(|(k, v)| format!("{}: {}", k, v))
407                        .collect::<Vec<_>>()
408                        .join(", ")
409                )
410            };
411
412            cypher.push_str(&format!("CREATE (n{}{})\n", labels, props));
413        }
414
415        // Export edges
416        for (id, edge) in self.edges.lock().iter() {
417            let props = if edge.properties.is_empty() {
418                String::new()
419            } else {
420                format!(
421                    " {{{}}}",
422                    edge.properties
423                        .iter()
424                        .map(|(k, v)| format!("{}: {}", k, v))
425                        .collect::<Vec<_>>()
426                        .join(", ")
427                )
428            };
429
430            cypher.push_str(&format!(
431                "MATCH (a), (b) WHERE id(a) = '{}' AND id(b) = '{}' CREATE (a)-[:{}{}]->(b)\n",
432                edge.from, edge.to, edge.edge_type, props
433            ));
434        }
435
436        cypher
437    }
438
439    /// Get database statistics
440    #[wasm_bindgen]
441    pub fn stats(&self) -> JsValue {
442        let node_count = self.nodes.lock().len();
443        let edge_count = self.edges.lock().len();
444        let hyperedge_count = self.hyperedges.lock().len();
445        let hypergraph_stats = self.hypergraph.lock().stats();
446
447        let obj = Object::new();
448        Reflect::set(&obj, &"nodeCount".into(), &JsValue::from(node_count)).unwrap();
449        Reflect::set(&obj, &"edgeCount".into(), &JsValue::from(edge_count)).unwrap();
450        Reflect::set(
451            &obj,
452            &"hyperedgeCount".into(),
453            &JsValue::from(hyperedge_count),
454        )
455        .unwrap();
456        Reflect::set(
457            &obj,
458            &"hypergraphEntities".into(),
459            &JsValue::from(hypergraph_stats.total_entities),
460        )
461        .unwrap();
462        Reflect::set(
463            &obj,
464            &"hypergraphEdges".into(),
465            &JsValue::from(hypergraph_stats.total_hyperedges),
466        )
467        .unwrap();
468        Reflect::set(
469            &obj,
470            &"avgEntityDegree".into(),
471            &JsValue::from(hypergraph_stats.avg_entity_degree),
472        )
473        .unwrap();
474
475        obj.into()
476    }
477}
478
479// Internal helper methods
480impl GraphDB {
481    fn execute_cypher(&self, cypher: &str) -> Result<QueryResult, String> {
482        let cypher = cypher.trim();
483
484        // Very basic Cypher parsing - in production, use a proper parser
485        if cypher.to_uppercase().starts_with("MATCH") {
486            self.execute_match_query(cypher)
487        } else if cypher.to_uppercase().starts_with("CREATE") {
488            self.execute_create_query(cypher)
489        } else {
490            Err(format!("Unsupported Cypher statement: {}", cypher))
491        }
492    }
493
494    fn execute_match_query(&self, _cypher: &str) -> Result<QueryResult, String> {
495        // Simplified MATCH implementation
496        // In production, parse the pattern and execute accordingly
497
498        Ok(QueryResult {
499            nodes: Vec::new(),
500            edges: Vec::new(),
501            hyperedges: Vec::new(),
502            data: Vec::new(),
503        })
504    }
505
506    fn execute_create_query(&self, _cypher: &str) -> Result<QueryResult, String> {
507        // Simplified CREATE implementation
508        // Parse CREATE statement and create nodes/relationships
509
510        Ok(QueryResult {
511            nodes: Vec::new(),
512            edges: Vec::new(),
513            hyperedges: Vec::new(),
514            data: Vec::new(),
515        })
516    }
517
518    fn generate_hyperedge_embedding(&self, node_ids: &[String]) -> Result<Vec<f32>, JsValue> {
519        let nodes = self.nodes.lock();
520        let embeddings: Vec<Vec<f32>> = node_ids
521            .iter()
522            .filter_map(|id| nodes.get(id).and_then(|n| n.embedding.clone()))
523            .collect();
524
525        if embeddings.is_empty() {
526            return Err(JsValue::from_str("No embeddings found for nodes"));
527        }
528
529        let dim = embeddings[0].len();
530        let mut avg_embedding = vec![0.0; dim];
531
532        for emb in &embeddings {
533            for (i, val) in emb.iter().enumerate() {
534                avg_embedding[i] += val;
535            }
536        }
537
538        for val in &mut avg_embedding {
539            *val /= embeddings.len() as f32;
540        }
541
542        Ok(avg_embedding)
543    }
544}
545
546/// Get version information
547#[wasm_bindgen]
548pub fn version() -> String {
549    env!("CARGO_PKG_VERSION").to_string()
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use wasm_bindgen_test::*;
556
557    wasm_bindgen_test_configure!(run_in_browser);
558
559    #[wasm_bindgen_test]
560    fn test_version() {
561        assert!(!version().is_empty());
562    }
563
564    #[wasm_bindgen_test]
565    fn test_graph_creation() {
566        let db = GraphDB::new(Some("cosine".to_string())).unwrap();
567        assert!(true); // Basic smoke test
568    }
569}