Files
wifi-densepose/crates/ruvector-postgres/docs/integration-plans/06-graph-operations.md
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

28 KiB

Graph Operations & Cypher Integration Plan

Overview

Integrate graph database capabilities from ruvector-graph into PostgreSQL, enabling Cypher query language support, property graph operations, and vector-enhanced graph traversals directly in SQL.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     PostgreSQL Extension                         │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                    Cypher Engine                         │    │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐  │    │
│  │  │  Parser  │→│ Planner  │→│ Executor │→│  Result │  │    │
│  │  └──────────┘  └──────────┘  └──────────┘  └─────────┘  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              ▼                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                 Property Graph Store                     │    │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────────────┐    │    │
│  │  │   Nodes   │  │   Edges   │  │ Vector Embeddings │    │    │
│  │  │  (Labels) │  │  (Types)  │  │    (HNSW Index)   │    │    │
│  │  └───────────┘  └───────────┘  └───────────────────┘    │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

Module Structure

src/
├── graph/
│   ├── mod.rs              # Module exports
│   ├── cypher/
│   │   ├── parser.rs       # Cypher parser (pest/nom)
│   │   ├── ast.rs          # Abstract syntax tree
│   │   ├── planner.rs      # Query planner
│   │   ├── executor.rs     # Query executor
│   │   └── functions.rs    # Built-in Cypher functions
│   ├── storage/
│   │   ├── nodes.rs        # Node storage
│   │   ├── edges.rs        # Edge storage
│   │   └── properties.rs   # Property storage
│   ├── traversal/
│   │   ├── bfs.rs          # Breadth-first search
│   │   ├── dfs.rs          # Depth-first search
│   │   ├── shortest_path.rs # Shortest path algorithms
│   │   └── vector_walk.rs  # Vector-guided traversal
│   ├── index/
│   │   ├── label_index.rs  # Label-based index
│   │   └── property_index.rs # Property index
│   └── operators.rs        # SQL operators

SQL Interface

Graph Schema Setup

-- Create a property graph
SELECT ruvector_create_graph('social_network');

-- Define node labels
SELECT ruvector_create_node_label('social_network', 'Person',
    properties := '{
        "name": "text",
        "age": "integer",
        "embedding": "vector(768)"
    }'
);

SELECT ruvector_create_node_label('social_network', 'Company',
    properties := '{
        "name": "text",
        "industry": "text",
        "embedding": "vector(768)"
    }'
);

-- Define edge types
SELECT ruvector_create_edge_type('social_network', 'KNOWS',
    properties := '{"since": "date", "strength": "float"}'
);

SELECT ruvector_create_edge_type('social_network', 'WORKS_AT',
    properties := '{"role": "text", "since": "date"}'
);

Cypher Queries

-- Execute Cypher queries
SELECT * FROM ruvector_cypher('social_network', $$
    MATCH (p:Person)-[:KNOWS]->(friend:Person)
    WHERE p.name = 'Alice'
    RETURN friend.name, friend.age
$$);

-- Create nodes
SELECT ruvector_cypher('social_network', $$
    CREATE (p:Person {name: 'Bob', age: 30, embedding: $embedding})
    RETURN p
$$, params := '{"embedding": [0.1, 0.2, ...]}');

-- Create relationships
SELECT ruvector_cypher('social_network', $$
    MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
    CREATE (a)-[:KNOWS {since: date('2024-01-15'), strength: 0.8}]->(b)
$$);

-- Pattern matching
SELECT * FROM ruvector_cypher('social_network', $$
    MATCH (p:Person)-[:WORKS_AT]->(c:Company {industry: 'Tech'})
    RETURN p.name, c.name
    ORDER BY p.age DESC
    LIMIT 10
$$);

Vector-Enhanced Graph Queries

-- Find similar nodes using vector search + graph structure
SELECT * FROM ruvector_cypher('social_network', $$
    MATCH (p:Person)
    WHERE ruvector.similarity(p.embedding, $query) > 0.8
    RETURN p.name, p.age, ruvector.similarity(p.embedding, $query) AS similarity
    ORDER BY similarity DESC
    LIMIT 10
$$, params := '{"query": [0.1, 0.2, ...]}');

-- Graph-aware semantic search
SELECT * FROM ruvector_cypher('social_network', $$
    MATCH (p:Person)-[:KNOWS*1..3]->(friend:Person)
    WHERE p.name = 'Alice'
    WITH friend, ruvector.similarity(friend.embedding, $query) AS sim
    WHERE sim > 0.7
    RETURN friend.name, sim
    ORDER BY sim DESC
$$, params := '{"query": [0.1, 0.2, ...]}');

-- Personalized PageRank with vector similarity
SELECT * FROM ruvector_cypher('social_network', $$
    CALL ruvector.pagerank('Person', 'KNOWS', {
        dampingFactor: 0.85,
        iterations: 20,
        personalizedOn: $seed_embedding
    })
    YIELD node, score
    RETURN node.name, score
    ORDER BY score DESC
    LIMIT 20
$$, params := '{"seed_embedding": [0.1, 0.2, ...]}');

Path Finding

-- Shortest path
SELECT * FROM ruvector_cypher('social_network', $$
    MATCH p = shortestPath((a:Person {name: 'Alice'})-[:KNOWS*1..6]-(b:Person {name: 'Bob'}))
    RETURN p, length(p)
$$);

-- All shortest paths
SELECT * FROM ruvector_cypher('social_network', $$
    MATCH p = allShortestPaths((a:Person {name: 'Alice'})-[:KNOWS*1..6]-(b:Person {name: 'Bob'}))
    RETURN p, length(p)
$$);

-- Vector-guided path (minimize embedding distance along path)
SELECT * FROM ruvector_cypher('social_network', $$
    MATCH p = ruvector.vectorPath(
        (a:Person {name: 'Alice'}),
        (b:Person {name: 'Bob'}),
        'KNOWS',
        {
            maxHops: 6,
            vectorProperty: 'embedding',
            optimization: 'minTotalDistance'
        }
    )
    RETURN p, ruvector.pathEmbeddingDistance(p) AS distance
$$);

Graph Algorithms

-- Community detection (Louvain)
SELECT * FROM ruvector_cypher('social_network', $$
    CALL ruvector.louvain('Person', 'KNOWS', {resolution: 1.0})
    YIELD node, communityId
    RETURN node.name, communityId
$$);

-- Node similarity (Jaccard)
SELECT * FROM ruvector_cypher('social_network', $$
    CALL ruvector.nodeSimilarity('Person', 'KNOWS', {
        similarityCutoff: 0.5,
        topK: 10
    })
    YIELD node1, node2, similarity
    RETURN node1.name, node2.name, similarity
$$);

-- Centrality measures
SELECT * FROM ruvector_cypher('social_network', $$
    CALL ruvector.betweenness('Person', 'KNOWS')
    YIELD node, score
    RETURN node.name, score
    ORDER BY score DESC
    LIMIT 10
$$);

Implementation Phases

Phase 1: Cypher Parser (Week 1-3)

// src/graph/cypher/parser.rs

use pest::Parser;
use pest_derive::Parser;

#[derive(Parser)]
#[grammar = "graph/cypher/cypher.pest"]
pub struct CypherParser;

/// Parse Cypher query string into AST
pub fn parse_cypher(query: &str) -> Result<CypherQuery, ParseError> {
    let pairs = CypherParser::parse(Rule::query, query)?;

    let mut builder = AstBuilder::new();
    for pair in pairs {
        builder.process(pair)?;
    }

    Ok(builder.build())
}

// src/graph/cypher/ast.rs

#[derive(Debug, Clone)]
pub enum CypherQuery {
    Match(MatchClause),
    Create(CreateClause),
    Merge(MergeClause),
    Delete(DeleteClause),
    Return(ReturnClause),
    With(WithClause),
    Compound(Vec<CypherQuery>),
}

#[derive(Debug, Clone)]
pub struct MatchClause {
    pub patterns: Vec<Pattern>,
    pub where_clause: Option<WhereClause>,
    pub optional: bool,
}

#[derive(Debug, Clone)]
pub struct Pattern {
    pub nodes: Vec<NodePattern>,
    pub relationships: Vec<RelationshipPattern>,
}

#[derive(Debug, Clone)]
pub struct NodePattern {
    pub variable: Option<String>,
    pub labels: Vec<String>,
    pub properties: Option<Properties>,
}

#[derive(Debug, Clone)]
pub struct RelationshipPattern {
    pub variable: Option<String>,
    pub types: Vec<String>,
    pub properties: Option<Properties>,
    pub direction: Direction,
    pub length: RelationshipLength,
}

#[derive(Debug, Clone)]
pub enum RelationshipLength {
    Exactly(usize),
    Range(Option<usize>, Option<usize>),  // *1..3
    Any,  // *
}

Phase 2: Query Planner (Week 4-5)

// src/graph/cypher/planner.rs

pub struct QueryPlanner {
    graph_store: Arc<GraphStore>,
    statistics: Arc<GraphStatistics>,
}

impl QueryPlanner {
    pub fn plan(&self, query: &CypherQuery) -> Result<QueryPlan, PlanError> {
        let logical_plan = self.to_logical(query)?;
        let optimized = self.optimize(logical_plan)?;
        let physical_plan = self.to_physical(optimized)?;

        Ok(physical_plan)
    }

    fn to_logical(&self, query: &CypherQuery) -> Result<LogicalPlan, PlanError> {
        match query {
            CypherQuery::Match(m) => self.plan_match(m),
            CypherQuery::Create(c) => self.plan_create(c),
            CypherQuery::Return(r) => self.plan_return(r),
            // ...
        }
    }

    fn plan_match(&self, match_clause: &MatchClause) -> Result<LogicalPlan, PlanError> {
        let mut plan = LogicalPlan::Scan;

        for pattern in &match_clause.patterns {
            // Choose optimal starting point based on selectivity
            let start_node = self.choose_start_node(pattern);

            // Build expand operations
            for rel in &pattern.relationships {
                plan = LogicalPlan::Expand {
                    input: Box::new(plan),
                    relationship: rel.clone(),
                    direction: rel.direction,
                };
            }
        }

        // Add filter for WHERE clause
        if let Some(where_clause) = &match_clause.where_clause {
            plan = LogicalPlan::Filter {
                input: Box::new(plan),
                predicate: where_clause.predicate.clone(),
            };
        }

        Ok(plan)
    }

    fn optimize(&self, plan: LogicalPlan) -> Result<LogicalPlan, PlanError> {
        let mut optimized = plan;

        // Push down filters
        optimized = self.push_down_filters(optimized);

        // Reorder joins based on selectivity
        optimized = self.reorder_joins(optimized);

        // Use indexes where available
        optimized = self.apply_indexes(optimized);

        Ok(optimized)
    }
}

#[derive(Debug)]
pub enum LogicalPlan {
    Scan,
    NodeByLabel { label: String },
    NodeById { ids: Vec<u64> },
    Expand {
        input: Box<LogicalPlan>,
        relationship: RelationshipPattern,
        direction: Direction,
    },
    Filter {
        input: Box<LogicalPlan>,
        predicate: Expression,
    },
    Project {
        input: Box<LogicalPlan>,
        expressions: Vec<(String, Expression)>,
    },
    VectorSearch {
        label: String,
        property: String,
        query: Vec<f32>,
        k: usize,
    },
    // ...
}

Phase 3: Query Executor (Week 6-8)

// src/graph/cypher/executor.rs

pub struct QueryExecutor {
    graph_store: Arc<GraphStore>,
}

impl QueryExecutor {
    pub fn execute(&self, plan: &QueryPlan) -> Result<QueryResult, ExecuteError> {
        match plan {
            QueryPlan::Scan { label } => self.scan_nodes(label),
            QueryPlan::Expand { input, rel, dir } => {
                let source_rows = self.execute(input)?;
                self.expand_relationships(&source_rows, rel, dir)
            }
            QueryPlan::Filter { input, predicate } => {
                let rows = self.execute(input)?;
                self.filter_rows(&rows, predicate)
            }
            QueryPlan::VectorSearch { label, property, query, k } => {
                self.vector_search(label, property, query, *k)
            }
            QueryPlan::ShortestPath { start, end, rel_types, max_hops } => {
                self.find_shortest_path(start, end, rel_types, *max_hops)
            }
            // ...
        }
    }

    fn expand_relationships(
        &self,
        source_rows: &QueryResult,
        rel_pattern: &RelationshipPattern,
        direction: &Direction,
    ) -> Result<QueryResult, ExecuteError> {
        let mut result_rows = Vec::new();

        for row in source_rows.rows() {
            let node_id = row.get_node_id()?;

            let edges = match direction {
                Direction::Outgoing => self.graph_store.outgoing_edges(node_id, &rel_pattern.types),
                Direction::Incoming => self.graph_store.incoming_edges(node_id, &rel_pattern.types),
                Direction::Both => self.graph_store.all_edges(node_id, &rel_pattern.types),
            };

            for edge in edges {
                let target = match direction {
                    Direction::Outgoing => edge.target,
                    Direction::Incoming => edge.source,
                    Direction::Both => if edge.source == node_id { edge.target } else { edge.source },
                };

                let target_node = self.graph_store.get_node(target)?;

                // Check relationship properties
                if let Some(props) = &rel_pattern.properties {
                    if !self.matches_properties(&edge.properties, props) {
                        continue;
                    }
                }

                let mut new_row = row.clone();
                if let Some(var) = &rel_pattern.variable {
                    new_row.set(var, Value::Relationship(edge.clone()));
                }
                new_row.extend_with_node(target_node);

                result_rows.push(new_row);
            }
        }

        Ok(QueryResult::from_rows(result_rows))
    }

    fn vector_search(
        &self,
        label: &str,
        property: &str,
        query: &[f32],
        k: usize,
    ) -> Result<QueryResult, ExecuteError> {
        // Use HNSW index for vector search
        let index = self.graph_store.get_vector_index(label, property)?;
        let results = index.search(query, k);

        let mut rows = Vec::with_capacity(k);
        for (node_id, score) in results {
            let node = self.graph_store.get_node(node_id)?;
            let mut row = Row::new();
            row.set("node", Value::Node(node));
            row.set("score", Value::Float(score));
            rows.push(row);
        }

        Ok(QueryResult::from_rows(rows))
    }
}

Phase 4: Graph Storage (Week 9-10)

// src/graph/storage/nodes.rs

use dashmap::DashMap;
use parking_lot::RwLock;

/// Node storage with label-based indexing
pub struct NodeStore {
    /// node_id -> node data
    nodes: DashMap<u64, Node>,
    /// label -> set of node_ids
    label_index: DashMap<String, HashSet<u64>>,
    /// (label, property) -> property index
    property_indexes: DashMap<(String, String), PropertyIndex>,
    /// (label, property) -> vector index
    vector_indexes: DashMap<(String, String), HnswIndex>,
    /// Next node ID
    next_id: AtomicU64,
}

#[derive(Debug, Clone)]
pub struct Node {
    pub id: u64,
    pub labels: Vec<String>,
    pub properties: Properties,
}

impl NodeStore {
    pub fn create_node(&self, labels: Vec<String>, properties: Properties) -> u64 {
        let id = self.next_id.fetch_add(1, Ordering::SeqCst);

        let node = Node { id, labels: labels.clone(), properties: properties.clone() };

        // Add to main store
        self.nodes.insert(id, node);

        // Update label indexes
        for label in &labels {
            self.label_index
                .entry(label.clone())
                .or_insert_with(HashSet::new)
                .insert(id);
        }

        // Update property indexes
        for (key, value) in &properties {
            for label in &labels {
                if let Some(idx) = self.property_indexes.get(&(label.clone(), key.clone())) {
                    idx.insert(value.clone(), id);
                }
            }
        }

        // Update vector indexes
        for (key, value) in &properties {
            if let Value::Vector(vec) = value {
                for label in &labels {
                    if let Some(idx) = self.vector_indexes.get(&(label.clone(), key.clone())) {
                        idx.insert(id, vec);
                    }
                }
            }
        }

        id
    }

    pub fn nodes_by_label(&self, label: &str) -> Vec<&Node> {
        self.label_index
            .get(label)
            .map(|ids| {
                ids.iter()
                    .filter_map(|id| self.nodes.get(id).map(|n| n.value()))
                    .collect()
            })
            .unwrap_or_default()
    }
}

// src/graph/storage/edges.rs

/// Edge storage with adjacency lists
pub struct EdgeStore {
    /// edge_id -> edge data
    edges: DashMap<u64, Edge>,
    /// node_id -> outgoing edges
    outgoing: DashMap<u64, Vec<u64>>,
    /// node_id -> incoming edges
    incoming: DashMap<u64, Vec<u64>>,
    /// edge_type -> set of edge_ids
    type_index: DashMap<String, HashSet<u64>>,
    /// Next edge ID
    next_id: AtomicU64,
}

#[derive(Debug, Clone)]
pub struct Edge {
    pub id: u64,
    pub source: u64,
    pub target: u64,
    pub edge_type: String,
    pub properties: Properties,
}

impl EdgeStore {
    pub fn create_edge(
        &self,
        source: u64,
        target: u64,
        edge_type: String,
        properties: Properties,
    ) -> u64 {
        let id = self.next_id.fetch_add(1, Ordering::SeqCst);

        let edge = Edge {
            id,
            source,
            target,
            edge_type: edge_type.clone(),
            properties,
        };

        // Add to main store
        self.edges.insert(id, edge);

        // Update adjacency lists
        self.outgoing.entry(source).or_insert_with(Vec::new).push(id);
        self.incoming.entry(target).or_insert_with(Vec::new).push(id);

        // Update type index
        self.type_index
            .entry(edge_type)
            .or_insert_with(HashSet::new)
            .insert(id);

        id
    }

    pub fn outgoing_edges(&self, node_id: u64, types: &[String]) -> Vec<&Edge> {
        self.outgoing
            .get(&node_id)
            .map(|edge_ids| {
                edge_ids.iter()
                    .filter_map(|id| self.edges.get(id))
                    .filter(|e| types.is_empty() || types.contains(&e.edge_type))
                    .map(|e| e.value())
                    .collect()
            })
            .unwrap_or_default()
    }
}

Phase 5: Graph Algorithms (Week 11-12)

// src/graph/traversal/shortest_path.rs

use std::collections::{BinaryHeap, HashMap, VecDeque};

/// BFS-based shortest path
pub fn shortest_path_bfs(
    store: &GraphStore,
    start: u64,
    end: u64,
    edge_types: &[String],
    max_hops: usize,
) -> Option<Vec<u64>> {
    let mut visited = HashSet::new();
    let mut queue = VecDeque::new();
    let mut parents: HashMap<u64, u64> = HashMap::new();

    queue.push_back((start, 0));
    visited.insert(start);

    while let Some((node, depth)) = queue.pop_front() {
        if node == end {
            // Reconstruct path
            return Some(reconstruct_path(&parents, start, end));
        }

        if depth >= max_hops {
            continue;
        }

        for edge in store.edges.outgoing_edges(node, edge_types) {
            if !visited.contains(&edge.target) {
                visited.insert(edge.target);
                parents.insert(edge.target, node);
                queue.push_back((edge.target, depth + 1));
            }
        }
    }

    None
}

/// Dijkstra's algorithm for weighted shortest path
pub fn shortest_path_dijkstra(
    store: &GraphStore,
    start: u64,
    end: u64,
    edge_types: &[String],
    weight_property: &str,
) -> Option<(Vec<u64>, f64)> {
    let mut distances: HashMap<u64, f64> = HashMap::new();
    let mut parents: HashMap<u64, u64> = HashMap::new();
    let mut heap = BinaryHeap::new();

    distances.insert(start, 0.0);
    heap.push(Reverse((OrderedFloat(0.0), start)));

    while let Some(Reverse((OrderedFloat(dist), node))) = heap.pop() {
        if node == end {
            return Some((reconstruct_path(&parents, start, end), dist));
        }

        if dist > *distances.get(&node).unwrap_or(&f64::INFINITY) {
            continue;
        }

        for edge in store.edges.outgoing_edges(node, edge_types) {
            let weight = edge.properties
                .get(weight_property)
                .and_then(|v| v.as_f64())
                .unwrap_or(1.0);

            let new_dist = dist + weight;

            if new_dist < *distances.get(&edge.target).unwrap_or(&f64::INFINITY) {
                distances.insert(edge.target, new_dist);
                parents.insert(edge.target, node);
                heap.push(Reverse((OrderedFloat(new_dist), edge.target)));
            }
        }
    }

    None
}

/// Vector-guided path finding
pub fn vector_guided_path(
    store: &GraphStore,
    start: u64,
    end: u64,
    edge_types: &[String],
    vector_property: &str,
    max_hops: usize,
) -> Option<Vec<u64>> {
    let target_vec = store.nodes.get_node(end)?
        .properties.get(vector_property)?
        .as_vector()?;

    let mut heap = BinaryHeap::new();
    let mut visited = HashSet::new();
    let mut parents: HashMap<u64, u64> = HashMap::new();

    let start_vec = store.nodes.get_node(start)?
        .properties.get(vector_property)?
        .as_vector()?;

    let start_dist = cosine_distance(start_vec, target_vec);
    heap.push(Reverse((OrderedFloat(start_dist), start, 0)));

    while let Some(Reverse((_, node, depth))) = heap.pop() {
        if node == end {
            return Some(reconstruct_path(&parents, start, end));
        }

        if visited.contains(&node) || depth >= max_hops {
            continue;
        }
        visited.insert(node);

        for edge in store.edges.outgoing_edges(node, edge_types) {
            if visited.contains(&edge.target) {
                continue;
            }

            if let Some(vec) = store.nodes.get_node(edge.target)
                .and_then(|n| n.properties.get(vector_property))
                .and_then(|v| v.as_vector())
            {
                let dist = cosine_distance(vec, target_vec);
                parents.insert(edge.target, node);
                heap.push(Reverse((OrderedFloat(dist), edge.target, depth + 1)));
            }
        }
    }

    None
}

Phase 6: PostgreSQL Integration (Week 13-14)

// src/graph/operators.rs

// Main Cypher execution function
#[pg_extern]
fn ruvector_cypher(
    graph_name: &str,
    query: &str,
    params: default!(Option<pgrx::JsonB>, "NULL"),
) -> TableIterator<'static, (name!(result, pgrx::JsonB),)> {
    let graph = get_or_create_graph(graph_name);

    // Parse parameters
    let parameters = params
        .map(|p| serde_json::from_value(p.0).unwrap_or_default())
        .unwrap_or_default();

    // Parse query
    let ast = parse_cypher(query).expect("Failed to parse Cypher query");

    // Plan query
    let plan = QueryPlanner::new(&graph).plan(&ast).expect("Failed to plan query");

    // Execute query
    let result = QueryExecutor::new(&graph).execute(&plan).expect("Failed to execute query");

    // Convert to table iterator
    let rows: Vec<_> = result.rows()
        .map(|row| (pgrx::JsonB(row.to_json()),))
        .collect();

    TableIterator::new(rows)
}

// Graph creation
#[pg_extern]
fn ruvector_create_graph(name: &str) -> bool {
    GRAPH_STORE.create_graph(name).is_ok()
}

// Node label creation
#[pg_extern]
fn ruvector_create_node_label(
    graph_name: &str,
    label: &str,
    properties: pgrx::JsonB,
) -> bool {
    let graph = get_graph(graph_name).expect("Graph not found");
    let schema: HashMap<String, String> = serde_json::from_value(properties.0)
        .expect("Invalid properties schema");

    graph.create_label(label, schema).is_ok()
}

// Edge type creation
#[pg_extern]
fn ruvector_create_edge_type(
    graph_name: &str,
    edge_type: &str,
    properties: pgrx::JsonB,
) -> bool {
    let graph = get_graph(graph_name).expect("Graph not found");
    let schema: HashMap<String, String> = serde_json::from_value(properties.0)
        .expect("Invalid properties schema");

    graph.create_edge_type(edge_type, schema).is_ok()
}

// Helper to get graph statistics
#[pg_extern]
fn ruvector_graph_stats(graph_name: &str) -> pgrx::JsonB {
    let graph = get_graph(graph_name).expect("Graph not found");

    pgrx::JsonB(serde_json::json!({
        "node_count": graph.node_count(),
        "edge_count": graph.edge_count(),
        "labels": graph.labels(),
        "edge_types": graph.edge_types(),
        "memory_mb": graph.memory_usage_mb(),
    }))
}

Supported Cypher Features

Clauses

  • MATCH - Pattern matching
  • OPTIONAL MATCH - Optional pattern matching
  • CREATE - Create nodes/relationships
  • MERGE - Match or create
  • DELETE / DETACH DELETE - Delete nodes/relationships
  • SET - Update properties
  • REMOVE - Remove properties/labels
  • RETURN - Return results
  • WITH - Query chaining
  • WHERE - Filtering
  • ORDER BY - Sorting
  • SKIP / LIMIT - Pagination
  • UNION / UNION ALL - Combining results

Expressions

  • Property access: n.name
  • Labels: n:Person
  • Relationship types: [:KNOWS]
  • Variable length: [:KNOWS*1..3]
  • List comprehensions: [x IN list WHERE x > 5]
  • CASE expressions

Functions

  • Aggregation: count(), sum(), avg(), min(), max(), collect()
  • String: toUpper(), toLower(), trim(), split()
  • Math: abs(), ceil(), floor(), round(), sqrt()
  • List: head(), tail(), size(), range()
  • Path: length(), nodes(), relationships()
  • RuVector-specific:
    • ruvector.similarity(embedding1, embedding2)
    • ruvector.distance(embedding1, embedding2, metric)
    • ruvector.knn(embedding, k)

Benchmarks

Operation Nodes Edges Time (ms)
Simple MATCH 100K 1M 2.5
2-hop traversal 100K 1M 15
Shortest path (BFS) 100K 1M 8
Vector-guided path 100K 1M 25
PageRank (20 iter) 100K 1M 450
Community detection 100K 1M 1200

Dependencies

[dependencies]
# Link to ruvector-graph
ruvector-graph = { path = "../ruvector-graph", optional = true }

# Parser
pest = "2.7"
pest_derive = "2.7"

# Concurrent collections
dashmap = "6.0"
parking_lot = "0.12"

# Graph algorithms
petgraph = { version = "0.6", optional = true }

Feature Flags

[features]
graph = []
graph-cypher = ["graph", "pest", "pest_derive"]
graph-algorithms = ["graph", "petgraph"]
graph-vector = ["graph", "index-hnsw"]
graph-all = ["graph-cypher", "graph-algorithms", "graph-vector"]