#![allow(clippy::pedantic)]
#![allow(clippy::nursery)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::similar_names)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::wildcard_imports)]
#![allow(clippy::redundant_closure_for_method_calls)]
uniffi::setup_scaffolding!();
mod agent;
mod graph;
mod types;
pub use agent::{SemanticResult, VelesSemanticMemory};
pub use graph::{MobileGraphEdge, MobileGraphNode, MobileGraphStore, TraversalResult};
pub use types::{
DistanceMetric, FusionStrategy, IndividualSearchRequest, MobileCollectionStats,
MobileIndexInfo, PqTrainConfig, SearchResult, StorageMode, VelesError, VelesPoint,
VelesSparseVector,
};
use std::sync::Arc;
use velesdb_core::Database as CoreDatabase;
use velesdb_core::FusionStrategy as CoreFusionStrategy;
use velesdb_core::VectorCollection as CoreCollection;
#[cfg(test)]
use velesdb_core::DistanceMetric as CoreDistanceMetric;
#[derive(uniffi::Object)]
pub struct VelesDatabase {
inner: CoreDatabase,
}
#[uniffi::export]
impl VelesDatabase {
#[uniffi::constructor]
pub fn open(path: String) -> Result<Arc<Self>, VelesError> {
let db = CoreDatabase::open(&path)?;
Ok(Arc::new(Self { inner: db }))
}
pub fn create_collection(
&self,
name: String,
dimension: u32,
metric: DistanceMetric,
) -> Result<(), VelesError> {
self.inner.create_collection(
&name,
usize::try_from(dimension).unwrap_or(usize::MAX),
metric.into(),
)?;
Ok(())
}
pub fn create_collection_with_storage(
&self,
name: String,
dimension: u32,
metric: DistanceMetric,
storage_mode: StorageMode,
) -> Result<(), VelesError> {
self.inner.create_vector_collection_with_options(
&name,
usize::try_from(dimension).unwrap_or(usize::MAX),
metric.into(),
storage_mode.into(),
)?;
Ok(())
}
pub fn create_metadata_collection(&self, name: String) -> Result<(), VelesError> {
self.inner.create_metadata_collection(&name)?;
Ok(())
}
pub fn get_collection(&self, name: String) -> Result<Option<Arc<VelesCollection>>, VelesError> {
if let Some(coll) = self.inner.get_vector_collection(&name) {
return Ok(Some(Arc::new(VelesCollection { inner: coll })));
}
let path = self.inner.data_dir().join(&name);
if path.join("config.json").exists() {
match velesdb_core::VectorCollection::open(path) {
Ok(coll) => return Ok(Some(Arc::new(VelesCollection { inner: coll }))),
Err(e) => {
tracing::warn!(
collection = %name,
error = %e,
"VectorCollection::open failed for existing config; collection skipped"
);
}
}
}
Ok(None)
}
pub fn list_collections(&self) -> Vec<String> {
self.inner.list_collections()
}
pub fn delete_collection(&self, name: String) -> Result<(), VelesError> {
self.inner.delete_collection(&name)?;
Ok(())
}
pub fn train_pq(
&self,
collection_name: String,
config: PqTrainConfig,
) -> Result<String, VelesError> {
use std::collections::HashMap;
use velesdb_core::velesql::{Query, TrainStatement, WithValue};
let mut params = HashMap::new();
params.insert("m".to_string(), WithValue::Integer(i64::from(config.m)));
params.insert("k".to_string(), WithValue::Integer(i64::from(config.k)));
if config.opq {
params.insert("type".to_string(), WithValue::Identifier("opq".to_string()));
}
let query = Query::new_train(TrainStatement {
collection: collection_name,
params,
});
let empty_params = HashMap::new();
self.inner
.execute_query(&query, &empty_params)
.map_err(|e| VelesError::Database {
message: format!("PQ training failed: {e}"),
})?;
Ok("PQ training complete".to_string())
}
}
#[derive(uniffi::Object)]
pub struct VelesCollection {
inner: CoreCollection,
}
#[uniffi::export]
impl VelesCollection {
pub fn search(&self, vector: Vec<f32>, limit: u32) -> Result<Vec<SearchResult>, VelesError> {
let results = self
.inner
.search_ids(&vector, usize::try_from(limit).unwrap_or(usize::MAX))?;
Ok(results
.into_iter()
.map(|sr| SearchResult {
id: sr.id,
score: sr.score,
payload: None,
})
.collect())
}
pub fn upsert(&self, point: VelesPoint) -> Result<(), VelesError> {
let payload = point
.payload
.map(|s| serde_json::from_str(&s))
.transpose()
.map_err(|e| VelesError::Database {
message: format!("Invalid JSON payload: {e}"),
})?;
let core_point = velesdb_core::Point::new(point.id, point.vector, payload);
self.inner.upsert(vec![core_point])?;
Ok(())
}
pub fn upsert_batch(&self, points: Vec<VelesPoint>) -> Result<(), VelesError> {
let core_points: Result<Vec<velesdb_core::Point>, VelesError> = points
.into_iter()
.map(|p| {
let payload = p
.payload
.map(|s| serde_json::from_str(&s))
.transpose()
.map_err(|e| VelesError::Database {
message: format!("Invalid JSON payload: {e}"),
})?;
Ok(velesdb_core::Point::new(p.id, p.vector, payload))
})
.collect();
self.inner.upsert(core_points?)?;
Ok(())
}
pub fn delete(&self, id: u64) -> Result<(), VelesError> {
self.inner.delete(&[id])?;
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
pub fn count(&self) -> u64 {
self.inner.config().point_count as u64
}
#[allow(clippy::cast_possible_truncation)]
pub fn dimension(&self) -> u32 {
self.inner.config().dimension as u32
}
pub fn get(&self, ids: Vec<u64>) -> Vec<VelesPoint> {
self.inner
.get(&ids)
.into_iter()
.flatten()
.map(|p| VelesPoint {
id: p.id,
vector: p.vector,
payload: p.payload.map(|v| v.to_string()),
})
.collect()
}
pub fn get_by_id(&self, id: u64) -> Option<VelesPoint> {
self.inner
.get(&[id])
.into_iter()
.flatten()
.next()
.map(|p| VelesPoint {
id: p.id,
vector: p.vector,
payload: p.payload.map(|v| v.to_string()),
})
}
pub fn is_metadata_only(&self) -> bool {
self.inner.config().metadata_only
}
pub fn text_search(&self, query: String, limit: u32) -> Result<Vec<SearchResult>, VelesError> {
let results = self
.inner
.text_search(&query, usize::try_from(limit).unwrap_or(usize::MAX))
.map_err(|e| VelesError::Database {
message: format!("Text search failed: {e}"),
})?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn hybrid_search(
&self,
vector: Vec<f32>,
text_query: String,
limit: u32,
vector_weight: f32,
) -> Result<Vec<SearchResult>, VelesError> {
let results = self.inner.hybrid_search(
&vector,
&text_query,
usize::try_from(limit).unwrap_or(usize::MAX),
Some(vector_weight),
)?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn search_with_filter(
&self,
vector: Vec<f32>,
limit: u32,
filter_json: String,
) -> Result<Vec<SearchResult>, VelesError> {
let filter: velesdb_core::Filter =
serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
message: format!("Invalid filter JSON: {e}"),
})?;
let results = self.inner.search_with_filter(
&vector,
usize::try_from(limit).unwrap_or(usize::MAX),
&filter,
)?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn batch_search(
&self,
searches: Vec<IndividualSearchRequest>,
) -> Result<Vec<Vec<SearchResult>>, VelesError> {
let query_refs: Vec<&[f32]> = searches.iter().map(|s| s.vector.as_slice()).collect();
let filters: Result<Vec<Option<velesdb_core::Filter>>, VelesError> = searches
.iter()
.map(|s| {
s.filter
.as_ref()
.map(|f_json| {
serde_json::from_str(f_json).map_err(|e| VelesError::Database {
message: format!("Invalid filter JSON in batch: {e}"),
})
})
.transpose()
})
.collect();
let filters = filters?;
let max_top_k = searches.iter().map(|s| s.top_k).max().unwrap_or(10);
let all_results = self.inner.search_batch_with_filters(
&query_refs,
usize::try_from(max_top_k).unwrap_or(usize::MAX),
&filters,
)?;
Ok(all_results
.into_iter()
.zip(searches)
.map(
|(results, s): (Vec<velesdb_core::SearchResult>, IndividualSearchRequest)| {
results
.into_iter()
.take(usize::try_from(s.top_k).unwrap_or(usize::MAX))
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect()
},
)
.collect())
}
pub fn text_search_with_filter(
&self,
query: String,
limit: u32,
filter_json: String,
) -> Result<Vec<SearchResult>, VelesError> {
let filter: velesdb_core::Filter =
serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
message: format!("Invalid filter JSON: {e}"),
})?;
let results = self
.inner
.text_search_with_filter(
&query,
usize::try_from(limit).unwrap_or(usize::MAX),
&filter,
)
.map_err(|e| VelesError::Database {
message: format!("Text search with filter failed: {e}"),
})?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn hybrid_search_with_filter(
&self,
vector: Vec<f32>,
text_query: String,
limit: u32,
vector_weight: f32,
filter_json: String,
) -> Result<Vec<SearchResult>, VelesError> {
let filter: velesdb_core::Filter =
serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
message: format!("Invalid filter JSON: {e}"),
})?;
let results = self.inner.hybrid_search_with_filter(
&vector,
&text_query,
usize::try_from(limit).unwrap_or(usize::MAX),
Some(vector_weight),
&filter,
)?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn query(
&self,
query_str: String,
params_json: Option<String>,
) -> Result<Vec<SearchResult>, VelesError> {
let parsed =
velesdb_core::velesql::Parser::parse(&query_str).map_err(|e| VelesError::Database {
message: format!("VelesQL parse error: {}", e.message),
})?;
let params: std::collections::HashMap<String, serde_json::Value> = params_json
.map(|json| serde_json::from_str(&json))
.transpose()
.map_err(|e| VelesError::Database {
message: format!("Invalid params JSON: {e}"),
})?
.unwrap_or_default();
let results =
self.inner
.execute_query(&parsed, ¶ms)
.map_err(|e| VelesError::Database {
message: format!("Query execution failed: {e}"),
})?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: r.point.payload.as_ref().map(|p| p.to_string()),
})
.collect())
}
pub fn multi_query_search(
&self,
vectors: Vec<Vec<f32>>,
limit: u32,
strategy: FusionStrategy,
) -> Result<Vec<SearchResult>, VelesError> {
if vectors.is_empty() {
return Err(VelesError::Database {
message: "multi_query_search requires at least one vector".to_string(),
});
}
let query_refs: Vec<&[f32]> = vectors.iter().map(|v| v.as_slice()).collect();
let core_strategy: CoreFusionStrategy = strategy.into();
let results = self
.inner
.multi_query_search(
&query_refs,
usize::try_from(limit).unwrap_or(usize::MAX),
core_strategy,
None,
)
.map_err(|e| VelesError::Database {
message: format!("Multi-query search failed: {e}"),
})?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn multi_query_search_with_filter(
&self,
vectors: Vec<Vec<f32>>,
limit: u32,
strategy: FusionStrategy,
filter_json: String,
) -> Result<Vec<SearchResult>, VelesError> {
if vectors.is_empty() {
return Err(VelesError::Database {
message: "multi_query_search requires at least one vector".to_string(),
});
}
let filter: velesdb_core::Filter =
serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
message: format!("Invalid filter JSON: {e}"),
})?;
let query_refs: Vec<&[f32]> = vectors.iter().map(|v| v.as_slice()).collect();
let core_strategy: CoreFusionStrategy = strategy.into();
let results = self
.inner
.multi_query_search(
&query_refs,
usize::try_from(limit).unwrap_or(usize::MAX),
core_strategy,
Some(&filter),
)
.map_err(|e| VelesError::Database {
message: format!("Multi-query search failed: {e}"),
})?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn flush(&self) -> Result<(), VelesError> {
self.inner.flush()?;
Ok(())
}
pub fn all_ids(&self) -> Vec<u64> {
self.inner.all_ids()
}
pub fn create_index(&self, field_name: String) -> Result<(), VelesError> {
self.inner.create_index(&field_name)?;
Ok(())
}
pub fn has_secondary_index(&self, field_name: String) -> bool {
self.inner.has_secondary_index(&field_name)
}
pub fn create_property_index(&self, label: String, property: String) -> Result<(), VelesError> {
self.inner.create_property_index(&label, &property)?;
Ok(())
}
pub fn create_range_index(&self, label: String, property: String) -> Result<(), VelesError> {
self.inner.create_range_index(&label, &property)?;
Ok(())
}
pub fn has_property_index(&self, label: String, property: String) -> bool {
self.inner.has_property_index(&label, &property)
}
pub fn has_range_index(&self, label: String, property: String) -> bool {
self.inner.has_range_index(&label, &property)
}
pub fn list_indexes(&self) -> Vec<MobileIndexInfo> {
self.inner
.list_indexes()
.into_iter()
.map(MobileIndexInfo::from)
.collect()
}
pub fn drop_index(&self, label: String, property: String) -> Result<bool, VelesError> {
Ok(self.inner.drop_index(&label, &property)?)
}
pub fn indexes_memory_usage(&self) -> u64 {
u64::try_from(self.inner.indexes_memory_usage()).unwrap_or(u64::MAX)
}
pub fn analyze(&self) -> Result<MobileCollectionStats, VelesError> {
Ok(self.inner.analyze()?.into())
}
pub fn get_stats(&self) -> MobileCollectionStats {
self.inner.get_stats().into()
}
pub fn sparse_search(
&self,
sparse_vector: VelesSparseVector,
limit: u32,
index_name: Option<String>,
) -> Result<Vec<SearchResult>, VelesError> {
let core_sv = Self::to_core_sparse_vector(&sparse_vector);
let idx_name = index_name.unwrap_or_default();
let results = self
.inner
.sparse_search(
&core_sv,
usize::try_from(limit).unwrap_or(usize::MAX),
&idx_name,
)
.map_err(|e| VelesError::Database {
message: format!("Sparse search failed: {e}"),
})?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn hybrid_sparse_search(
&self,
vector: Vec<f32>,
sparse_vector: VelesSparseVector,
limit: u32,
index_name: Option<String>,
) -> Result<Vec<SearchResult>, VelesError> {
let core_sv = Self::to_core_sparse_vector(&sparse_vector);
let strategy = velesdb_core::fusion::FusionStrategy::RRF { k: 60 };
let idx_name = index_name.unwrap_or_default();
let results = self
.inner
.hybrid_sparse_search(
&vector,
&core_sv,
usize::try_from(limit).unwrap_or(usize::MAX),
&idx_name,
&strategy,
)
.map_err(|e| VelesError::Database {
message: format!("Hybrid sparse search failed: {e}"),
})?;
Ok(results
.into_iter()
.map(|r| SearchResult {
id: r.point.id,
score: r.score,
payload: None,
})
.collect())
}
pub fn upsert_with_sparse(
&self,
point: VelesPoint,
sparse_vector: VelesSparseVector,
) -> Result<(), VelesError> {
let payload = point
.payload
.map(|s| serde_json::from_str(&s))
.transpose()
.map_err(|e| VelesError::Database {
message: format!("Invalid JSON payload: {e}"),
})?;
let core_sv = Self::to_core_sparse_vector(&sparse_vector);
let mut sparse_map = std::collections::BTreeMap::new();
sparse_map.insert(String::new(), core_sv);
let core_point =
velesdb_core::Point::with_sparse(point.id, point.vector, payload, Some(sparse_map));
self.inner.upsert(vec![core_point])?;
Ok(())
}
}
impl VelesCollection {
fn to_core_sparse_vector(sv: &VelesSparseVector) -> velesdb_core::sparse_index::SparseVector {
let pairs: Vec<(u32, f32)> = sv
.indices
.iter()
.copied()
.zip(sv.values.iter().copied())
.collect();
velesdb_core::sparse_index::SparseVector::new(pairs)
}
}
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;