#![allow(clippy::pedantic)]
#![allow(clippy::nursery)]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
use std::path::Path;
use tauri::{
plugin::{Builder, TauriPlugin},
Manager, Runtime,
};
pub mod commands;
pub mod commands_graph;
pub mod commands_index;
pub mod commands_memory;
pub mod commands_query;
pub mod commands_sparse;
pub mod error;
pub mod events;
pub mod helpers;
pub mod state;
pub mod types;
pub mod types_graph;
pub use error::{CommandError, Error, Result};
pub use state::VelesDbState;
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use velesdb_core::DistanceMetric;
pub(crate) struct SimpleVectorIndex {
vectors: HashMap<u64, Vec<f32>>,
dimension: usize,
metric: DistanceMetric,
}
impl SimpleVectorIndex {
#[must_use]
pub fn new(dimension: usize) -> Self {
Self {
vectors: HashMap::new(),
dimension,
metric: DistanceMetric::Cosine, }
}
pub fn insert(&mut self, id: u64, vector: &[f32]) -> Result<()> {
if vector.len() != self.dimension {
return Err(Error::InvalidConfig(format!(
"Vector dimension mismatch: expected {}, got {}",
self.dimension,
vector.len()
)));
}
self.vectors.insert(id, vector.to_vec());
Ok(())
}
pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<(u64, f32)>> {
if query.len() != self.dimension {
return Err(Error::InvalidConfig(format!(
"Query dimension mismatch: expected {}, got {}",
self.dimension,
query.len()
)));
}
let mut scores: Vec<(u64, f32)> = self
.vectors
.iter()
.map(|(id, vec)| {
let score = self.metric.calculate(query, vec);
(*id, score)
})
.collect();
self.metric.sort_results(&mut scores);
scores.truncate(k);
Ok(scores)
}
#[must_use]
pub fn len(&self) -> usize {
self.vectors.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.vectors.is_empty()
}
#[must_use]
pub fn dimension(&self) -> usize {
self.dimension
}
pub fn clear(&mut self) {
self.vectors.clear();
}
}
pub(crate) struct SimpleIndexState(pub(crate) Arc<RwLock<SimpleVectorIndex>>);
pub trait VelesDbExt<R: Runtime> {
fn velesdb(&self) -> Option<SimpleIndexHandle>;
}
impl<R: Runtime, T: Manager<R>> VelesDbExt<R> for T {
fn velesdb(&self) -> Option<SimpleIndexHandle> {
self.try_state::<SimpleIndexState>()
.map(|state| SimpleIndexHandle(Arc::clone(&state.0)))
}
}
pub struct SimpleIndexHandle(Arc<RwLock<SimpleVectorIndex>>);
impl SimpleIndexHandle {
pub fn insert(&self, id: u64, vector: &[f32]) -> Result<()> {
self.0.write().insert(id, vector)
}
pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<(u64, f32)>> {
self.0.read().search(query, k)
}
#[must_use]
pub fn len(&self) -> usize {
self.0.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.read().is_empty()
}
#[must_use]
pub fn dimension(&self) -> usize {
self.0.read().dimension()
}
pub fn clear(&self) {
self.0.write().clear();
}
}
#[must_use]
pub fn init<R: Runtime>() -> TauriPlugin<R> {
init_with_path("./velesdb_data")
}
macro_rules! velesdb_invoke_handler {
(
common: [$($common:path),* $(,)?],
persistence_only: [$($persist:path),* $(,)?] $(,)?
) => {{
#[cfg(feature = "persistence")]
{
tauri::generate_handler![$($common,)* $($persist,)*]
}
#[cfg(not(feature = "persistence"))]
{
tauri::generate_handler![$($common,)*]
}
}};
}
#[must_use]
pub fn init_with_path<R: Runtime, P: AsRef<Path>>(path: P) -> TauriPlugin<R> {
let db_path = path.as_ref().to_path_buf();
let builder = Builder::new("velesdb").invoke_handler(velesdb_invoke_handler!(
common: [
commands::create_collection,
commands::create_metadata_collection,
commands::delete_collection,
commands::list_collections,
commands::get_collection,
commands::upsert,
commands::upsert_metadata,
commands::get_points,
commands::delete_points,
commands::search,
commands::batch_search,
commands::text_search,
commands::hybrid_search,
commands::multi_query_search,
commands_query::query,
commands::is_empty,
commands::flush,
commands_sparse::sparse_search,
commands_sparse::hybrid_sparse_search,
commands_sparse::sparse_upsert,
commands::train_pq,
commands_memory::semantic_store,
commands_memory::semantic_query,
commands_graph::add_edge,
commands_graph::get_edges,
commands_graph::traverse_graph,
commands_graph::get_node_degree,
commands_graph::traverse_graph_parallel,
commands_index::create_index,
commands_index::drop_index,
commands_index::list_indexes,
],
persistence_only: [
commands::stream_insert,
],
));
builder
.setup(move |app, _api| {
let state = VelesDbState::new(db_path.clone());
app.manage(state);
let simple_index = SimpleIndexState(Arc::new(RwLock::new(SimpleVectorIndex::new(384))));
app.manage(simple_index);
tracing::info!("VelesDB plugin initialized with path: {:?}", db_path);
Ok(())
})
.build()
}
#[must_use]
pub fn init_default<R: Runtime>() -> TauriPlugin<R> {
init()
}
pub fn init_with_app_data<R: Runtime>(app_name: &str) -> Result<TauriPlugin<R>> {
let app_data_dir = get_app_data_dir(app_name)?;
Ok(init_with_path(app_data_dir))
}
pub fn get_app_data_dir(app_name: &str) -> Result<std::path::PathBuf> {
let base_dir = dirs::data_dir().or_else(dirs::config_dir).ok_or_else(|| {
Error::InvalidConfig("Could not determine app data directory for this platform".to_string())
})?;
Ok(base_dir.join(app_name).join("velesdb"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_velesdb_state_creation() {
let path = std::path::PathBuf::from("/tmp/test");
let state = VelesDbState::new(path.clone());
assert_eq!(state.path(), &path);
}
#[test]
fn test_get_app_data_dir_structure() {
let path = get_app_data_dir("test-app").unwrap();
assert!(path.ends_with("test-app/velesdb") || path.ends_with("test-app\\velesdb"));
assert!(path.to_string_lossy().contains("test-app"));
}
#[test]
fn test_get_app_data_dir_different_apps() {
let path1 = get_app_data_dir("app1").unwrap();
let path2 = get_app_data_dir("app2").unwrap();
assert_ne!(path1, path2);
}
}