velesdb-mobile 1.16.0

VelesDB mobile bindings for iOS and Android via UniFFI
Documentation
//! Mobile types and enums module (EPIC-061/US-005 refactoring).
//!
//! Extracted from lib.rs to improve modularity.

use velesdb_core::DistanceMetric as CoreDistanceMetric;
use velesdb_core::FusionStrategy as CoreFusionStrategy;

// ============================================================================
// Error Types
// ============================================================================

/// Errors that can occur when using VelesDB on mobile.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum VelesError {
    /// Database operation failed.
    #[error("Database error: {message}")]
    Database { message: String },

    /// Collection operation failed.
    #[error("Collection error: {message}")]
    Collection { message: String },

    /// Vector dimension mismatch.
    #[error("Dimension mismatch: expected {expected}, got {actual}")]
    DimensionMismatch { expected: u32, actual: u32 },
}

impl From<velesdb_core::Error> for VelesError {
    fn from(err: velesdb_core::Error) -> Self {
        match err {
            velesdb_core::Error::DimensionMismatch { expected, actual } =>
            {
                #[allow(clippy::cast_possible_truncation)]
                VelesError::DimensionMismatch {
                    expected: expected as u32,
                    actual: actual as u32,
                }
            }
            velesdb_core::Error::CollectionNotFound(name) => VelesError::Collection {
                message: format!("Collection not found: {name}"),
            },
            velesdb_core::Error::CollectionExists(name) => VelesError::Collection {
                message: format!("Collection already exists: {name}"),
            },
            other => VelesError::Database {
                message: other.to_string(),
            },
        }
    }
}

// ============================================================================
// Enums
// ============================================================================

/// Distance metric for vector similarity.
#[derive(Debug, Clone, Copy, uniffi::Enum)]
pub enum DistanceMetric {
    /// Cosine similarity (1 - cosine_distance). Higher is more similar.
    Cosine,
    /// Euclidean (L2) distance. Lower is more similar.
    Euclidean,
    /// Dot product. Higher is more similar (for normalized vectors).
    DotProduct,
    /// Hamming distance for binary vectors. Lower is more similar.
    Hamming,
    /// Jaccard similarity for set-like vectors. Higher is more similar.
    Jaccard,
}

impl From<DistanceMetric> for CoreDistanceMetric {
    fn from(metric: DistanceMetric) -> Self {
        match metric {
            DistanceMetric::Cosine => CoreDistanceMetric::Cosine,
            DistanceMetric::Euclidean => CoreDistanceMetric::Euclidean,
            DistanceMetric::DotProduct => CoreDistanceMetric::DotProduct,
            DistanceMetric::Hamming => CoreDistanceMetric::Hamming,
            DistanceMetric::Jaccard => CoreDistanceMetric::Jaccard,
        }
    }
}

/// Storage mode for vector quantization (IoT/Edge optimization).
#[derive(Debug, Clone, Copy, uniffi::Enum)]
pub enum StorageMode {
    /// Full f32 precision (4 bytes/dimension). Best recall.
    Full,
    /// SQ8: 8-bit scalar quantization (1 byte/dimension). 4x compression, ~1% recall loss.
    Sq8,
    /// Binary: 1-bit quantization (1 bit/dimension). 32x compression, ~5-10% recall loss.
    Binary,
    /// Product Quantization (PQ): aggressive lossy compression (8x-16x typical).
    ProductQuantization,
    /// `RaBitQ`: 1-bit with rotation + scalar correction. 32x compression, ~1-2% recall loss.
    Rabitq,
}

impl From<StorageMode> for velesdb_core::StorageMode {
    fn from(mode: StorageMode) -> Self {
        match mode {
            StorageMode::Full => velesdb_core::StorageMode::Full,
            StorageMode::Sq8 => velesdb_core::StorageMode::SQ8,
            StorageMode::Binary => velesdb_core::StorageMode::Binary,
            StorageMode::ProductQuantization => velesdb_core::StorageMode::ProductQuantization,
            StorageMode::Rabitq => velesdb_core::StorageMode::RaBitQ,
        }
    }
}

/// Search quality profile controlling the recall/latency tradeoff.
///
/// Maps to core [`velesdb_core::SearchQuality`]. In HNSW-backed collections,
/// this controls the `ef_search` parameter. Higher quality means better recall
/// at the cost of increased latency.
#[derive(Debug, Clone, Default, uniffi::Enum)]
pub enum SearchQuality {
    /// Fast search (`ef_search=96`). ~95% recall, lowest latency.
    Fast,
    /// Balanced search (`ef_search=160`). ~99.5% recall, production default.
    #[default]
    Balanced,
    /// Accurate search (`ef_search=512`). ~100% recall.
    Accurate,
    /// Perfect recall mode (`ef_search=4096`). Guaranteed 100% recall.
    Perfect,
    /// Custom `ef_search` value for fine-grained control.
    Custom {
        /// The `ef_search` expansion factor.
        ef: u32,
    },
    /// Adaptive two-phase search that starts low and doubles if needed.
    Adaptive {
        /// Minimum `ef_search` (starting point).
        min_ef: u32,
        /// Maximum `ef_search` (cap).
        max_ef: u32,
    },
    /// Auto-tuned adaptive search based on collection statistics.
    AutoTune,
}

impl From<SearchQuality> for velesdb_core::SearchQuality {
    fn from(quality: SearchQuality) -> Self {
        match quality {
            SearchQuality::Fast => velesdb_core::SearchQuality::Fast,
            SearchQuality::Balanced => velesdb_core::SearchQuality::Balanced,
            SearchQuality::Accurate => velesdb_core::SearchQuality::Accurate,
            SearchQuality::Perfect => velesdb_core::SearchQuality::Perfect,
            SearchQuality::Custom { ef } => {
                velesdb_core::SearchQuality::Custom(usize::try_from(ef).unwrap_or(usize::MAX))
            }
            SearchQuality::Adaptive { min_ef, max_ef } => velesdb_core::SearchQuality::Adaptive {
                min_ef: usize::try_from(min_ef).unwrap_or(usize::MAX),
                max_ef: usize::try_from(max_ef).unwrap_or(usize::MAX),
            },
            SearchQuality::AutoTune => velesdb_core::SearchQuality::AutoTune,
        }
    }
}

/// Fusion strategy for combining results from multiple vector searches.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum FusionStrategy {
    /// Average scores across all queries.
    Average,
    /// Take the maximum score for each document.
    Maximum,
    /// Reciprocal Rank Fusion with configurable k parameter.
    Rrf {
        /// RRF k parameter (default: 60). Lower k emphasizes top ranks more.
        k: u32,
    },
    /// Weighted combination of average, maximum, and hit ratio.
    Weighted {
        /// Weight for average score (0.0-1.0).
        avg_weight: f32,
        /// Weight for maximum score (0.0-1.0).
        max_weight: f32,
        /// Weight for hit ratio (0.0-1.0).
        hit_weight: f32,
    },
    /// Relative Score Fusion for dense + sparse hybrid search.
    RelativeScore {
        /// Weight for the dense (vector) branch (0.0-1.0).
        dense_weight: f32,
        /// Weight for the sparse branch (0.0-1.0).
        sparse_weight: f32,
    },
}

impl From<FusionStrategy> for CoreFusionStrategy {
    fn from(strategy: FusionStrategy) -> Self {
        match strategy {
            FusionStrategy::Average => CoreFusionStrategy::Average,
            FusionStrategy::Maximum => CoreFusionStrategy::Maximum,
            FusionStrategy::Rrf { k } => CoreFusionStrategy::RRF { k },
            FusionStrategy::Weighted {
                avg_weight,
                max_weight,
                hit_weight,
            } => CoreFusionStrategy::Weighted {
                avg_weight,
                max_weight,
                hit_weight,
            },
            FusionStrategy::RelativeScore {
                dense_weight,
                sparse_weight,
            } => CoreFusionStrategy::RelativeScore {
                dense_weight,
                sparse_weight,
            },
        }
    }
}

impl Default for FusionStrategy {
    fn default() -> Self {
        Self::Rrf { k: 60 }
    }
}

// ============================================================================
// Data Types
// ============================================================================

/// A sparse vector represented as parallel arrays of indices and values.
///
/// Uses parallel `Vec<u32>` / `Vec<f32>` instead of `HashMap` for safe FFI
/// mapping to all mobile targets (Swift arrays, Kotlin IntArray/FloatArray).
#[derive(Debug, Clone, uniffi::Record)]
pub struct VelesSparseVector {
    /// Dimension indices (must be sorted, unique).
    pub indices: Vec<u32>,
    /// Weights corresponding to each index.
    pub values: Vec<f32>,
}

/// Configuration for Product Quantization training.
#[derive(Debug, Clone, uniffi::Record)]
pub struct PqTrainConfig {
    /// Number of sub-quantizers (subspaces).
    pub m: u32,
    /// Number of centroids per sub-quantizer.
    pub k: u32,
    /// Whether to use Optimized Product Quantization.
    pub opq: bool,
}

/// A search result containing an ID and similarity score.
#[derive(Debug, Clone, uniffi::Record)]
pub struct SearchResult {
    /// Vector ID.
    pub id: u64,
    /// Similarity score.
    pub score: f32,
    /// Optional payload as JSON string (populated by `query()` method).
    pub payload: Option<String>,
}

/// A point to insert into the database.
#[derive(Debug, Clone, uniffi::Record)]
pub struct VelesPoint {
    /// Unique identifier.
    pub id: u64,
    /// Vector embedding.
    pub vector: Vec<f32>,
    /// Optional JSON payload as string.
    pub payload: Option<String>,
}

/// Individual search request within a batch.
#[derive(Debug, Clone, uniffi::Record)]
pub struct IndividualSearchRequest {
    /// Query vector.
    pub vector: Vec<f32>,
    /// Number of results.
    pub top_k: u32,
    /// Optional metadata filter as JSON string.
    pub filter: Option<String>,
}

/// Public statistics snapshot for a collection.
#[derive(Debug, Clone, uniffi::Record)]
pub struct MobileCollectionStats {
    /// Total number of points currently stored.
    pub total_points: u64,
    /// Total payload footprint in bytes.
    pub payload_size_bytes: u64,
    /// Number of rows in storage.
    pub row_count: u64,
    /// Number of deleted/tombstoned rows.
    pub deleted_count: u64,
    /// Mean row size estimate in bytes.
    pub avg_row_size_bytes: u64,
    /// Total collection size estimate in bytes.
    pub total_size_bytes: u64,
    /// Number of tracked fields.
    pub field_stats_count: u32,
    /// Number of tracked columns.
    pub column_stats_count: u32,
    /// Number of tracked indexes.
    pub index_stats_count: u32,
}

impl From<velesdb_core::collection::stats::CollectionStats> for MobileCollectionStats {
    fn from(stats: velesdb_core::collection::stats::CollectionStats) -> Self {
        Self {
            total_points: stats.total_points,
            payload_size_bytes: stats.payload_size_bytes,
            row_count: stats.row_count,
            deleted_count: stats.deleted_count,
            avg_row_size_bytes: stats.avg_row_size_bytes,
            total_size_bytes: stats.total_size_bytes,
            field_stats_count: u32::try_from(stats.field_stats.len()).unwrap_or(u32::MAX),
            column_stats_count: u32::try_from(stats.column_stats.len()).unwrap_or(u32::MAX),
            index_stats_count: u32::try_from(stats.index_stats.len()).unwrap_or(u32::MAX),
        }
    }
}

/// Metadata and graph index details.
#[derive(Debug, Clone, uniffi::Record)]
pub struct MobileIndexInfo {
    /// Node label.
    pub label: String,
    /// Property name.
    pub property: String,
    /// Index type name.
    pub index_type: String,
    /// Number of distinct values.
    pub cardinality: u64,
    /// Approximate memory usage in bytes.
    pub memory_bytes: u64,
}

impl From<velesdb_core::IndexInfo> for MobileIndexInfo {
    fn from(value: velesdb_core::IndexInfo) -> Self {
        Self {
            label: value.label,
            property: value.property,
            index_type: value.index_type,
            cardinality: u64::try_from(value.cardinality).unwrap_or(u64::MAX),
            memory_bytes: u64::try_from(value.memory_bytes).unwrap_or(u64::MAX),
        }
    }
}