1#![allow(clippy::pedantic)]
3#![allow(clippy::nursery)]
4#![allow(clippy::needless_pass_by_value)]
5#![allow(clippy::missing_errors_doc)]
7#![allow(clippy::missing_panics_doc)]
8#![allow(clippy::must_use_candidate)]
9#![allow(clippy::uninlined_format_args)]
10#![allow(clippy::similar_names)]
11#![allow(clippy::module_name_repetitions)]
12#![allow(clippy::doc_markdown)]
13#![allow(clippy::wildcard_imports)]
14#![allow(clippy::redundant_closure_for_method_calls)]
15
16uniffi::setup_scaffolding!();
38
39mod graph;
40mod types;
41
42pub use graph::{MobileGraphEdge, MobileGraphNode, MobileGraphStore, TraversalResult};
43pub use types::{
44 DistanceMetric, FusionStrategy, IndividualSearchRequest, MobileCollectionStats,
45 MobileIndexInfo, PqTrainConfig, SearchResult, StorageMode, VelesError, VelesPoint,
46 VelesSparseVector,
47};
48
49use std::sync::Arc;
50use velesdb_core::Database as CoreDatabase;
51use velesdb_core::FusionStrategy as CoreFusionStrategy;
52use velesdb_core::VectorCollection as CoreCollection;
53
54#[cfg(test)]
55use velesdb_core::DistanceMetric as CoreDistanceMetric;
56
57#[derive(uniffi::Object)]
68pub struct VelesDatabase {
69 inner: CoreDatabase,
70}
71
72#[uniffi::export]
73impl VelesDatabase {
74 #[uniffi::constructor]
84 pub fn open(path: String) -> Result<Arc<Self>, VelesError> {
85 let db = CoreDatabase::open(&path)?;
86 Ok(Arc::new(Self { inner: db }))
87 }
88
89 pub fn create_collection(
97 &self,
98 name: String,
99 dimension: u32,
100 metric: DistanceMetric,
101 ) -> Result<(), VelesError> {
102 self.inner.create_collection(
103 &name,
104 usize::try_from(dimension).unwrap_or(usize::MAX),
105 metric.into(),
106 )?;
107 Ok(())
108 }
109
110 pub fn create_collection_with_storage(
125 &self,
126 name: String,
127 dimension: u32,
128 metric: DistanceMetric,
129 storage_mode: StorageMode,
130 ) -> Result<(), VelesError> {
131 self.inner.create_vector_collection_with_options(
132 &name,
133 usize::try_from(dimension).unwrap_or(usize::MAX),
134 metric.into(),
135 storage_mode.into(),
136 )?;
137 Ok(())
138 }
139
140 pub fn create_metadata_collection(&self, name: String) -> Result<(), VelesError> {
149 self.inner.create_metadata_collection(&name)?;
150 Ok(())
151 }
152
153 pub fn get_collection(&self, name: String) -> Result<Option<Arc<VelesCollection>>, VelesError> {
158 if let Some(coll) = self.inner.get_vector_collection(&name) {
160 return Ok(Some(Arc::new(VelesCollection { inner: coll })));
161 }
162 let path = self.inner.data_dir().join(&name);
172 if path.join("config.json").exists() {
173 match velesdb_core::VectorCollection::open(path) {
174 Ok(coll) => return Ok(Some(Arc::new(VelesCollection { inner: coll }))),
175 Err(e) => {
176 tracing::warn!(
177 collection = %name,
178 error = %e,
179 "VectorCollection::open failed for existing config; collection skipped"
180 );
181 }
182 }
183 }
184 Ok(None)
185 }
186
187 pub fn list_collections(&self) -> Vec<String> {
189 self.inner.list_collections()
190 }
191
192 pub fn delete_collection(&self, name: String) -> Result<(), VelesError> {
194 self.inner.delete_collection(&name)?;
195 Ok(())
196 }
197
198 pub fn train_pq(
212 &self,
213 collection_name: String,
214 config: PqTrainConfig,
215 ) -> Result<String, VelesError> {
216 use std::collections::HashMap;
217 use velesdb_core::velesql::{Query, TrainStatement, WithValue};
218
219 let mut params = HashMap::new();
220 params.insert("m".to_string(), WithValue::Integer(i64::from(config.m)));
221 params.insert("k".to_string(), WithValue::Integer(i64::from(config.k)));
222 if config.opq {
223 params.insert("type".to_string(), WithValue::Identifier("opq".to_string()));
224 }
225
226 let query = Query::new_train(TrainStatement {
227 collection: collection_name,
228 params,
229 });
230
231 let empty_params = HashMap::new();
232 self.inner
233 .execute_query(&query, &empty_params)
234 .map_err(|e| VelesError::Database {
235 message: format!("PQ training failed: {e}"),
236 })?;
237
238 Ok("PQ training complete".to_string())
239 }
240}
241
242#[derive(uniffi::Object)]
248pub struct VelesCollection {
249 inner: CoreCollection,
250}
251
252#[uniffi::export]
253impl VelesCollection {
254 pub fn search(&self, vector: Vec<f32>, limit: u32) -> Result<Vec<SearchResult>, VelesError> {
265 let results = self
266 .inner
267 .search_ids(&vector, usize::try_from(limit).unwrap_or(usize::MAX))?;
268
269 Ok(results
270 .into_iter()
271 .map(|(id, score)| SearchResult { id, score })
272 .collect())
273 }
274
275 pub fn upsert(&self, point: VelesPoint) -> Result<(), VelesError> {
281 let payload = point
282 .payload
283 .map(|s| serde_json::from_str(&s))
284 .transpose()
285 .map_err(|e| VelesError::Database {
286 message: format!("Invalid JSON payload: {e}"),
287 })?;
288
289 let core_point = velesdb_core::Point::new(point.id, point.vector, payload);
290 self.inner.upsert(vec![core_point])?;
291 Ok(())
292 }
293
294 pub fn upsert_batch(&self, points: Vec<VelesPoint>) -> Result<(), VelesError> {
300 let core_points: Result<Vec<velesdb_core::Point>, VelesError> = points
301 .into_iter()
302 .map(|p| {
303 let payload = p
304 .payload
305 .map(|s| serde_json::from_str(&s))
306 .transpose()
307 .map_err(|e| VelesError::Database {
308 message: format!("Invalid JSON payload: {e}"),
309 })?;
310 Ok(velesdb_core::Point::new(p.id, p.vector, payload))
311 })
312 .collect();
313
314 self.inner.upsert(core_points?)?;
315 Ok(())
316 }
317
318 pub fn delete(&self, id: u64) -> Result<(), VelesError> {
320 self.inner.delete(&[id])?;
321 Ok(())
322 }
323
324 #[allow(clippy::cast_possible_truncation)]
326 pub fn count(&self) -> u64 {
327 self.inner.config().point_count as u64
328 }
329
330 #[allow(clippy::cast_possible_truncation)]
332 pub fn dimension(&self) -> u32 {
333 self.inner.config().dimension as u32
334 }
335
336 pub fn get(&self, ids: Vec<u64>) -> Vec<VelesPoint> {
346 self.inner
347 .get(&ids)
348 .into_iter()
349 .flatten()
350 .map(|p| VelesPoint {
351 id: p.id,
352 vector: p.vector,
353 payload: p.payload.map(|v| v.to_string()),
354 })
355 .collect()
356 }
357
358 pub fn get_by_id(&self, id: u64) -> Option<VelesPoint> {
368 self.inner
369 .get(&[id])
370 .into_iter()
371 .flatten()
372 .next()
373 .map(|p| VelesPoint {
374 id: p.id,
375 vector: p.vector,
376 payload: p.payload.map(|v| v.to_string()),
377 })
378 }
379
380 pub fn is_metadata_only(&self) -> bool {
382 self.inner.config().metadata_only
383 }
384
385 pub fn text_search(&self, query: String, limit: u32) -> Vec<SearchResult> {
396 let results = self
397 .inner
398 .text_search(&query, usize::try_from(limit).unwrap_or(usize::MAX));
399
400 results
401 .into_iter()
402 .map(|r| SearchResult {
403 id: r.point.id,
404 score: r.score,
405 })
406 .collect()
407 }
408
409 pub fn hybrid_search(
422 &self,
423 vector: Vec<f32>,
424 text_query: String,
425 limit: u32,
426 vector_weight: f32,
427 ) -> Result<Vec<SearchResult>, VelesError> {
428 let results = self.inner.hybrid_search(
429 &vector,
430 &text_query,
431 usize::try_from(limit).unwrap_or(usize::MAX),
432 Some(vector_weight),
433 )?;
434
435 Ok(results
436 .into_iter()
437 .map(|r| SearchResult {
438 id: r.point.id,
439 score: r.score,
440 })
441 .collect())
442 }
443
444 pub fn search_with_filter(
456 &self,
457 vector: Vec<f32>,
458 limit: u32,
459 filter_json: String,
460 ) -> Result<Vec<SearchResult>, VelesError> {
461 let filter: velesdb_core::Filter =
463 serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
464 message: format!("Invalid filter JSON: {e}"),
465 })?;
466
467 let results = self.inner.search_with_filter(
468 &vector,
469 usize::try_from(limit).unwrap_or(usize::MAX),
470 &filter,
471 )?;
472
473 Ok(results
474 .into_iter()
475 .map(|r| SearchResult {
476 id: r.point.id,
477 score: r.score,
478 })
479 .collect())
480 }
481
482 pub fn batch_search(
492 &self,
493 searches: Vec<IndividualSearchRequest>,
494 ) -> Result<Vec<Vec<SearchResult>>, VelesError> {
495 let query_refs: Vec<&[f32]> = searches.iter().map(|s| s.vector.as_slice()).collect();
496
497 let filters: Result<Vec<Option<velesdb_core::Filter>>, VelesError> = searches
498 .iter()
499 .map(|s| {
500 s.filter
501 .as_ref()
502 .map(|f_json| {
503 serde_json::from_str(f_json).map_err(|e| VelesError::Database {
504 message: format!("Invalid filter JSON in batch: {e}"),
505 })
506 })
507 .transpose()
508 })
509 .collect();
510
511 let filters = filters?;
512 let max_top_k = searches.iter().map(|s| s.top_k).max().unwrap_or(10);
513
514 let all_results = self.inner.search_batch_with_filters(
515 &query_refs,
516 usize::try_from(max_top_k).unwrap_or(usize::MAX),
517 &filters,
518 )?;
519
520 Ok(all_results
521 .into_iter()
522 .zip(searches)
523 .map(
524 |(results, s): (Vec<velesdb_core::SearchResult>, IndividualSearchRequest)| {
525 results
526 .into_iter()
527 .take(usize::try_from(s.top_k).unwrap_or(usize::MAX))
528 .map(|r| SearchResult {
529 id: r.point.id,
530 score: r.score,
531 })
532 .collect()
533 },
534 )
535 .collect())
536 }
537
538 pub fn text_search_with_filter(
546 &self,
547 query: String,
548 limit: u32,
549 filter_json: String,
550 ) -> Result<Vec<SearchResult>, VelesError> {
551 let filter: velesdb_core::Filter =
552 serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
553 message: format!("Invalid filter JSON: {e}"),
554 })?;
555
556 let results = self.inner.text_search_with_filter(
557 &query,
558 usize::try_from(limit).unwrap_or(usize::MAX),
559 &filter,
560 );
561
562 Ok(results
563 .into_iter()
564 .map(|r| SearchResult {
565 id: r.point.id,
566 score: r.score,
567 })
568 .collect())
569 }
570
571 pub fn hybrid_search_with_filter(
581 &self,
582 vector: Vec<f32>,
583 text_query: String,
584 limit: u32,
585 vector_weight: f32,
586 filter_json: String,
587 ) -> Result<Vec<SearchResult>, VelesError> {
588 let filter: velesdb_core::Filter =
589 serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
590 message: format!("Invalid filter JSON: {e}"),
591 })?;
592
593 let results = self.inner.hybrid_search_with_filter(
594 &vector,
595 &text_query,
596 usize::try_from(limit).unwrap_or(usize::MAX),
597 Some(vector_weight),
598 &filter,
599 )?;
600
601 Ok(results
602 .into_iter()
603 .map(|r| SearchResult {
604 id: r.point.id,
605 score: r.score,
606 })
607 .collect())
608 }
609
610 pub fn query(
630 &self,
631 query_str: String,
632 params_json: Option<String>,
633 ) -> Result<Vec<SearchResult>, VelesError> {
634 let parsed =
636 velesdb_core::velesql::Parser::parse(&query_str).map_err(|e| VelesError::Database {
637 message: format!("VelesQL parse error: {}", e.message),
638 })?;
639
640 let params: std::collections::HashMap<String, serde_json::Value> = params_json
642 .map(|json| serde_json::from_str(&json))
643 .transpose()
644 .map_err(|e| VelesError::Database {
645 message: format!("Invalid params JSON: {e}"),
646 })?
647 .unwrap_or_default();
648
649 let results =
651 self.inner
652 .execute_query(&parsed, ¶ms)
653 .map_err(|e| VelesError::Database {
654 message: format!("Query execution failed: {e}"),
655 })?;
656
657 Ok(results
658 .into_iter()
659 .map(|r| SearchResult {
660 id: r.point.id,
661 score: r.score,
662 })
663 .collect())
664 }
665
666 pub fn multi_query_search(
692 &self,
693 vectors: Vec<Vec<f32>>,
694 limit: u32,
695 strategy: FusionStrategy,
696 ) -> Result<Vec<SearchResult>, VelesError> {
697 if vectors.is_empty() {
698 return Err(VelesError::Database {
699 message: "multi_query_search requires at least one vector".to_string(),
700 });
701 }
702
703 let query_refs: Vec<&[f32]> = vectors.iter().map(|v| v.as_slice()).collect();
704 let core_strategy: CoreFusionStrategy = strategy.into();
705
706 let results = self
707 .inner
708 .multi_query_search(
709 &query_refs,
710 usize::try_from(limit).unwrap_or(usize::MAX),
711 core_strategy,
712 None,
713 )
714 .map_err(|e| VelesError::Database {
715 message: format!("Multi-query search failed: {e}"),
716 })?;
717
718 Ok(results
719 .into_iter()
720 .map(|r| SearchResult {
721 id: r.point.id,
722 score: r.score,
723 })
724 .collect())
725 }
726
727 pub fn multi_query_search_with_filter(
736 &self,
737 vectors: Vec<Vec<f32>>,
738 limit: u32,
739 strategy: FusionStrategy,
740 filter_json: String,
741 ) -> Result<Vec<SearchResult>, VelesError> {
742 if vectors.is_empty() {
743 return Err(VelesError::Database {
744 message: "multi_query_search requires at least one vector".to_string(),
745 });
746 }
747
748 let filter: velesdb_core::Filter =
749 serde_json::from_str(&filter_json).map_err(|e| VelesError::Database {
750 message: format!("Invalid filter JSON: {e}"),
751 })?;
752
753 let query_refs: Vec<&[f32]> = vectors.iter().map(|v| v.as_slice()).collect();
754 let core_strategy: CoreFusionStrategy = strategy.into();
755
756 let results = self
757 .inner
758 .multi_query_search(
759 &query_refs,
760 usize::try_from(limit).unwrap_or(usize::MAX),
761 core_strategy,
762 Some(&filter),
763 )
764 .map_err(|e| VelesError::Database {
765 message: format!("Multi-query search failed: {e}"),
766 })?;
767
768 Ok(results
769 .into_iter()
770 .map(|r| SearchResult {
771 id: r.point.id,
772 score: r.score,
773 })
774 .collect())
775 }
776
777 pub fn flush(&self) -> Result<(), VelesError> {
779 self.inner.flush()?;
780 Ok(())
781 }
782
783 pub fn all_ids(&self) -> Vec<u64> {
785 self.inner.all_ids()
786 }
787
788 pub fn create_index(&self, field_name: String) -> Result<(), VelesError> {
790 self.inner.create_index(&field_name)?;
791 Ok(())
792 }
793
794 pub fn has_secondary_index(&self, field_name: String) -> bool {
796 self.inner.has_secondary_index(&field_name)
797 }
798
799 pub fn create_property_index(&self, label: String, property: String) -> Result<(), VelesError> {
801 self.inner.create_property_index(&label, &property)?;
802 Ok(())
803 }
804
805 pub fn create_range_index(&self, label: String, property: String) -> Result<(), VelesError> {
807 self.inner.create_range_index(&label, &property)?;
808 Ok(())
809 }
810
811 pub fn has_property_index(&self, label: String, property: String) -> bool {
813 self.inner.has_property_index(&label, &property)
814 }
815
816 pub fn has_range_index(&self, label: String, property: String) -> bool {
818 self.inner.has_range_index(&label, &property)
819 }
820
821 pub fn list_indexes(&self) -> Vec<MobileIndexInfo> {
823 self.inner
824 .list_indexes()
825 .into_iter()
826 .map(MobileIndexInfo::from)
827 .collect()
828 }
829
830 pub fn drop_index(&self, label: String, property: String) -> Result<bool, VelesError> {
832 Ok(self.inner.drop_index(&label, &property)?)
833 }
834
835 pub fn indexes_memory_usage(&self) -> u64 {
837 u64::try_from(self.inner.indexes_memory_usage()).unwrap_or(u64::MAX)
838 }
839
840 pub fn analyze(&self) -> Result<MobileCollectionStats, VelesError> {
842 Ok(self.inner.analyze()?.into())
843 }
844
845 pub fn get_stats(&self) -> MobileCollectionStats {
847 self.inner.get_stats().into()
848 }
849
850 pub fn sparse_search(
866 &self,
867 sparse_vector: VelesSparseVector,
868 limit: u32,
869 index_name: Option<String>,
870 ) -> Result<Vec<SearchResult>, VelesError> {
871 let core_sv = Self::to_core_sparse_vector(&sparse_vector);
872 let idx_name = index_name.unwrap_or_default();
873
874 let results = self
875 .inner
876 .sparse_search(
877 &core_sv,
878 usize::try_from(limit).unwrap_or(usize::MAX),
879 &idx_name,
880 )
881 .map_err(|e| VelesError::Database {
882 message: format!("Sparse search failed: {e}"),
883 })?;
884
885 Ok(results
886 .into_iter()
887 .map(|r| SearchResult {
888 id: r.point.id,
889 score: r.score,
890 })
891 .collect())
892 }
893
894 pub fn hybrid_sparse_search(
910 &self,
911 vector: Vec<f32>,
912 sparse_vector: VelesSparseVector,
913 limit: u32,
914 index_name: Option<String>,
915 ) -> Result<Vec<SearchResult>, VelesError> {
916 let core_sv = Self::to_core_sparse_vector(&sparse_vector);
917 let strategy = velesdb_core::fusion::FusionStrategy::RRF { k: 60 };
918 let idx_name = index_name.unwrap_or_default();
919
920 let results = self
921 .inner
922 .hybrid_sparse_search(
923 &vector,
924 &core_sv,
925 usize::try_from(limit).unwrap_or(usize::MAX),
926 &idx_name,
927 &strategy,
928 )
929 .map_err(|e| VelesError::Database {
930 message: format!("Hybrid sparse search failed: {e}"),
931 })?;
932
933 Ok(results
934 .into_iter()
935 .map(|r| SearchResult {
936 id: r.point.id,
937 score: r.score,
938 })
939 .collect())
940 }
941
942 pub fn upsert_with_sparse(
949 &self,
950 point: VelesPoint,
951 sparse_vector: VelesSparseVector,
952 ) -> Result<(), VelesError> {
953 let payload = point
954 .payload
955 .map(|s| serde_json::from_str(&s))
956 .transpose()
957 .map_err(|e| VelesError::Database {
958 message: format!("Invalid JSON payload: {e}"),
959 })?;
960
961 let core_sv = Self::to_core_sparse_vector(&sparse_vector);
962 let mut sparse_map = std::collections::BTreeMap::new();
963 sparse_map.insert(String::new(), core_sv);
964
965 let core_point =
966 velesdb_core::Point::with_sparse(point.id, point.vector, payload, Some(sparse_map));
967 self.inner.upsert(vec![core_point])?;
968 Ok(())
969 }
970}
971
972impl VelesCollection {
973 fn to_core_sparse_vector(sv: &VelesSparseVector) -> velesdb_core::sparse_index::SparseVector {
976 let pairs: Vec<(u32, f32)> = sv
977 .indices
978 .iter()
979 .copied()
980 .zip(sv.values.iter().copied())
981 .collect();
982 velesdb_core::sparse_index::SparseVector::new(pairs)
983 }
984}
985
986#[cfg(test)]
991#[path = "lib_tests.rs"]
992mod tests;