Skip to main content

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}