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}