Skip to main content

velesdb_core/collection/
graph_collection.rs

1//! `GraphCollection`: knowledge graph with optional node embeddings.
2//!
3//! # Design
4//!
5//! `GraphCollection` is a pure newtype over `Collection` (C-02).
6//! All graph state (edge store, property/range indexes, node payloads, optional
7//! HNSW for node embeddings) lives inside the single `inner: Collection`.
8//! The graph schema and embedding dimension are persisted in `config.json`.
9//! There are no separate engine fields — no dual-storage risk.
10
11use std::collections::HashMap;
12use std::path::PathBuf;
13
14use crate::collection::graph::{GraphEdge, GraphSchema, TraversalConfig, TraversalResult};
15use crate::collection::types::Collection;
16use crate::distance::DistanceMetric;
17use crate::error::Result;
18use crate::point::SearchResult;
19
20/// A graph collection storing typed relationships between nodes.
21///
22/// Node embeddings are optional: if `dimension` is `None`, no vector index is created.
23///
24/// # Examples
25///
26/// ```rust,no_run
27/// use velesdb_core::{GraphCollection, GraphSchema, GraphEdge, DistanceMetric};
28///
29/// let coll = GraphCollection::create(
30///     "./data/kg".into(),
31///     "knowledge",
32///     None,                    // no embeddings
33///     DistanceMetric::Cosine,  // unused when no embeddings
34///     GraphSchema::schemaless(),
35/// )?;
36///
37/// let edge = GraphEdge::new(1, 100, 200, "KNOWS")?;
38/// coll.add_edge(edge)?;
39/// # Ok::<(), velesdb_core::Error>(())
40/// ```
41#[derive(Clone)]
42pub struct GraphCollection {
43    /// Single source of truth — all graph state lives here (C-02 pure newtype).
44    pub(crate) inner: Collection,
45}
46
47impl GraphCollection {
48    // -------------------------------------------------------------------------
49    // Lifecycle
50    // -------------------------------------------------------------------------
51
52    /// Creates a new `GraphCollection`.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error if the directory cannot be created or storage fails.
57    pub fn create(
58        path: PathBuf,
59        name: &str,
60        dimension: Option<usize>,
61        metric: DistanceMetric,
62        schema: GraphSchema,
63    ) -> Result<Self> {
64        Ok(Self {
65            inner: Collection::create_graph_collection(path, name, schema, dimension, metric)?,
66        })
67    }
68
69    /// Opens an existing `GraphCollection` from disk.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if config or storage cannot be opened.
74    pub fn open(path: PathBuf) -> Result<Self> {
75        Ok(Self {
76            inner: Collection::open(path)?,
77        })
78    }
79
80    /// Flushes all state to disk.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if any flush operation fails.
85    pub fn flush(&self) -> Result<()> {
86        self.inner.flush()
87    }
88
89    // -------------------------------------------------------------------------
90    // Metadata
91    // -------------------------------------------------------------------------
92
93    /// Returns the collection name.
94    #[must_use]
95    pub fn name(&self) -> String {
96        self.inner.config().name
97    }
98
99    /// Returns the graph schema stored in config.
100    ///
101    /// Returns `GraphSchema::schemaless()` for collections that have no schema set.
102    #[must_use]
103    pub fn schema(&self) -> GraphSchema {
104        self.inner
105            .graph_schema()
106            .unwrap_or_else(GraphSchema::schemaless)
107    }
108
109    /// Returns `true` if this collection stores node embeddings.
110    #[must_use]
111    pub fn has_embeddings(&self) -> bool {
112        self.inner.has_embeddings()
113    }
114
115    // -------------------------------------------------------------------------
116    // Graph operations — delegate to Collection graph API
117    // -------------------------------------------------------------------------
118
119    /// Adds an edge between two nodes.
120    ///
121    /// # Errors
122    ///
123    /// Returns `Error::EdgeExists` if an edge with the same ID already exists.
124    pub fn add_edge(&self, edge: GraphEdge) -> Result<()> {
125        self.inner.add_edge(edge)
126    }
127
128    /// Returns edges, optionally filtered by label.
129    #[must_use]
130    pub fn get_edges(&self, label: Option<&str>) -> Vec<GraphEdge> {
131        match label {
132            Some(lbl) => self.inner.get_edges_by_label(lbl),
133            None => self.inner.get_all_edges(),
134        }
135    }
136
137    /// Returns all outgoing edges from a node.
138    #[must_use]
139    pub fn get_outgoing(&self, node_id: u64) -> Vec<GraphEdge> {
140        self.inner.get_outgoing_edges(node_id)
141    }
142
143    /// Returns all incoming edges to a node.
144    #[must_use]
145    pub fn get_incoming(&self, node_id: u64) -> Vec<GraphEdge> {
146        self.inner.get_incoming_edges(node_id)
147    }
148
149    /// Returns `(in_degree, out_degree)` for a node.
150    #[must_use]
151    pub fn node_degree(&self, node_id: u64) -> (usize, usize) {
152        self.inner.get_node_degree(node_id)
153    }
154
155    /// Performs BFS traversal from a source node.
156    #[must_use]
157    pub fn traverse_bfs(&self, source_id: u64, config: &TraversalConfig) -> Vec<TraversalResult> {
158        self.inner.traverse_bfs_config(source_id, config)
159    }
160
161    /// Performs DFS traversal from a source node.
162    #[must_use]
163    pub fn traverse_dfs(&self, source_id: u64, config: &TraversalConfig) -> Vec<TraversalResult> {
164        self.inner.traverse_dfs_config(source_id, config)
165    }
166
167    // -------------------------------------------------------------------------
168    // Payload / node properties
169    // -------------------------------------------------------------------------
170
171    /// Stores node payload (properties).
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if storage fails.
176    pub fn store_node_payload(&self, node_id: u64, payload: &serde_json::Value) -> Result<()> {
177        self.inner.store_node_payload(node_id, payload)
178    }
179
180    /// Retrieves node payload.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if retrieval fails.
185    pub fn get_node_payload(&self, node_id: u64) -> Result<Option<serde_json::Value>> {
186        self.inner.get_node_payload(node_id)
187    }
188
189    // -------------------------------------------------------------------------
190    // Optional embedding search
191    // -------------------------------------------------------------------------
192
193    /// Searches for similar nodes by embedding (only available if `has_embeddings()`).
194    ///
195    /// # Errors
196    ///
197    /// Returns `Error::VectorNotAllowed` if this collection has no embeddings,
198    /// or `Error::DimensionMismatch` if the query dimension is wrong.
199    pub fn search_by_embedding(&self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
200        self.inner.search_by_embedding(query, k)
201    }
202
203    // -------------------------------------------------------------------------
204    // VelesQL
205    // -------------------------------------------------------------------------
206
207    /// Executes a `VelesQL` query.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the query is invalid or execution fails.
212    pub fn execute_query(
213        &self,
214        query: &crate::velesql::Query,
215        params: &HashMap<String, serde_json::Value>,
216    ) -> Result<Vec<SearchResult>> {
217        self.inner.execute_query(query, params)
218    }
219}