tauri_plugin_velesdb/lib.rs
1// Tauri plugin - pedantic/nursery lints relaxed
2#![allow(clippy::pedantic)]
3#![allow(clippy::nursery)]
4
5//! # tauri-plugin-velesdb
6//!
7//! A Tauri plugin for `VelesDB` - Vector search in desktop applications.
8//!
9//! This plugin provides seamless integration of `VelesDB`'s vector database
10//! capabilities into Tauri desktop applications.
11//!
12//! ## Features
13//!
14//! - **Collection Management**: Create, list, and delete vector collections
15//! - **Vector Operations**: Insert, update, and delete vectors with payloads
16//! - **Vector Search**: Fast similarity search with multiple distance metrics
17//! - **Text Search**: BM25 full-text search across payloads
18//! - **Hybrid Search**: Combined vector + text search with RRF fusion
19//! - **`VelesQL`**: SQL-like query language for advanced searches
20//!
21//! ## Usage
22//!
23//! ### Rust (Plugin Registration)
24//!
25//! ```rust,ignore
26//! fn main() {
27//! tauri::Builder::default()
28//! .plugin(tauri_plugin_velesdb::init("./data"))
29//! .run(tauri::generate_context!())
30//! .expect("error while running tauri application");
31//! }
32//! ```
33//!
34//! ### JavaScript (Frontend)
35//!
36//! ```javascript
37//! import { invoke } from '@tauri-apps/api/core';
38//!
39//! // Create a collection
40//! await invoke('plugin:velesdb|create_collection', {
41//! request: { name: 'documents', dimension: 768, metric: 'cosine' }
42//! });
43//!
44//! // Insert vectors
45//! await invoke('plugin:velesdb|upsert', {
46//! request: {
47//! collection: 'documents',
48//! points: [{ id: 1, vector: [...], payload: { title: 'Doc' } }]
49//! }
50//! });
51//!
52//! // Search
53//! const results = await invoke('plugin:velesdb|search', {
54//! request: { collection: 'documents', vector: [...], topK: 10 }
55//! });
56//! ```
57
58#![warn(clippy::all, clippy::pedantic)]
59#![allow(clippy::module_name_repetitions)]
60
61use std::path::Path;
62
63use tauri::{
64 plugin::{Builder, TauriPlugin},
65 Manager, Runtime,
66};
67
68pub mod commands;
69pub mod commands_graph;
70pub mod error;
71pub mod events;
72pub mod helpers;
73pub mod state;
74pub mod types;
75
76pub use error::{CommandError, Error, Result};
77pub use state::VelesDbState;
78
79// ============================================================================
80// Simple In-Memory Index for Demo (VelesDbExt trait)
81// ============================================================================
82
83use std::collections::HashMap;
84use std::sync::Arc;
85
86use parking_lot::RwLock;
87
88// Use DistanceMetric from velesdb_core
89use velesdb_core::DistanceMetric;
90
91/// Simple in-memory vector index for demo purposes.
92/// For production, use the full plugin commands with persistent storage.
93pub struct SimpleVectorIndex {
94 vectors: HashMap<u64, Vec<f32>>,
95 dimension: usize,
96 metric: DistanceMetric,
97}
98
99impl SimpleVectorIndex {
100 /// Creates a new empty index with the given dimension.
101 #[must_use]
102 pub fn new(dimension: usize) -> Self {
103 Self {
104 vectors: HashMap::new(),
105 dimension,
106 metric: DistanceMetric::Cosine, // Default metric
107 }
108 }
109
110 /// Creates a new empty index with the given dimension and metric.
111 #[must_use]
112 pub fn with_metric(dimension: usize, metric: DistanceMetric) -> Self {
113 Self {
114 vectors: HashMap::new(),
115 dimension,
116 metric,
117 }
118 }
119
120 /// Inserts a vector with the given ID.
121 ///
122 /// # Errors
123 ///
124 /// Returns an error if the vector dimension doesn't match the index dimension.
125 pub fn insert(&mut self, id: u64, vector: &[f32]) -> Result<()> {
126 if vector.len() != self.dimension {
127 return Err(Error::InvalidConfig(format!(
128 "Vector dimension mismatch: expected {}, got {}",
129 self.dimension,
130 vector.len()
131 )));
132 }
133 self.vectors.insert(id, vector.to_vec());
134 Ok(())
135 }
136
137 /// Searches for the k most similar vectors.
138 ///
139 /// # Errors
140 ///
141 /// Returns an error if the query dimension doesn't match the index dimension.
142 pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<(u64, f32)>> {
143 if query.len() != self.dimension {
144 return Err(Error::InvalidConfig(format!(
145 "Query dimension mismatch: expected {}, got {}",
146 self.dimension,
147 query.len()
148 )));
149 }
150
151 let mut scores: Vec<(u64, f32)> = self
152 .vectors
153 .iter()
154 .map(|(id, vec)| {
155 let score = self.metric.calculate(query, vec);
156 (*id, score)
157 })
158 .collect();
159
160 // Sort by score according to metric ordering
161 self.metric.sort_results(&mut scores);
162 scores.truncate(k);
163 Ok(scores)
164 }
165
166 /// Returns the number of vectors in the index.
167 #[must_use]
168 pub fn len(&self) -> usize {
169 self.vectors.len()
170 }
171
172 /// Returns true if the index is empty.
173 #[must_use]
174 pub fn is_empty(&self) -> bool {
175 self.vectors.is_empty()
176 }
177
178 /// Returns the dimension of vectors in this index.
179 #[must_use]
180 pub fn dimension(&self) -> usize {
181 self.dimension
182 }
183
184 /// Clears all vectors from the index.
185 pub fn clear(&mut self) {
186 self.vectors.clear();
187 }
188}
189
190/// State for the simple vector index (used by `VelesDbExt`).
191pub struct SimpleIndexState(pub Arc<RwLock<SimpleVectorIndex>>);
192
193/// Extension trait for easy access to `VelesDB` from Tauri `AppHandle`.
194pub trait VelesDbExt<R: Runtime> {
195 /// Returns a handle to the simple vector index, or `None` if not initialized.
196 ///
197 /// Returns `None` when `init()` has not been called before this method.
198 fn velesdb(&self) -> Option<SimpleIndexHandle>;
199}
200
201impl<R: Runtime, T: Manager<R>> VelesDbExt<R> for T {
202 fn velesdb(&self) -> Option<SimpleIndexHandle> {
203 self.try_state::<SimpleIndexState>()
204 .map(|state| SimpleIndexHandle(Arc::clone(&state.0)))
205 }
206}
207
208/// Handle to interact with the simple vector index.
209pub struct SimpleIndexHandle(Arc<RwLock<SimpleVectorIndex>>);
210
211impl SimpleIndexHandle {
212 /// Inserts a vector with the given ID.
213 ///
214 /// # Errors
215 ///
216 /// Returns an error if the vector dimension doesn't match the index dimension.
217 ///
218 pub fn insert(&self, id: u64, vector: &[f32]) -> Result<()> {
219 self.0.write().insert(id, vector)
220 }
221
222 /// Searches for the k most similar vectors.
223 ///
224 /// # Errors
225 ///
226 /// Returns an error if the query dimension doesn't match the index dimension.
227 ///
228 pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<(u64, f32)>> {
229 self.0.read().search(query, k)
230 }
231
232 /// Returns the number of vectors in the index.
233 ///
234 /// Returns `0` and logs an error if the index state is unavailable.
235 #[must_use]
236 pub fn len(&self) -> usize {
237 self.0.read().len()
238 }
239
240 /// Returns true if the index is empty.
241 ///
242 /// Returns `true` and logs an error if the index state is unavailable.
243 #[must_use]
244 pub fn is_empty(&self) -> bool {
245 self.0.read().is_empty()
246 }
247
248 /// Returns the dimension of vectors in this index.
249 ///
250 /// Returns `0` and logs an error if the index state is unavailable.
251 #[must_use]
252 pub fn dimension(&self) -> usize {
253 self.0.read().dimension()
254 }
255
256 /// Clears all vectors from the index.
257 pub fn clear(&self) {
258 self.0.write().clear();
259 }
260}
261
262/// Initializes the `VelesDB` plugin with the default settings.
263///
264/// Uses `./velesdb_data` as the default path for persistence.
265/// This is the simplest way to get started.
266///
267/// # Example
268///
269/// ```rust,ignore
270/// tauri::Builder::default()
271/// .plugin(tauri_plugin_velesdb::init())
272/// .run(tauri::generate_context!())
273/// .expect("error while running tauri application");
274/// ```
275#[must_use]
276pub fn init<R: Runtime>() -> TauriPlugin<R> {
277 init_with_path("./velesdb_data")
278}
279
280/// Initializes the `VelesDB` plugin with a custom data directory.
281///
282/// # Arguments
283///
284/// * `path` - Path to the database directory
285///
286/// # Example
287///
288/// ```rust,ignore
289/// tauri::Builder::default()
290/// .plugin(tauri_plugin_velesdb::init_with_path("./my_data"))
291/// .run(tauri::generate_context!())
292/// .expect("error while running tauri application");
293/// ```
294#[must_use]
295pub fn init_with_path<R: Runtime, P: AsRef<Path>>(path: P) -> TauriPlugin<R> {
296 let db_path = path.as_ref().to_path_buf();
297
298 #[cfg(feature = "persistence")]
299 let builder = Builder::new("velesdb").invoke_handler(tauri::generate_handler![
300 commands::create_collection,
301 commands::create_metadata_collection,
302 commands::delete_collection,
303 commands::list_collections,
304 commands::get_collection,
305 commands::upsert,
306 commands::upsert_metadata,
307 commands::get_points,
308 commands::delete_points,
309 commands::search,
310 commands::batch_search,
311 commands::text_search,
312 commands::hybrid_search,
313 commands::multi_query_search,
314 commands::query,
315 commands::is_empty,
316 commands::flush,
317 // Sparse vector commands
318 commands::sparse_search,
319 commands::hybrid_sparse_search,
320 commands::sparse_upsert,
321 // PQ training command
322 commands::train_pq,
323 // Streaming insert command (requires persistence)
324 commands::stream_insert,
325 // AgentMemory commands (EPIC-016 US-003)
326 commands::semantic_store,
327 commands::semantic_query,
328 // Knowledge Graph commands (EPIC-015 US-001)
329 commands_graph::add_edge,
330 commands_graph::get_edges,
331 commands_graph::traverse_graph,
332 commands_graph::get_node_degree,
333 ]);
334 #[cfg(not(feature = "persistence"))]
335 let builder = Builder::new("velesdb").invoke_handler(tauri::generate_handler![
336 commands::create_collection,
337 commands::create_metadata_collection,
338 commands::delete_collection,
339 commands::list_collections,
340 commands::get_collection,
341 commands::upsert,
342 commands::upsert_metadata,
343 commands::get_points,
344 commands::delete_points,
345 commands::search,
346 commands::batch_search,
347 commands::text_search,
348 commands::hybrid_search,
349 commands::multi_query_search,
350 commands::query,
351 commands::is_empty,
352 commands::flush,
353 // Sparse vector commands
354 commands::sparse_search,
355 commands::hybrid_sparse_search,
356 commands::sparse_upsert,
357 // PQ training command
358 commands::train_pq,
359 // AgentMemory commands (EPIC-016 US-003)
360 commands::semantic_store,
361 commands::semantic_query,
362 // Knowledge Graph commands (EPIC-015 US-001)
363 commands_graph::add_edge,
364 commands_graph::get_edges,
365 commands_graph::traverse_graph,
366 commands_graph::get_node_degree,
367 ]);
368 builder
369 .setup(move |app, _api| {
370 let state = VelesDbState::new(db_path.clone());
371 app.manage(state);
372 // Initialize simple in-memory index for VelesDbExt trait (384 dimensions for AllMiniLML6V2)
373 let simple_index = SimpleIndexState(Arc::new(RwLock::new(SimpleVectorIndex::new(384))));
374 app.manage(simple_index);
375 tracing::info!("VelesDB plugin initialized with path: {:?}", db_path);
376 Ok(())
377 })
378 .build()
379}
380
381/// Alias for `init()` for backward compatibility.
382#[must_use]
383pub fn init_default<R: Runtime>() -> TauriPlugin<R> {
384 init()
385}
386
387/// Initializes the `VelesDB` plugin using the platform's app data directory.
388///
389/// This is the recommended way to initialize the plugin for production apps.
390/// Data is stored in the standard location for each platform:
391/// - **Windows**: `%APPDATA%\<app_name>\velesdb\`
392/// - **macOS**: `~/Library/Application Support/<app_name>/velesdb/`
393/// - **Linux**: `~/.local/share/<app_name>/velesdb/`
394///
395/// # Arguments
396///
397/// * `app_name` - Your application's name (used in the path)
398///
399/// # Example
400///
401/// ```rust,ignore
402/// tauri::Builder::default()
403/// .plugin(tauri_plugin_velesdb::init_with_app_data("my-app"))
404/// .run(tauri::generate_context!())
405/// .expect("error while running tauri application");
406/// ```
407///
408/// # Panics
409///
410/// Panics if the app data directory cannot be determined for the platform.
411#[must_use]
412pub fn init_with_app_data<R: Runtime>(app_name: &str) -> TauriPlugin<R> {
413 let app_data_dir = get_app_data_dir(app_name);
414 init_with_path(app_data_dir)
415}
416
417/// Returns the platform-specific app data directory for `VelesDB`.
418///
419/// # Arguments
420///
421/// * `app_name` - Your application's name
422///
423/// # Returns
424///
425/// Path to `<app_data>/<app_name>/velesdb/`
426///
427/// # Panics
428///
429/// Panics if the app data directory cannot be determined.
430#[must_use]
431pub fn get_app_data_dir(app_name: &str) -> std::path::PathBuf {
432 let Some(base_dir) = dirs::data_dir().or_else(dirs::config_dir) else {
433 panic!("Could not determine app data directory for this platform");
434 };
435
436 base_dir.join(app_name).join("velesdb")
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_velesdb_state_creation() {
445 // Arrange
446 let path = std::path::PathBuf::from("/tmp/test");
447
448 // Act
449 let state = VelesDbState::new(path.clone());
450
451 // Assert
452 assert_eq!(state.path(), &path);
453 }
454
455 #[test]
456 fn test_get_app_data_dir_structure() {
457 // Act
458 let path = get_app_data_dir("test-app");
459
460 // Assert - path should end with test-app/velesdb
461 assert!(path.ends_with("test-app/velesdb") || path.ends_with("test-app\\velesdb"));
462 assert!(path.to_string_lossy().contains("test-app"));
463 }
464
465 #[test]
466 fn test_get_app_data_dir_different_apps() {
467 // Act
468 let path1 = get_app_data_dir("app1");
469 let path2 = get_app_data_dir("app2");
470
471 // Assert - different apps should have different paths
472 assert_ne!(path1, path2);
473 }
474}