Skip to main content

velesdb_mobile/
types.rs

1//! Mobile types and enums module (EPIC-061/US-005 refactoring).
2//!
3//! Extracted from lib.rs to improve modularity.
4
5use velesdb_core::DistanceMetric as CoreDistanceMetric;
6use velesdb_core::FusionStrategy as CoreFusionStrategy;
7
8// ============================================================================
9// Error Types
10// ============================================================================
11
12/// Errors that can occur when using VelesDB on mobile.
13#[derive(Debug, thiserror::Error, uniffi::Error)]
14pub enum VelesError {
15    /// Database operation failed.
16    ///
17    /// `code` carries the canonical core taxonomy code (e.g. `"VELES-006"`)
18    /// when the error originated in `velesdb-core`, or an empty string for
19    /// binding-level failures (JSON parsing, runtime setup) that have no core
20    /// code. `recoverable` mirrors core's [`velesdb_core::Error::is_recoverable`].
21    #[error("[{code}] Database error: {message}")]
22    Database {
23        message: String,
24        code: String,
25        recoverable: bool,
26    },
27
28    /// Collection operation failed.
29    #[error("Collection error: {message}")]
30    Collection { message: String },
31
32    /// Vector dimension mismatch.
33    #[error("Dimension mismatch: expected {expected}, got {actual}")]
34    DimensionMismatch { expected: u32, actual: u32 },
35}
36
37impl VelesError {
38    /// Constructs a binding-level `Database` error with no core taxonomy code.
39    ///
40    /// Use for failures that originate in the mobile binding itself (JSON
41    /// parsing, runtime creation, configuration) rather than in `velesdb-core`.
42    /// Binding-level errors are treated as recoverable.
43    #[must_use]
44    pub fn database(message: String) -> Self {
45        VelesError::Database {
46            message,
47            code: String::new(),
48            recoverable: true,
49        }
50    }
51}
52
53impl From<velesdb_core::Error> for VelesError {
54    fn from(err: velesdb_core::Error) -> Self {
55        let code = err.code().to_string();
56        let recoverable = err.is_recoverable();
57        match err {
58            velesdb_core::Error::DimensionMismatch { expected, actual } =>
59            {
60                #[allow(clippy::cast_possible_truncation)]
61                VelesError::DimensionMismatch {
62                    expected: expected as u32,
63                    actual: actual as u32,
64                }
65            }
66            velesdb_core::Error::CollectionNotFound(name) => VelesError::Collection {
67                message: format!("Collection not found: {name}"),
68            },
69            velesdb_core::Error::CollectionExists(name) => VelesError::Collection {
70                message: format!("Collection already exists: {name}"),
71            },
72            other => VelesError::Database {
73                message: other.to_string(),
74                code,
75                recoverable,
76            },
77        }
78    }
79}
80
81// ============================================================================
82// Enums
83// ============================================================================
84
85/// Distance metric for vector similarity.
86#[derive(Debug, Clone, Copy, uniffi::Enum)]
87pub enum DistanceMetric {
88    /// Cosine similarity (1 - cosine_distance). Higher is more similar.
89    Cosine,
90    /// Euclidean (L2) distance. Lower is more similar.
91    Euclidean,
92    /// Dot product. Higher is more similar (for normalized vectors).
93    DotProduct,
94    /// Hamming distance for binary vectors. Lower is more similar.
95    Hamming,
96    /// Jaccard similarity for set-like vectors. Higher is more similar.
97    Jaccard,
98}
99
100impl From<DistanceMetric> for CoreDistanceMetric {
101    fn from(metric: DistanceMetric) -> Self {
102        match metric {
103            DistanceMetric::Cosine => CoreDistanceMetric::Cosine,
104            DistanceMetric::Euclidean => CoreDistanceMetric::Euclidean,
105            DistanceMetric::DotProduct => CoreDistanceMetric::DotProduct,
106            DistanceMetric::Hamming => CoreDistanceMetric::Hamming,
107            DistanceMetric::Jaccard => CoreDistanceMetric::Jaccard,
108        }
109    }
110}
111
112/// Storage mode for vector quantization (IoT/Edge optimization).
113#[derive(Debug, Clone, Copy, uniffi::Enum)]
114pub enum StorageMode {
115    /// Full f32 precision (4 bytes/dimension). Best recall.
116    Full,
117    /// SQ8: 8-bit scalar quantization (1 byte/dimension). 4x compression, ~1% recall loss.
118    Sq8,
119    /// Binary: 1-bit quantization (1 bit/dimension). 32x compression, ~5-10% recall loss.
120    Binary,
121    /// Product Quantization (PQ): aggressive lossy compression (8x-16x typical).
122    ProductQuantization,
123    /// `RaBitQ`: 1-bit with rotation + scalar correction. 32x compression, ~1-2% recall loss.
124    Rabitq,
125}
126
127impl From<StorageMode> for velesdb_core::StorageMode {
128    fn from(mode: StorageMode) -> Self {
129        match mode {
130            StorageMode::Full => velesdb_core::StorageMode::Full,
131            StorageMode::Sq8 => velesdb_core::StorageMode::SQ8,
132            StorageMode::Binary => velesdb_core::StorageMode::Binary,
133            StorageMode::ProductQuantization => velesdb_core::StorageMode::ProductQuantization,
134            StorageMode::Rabitq => velesdb_core::StorageMode::RaBitQ,
135        }
136    }
137}
138
139/// Search quality profile controlling the recall/latency tradeoff.
140///
141/// Maps to core [`velesdb_core::SearchQuality`]. In HNSW-backed collections,
142/// this controls the `ef_search` parameter. Higher quality means better recall
143/// at the cost of increased latency.
144#[derive(Debug, Clone, Default, uniffi::Enum)]
145pub enum SearchQuality {
146    /// Fast search (`ef_search=96`). ~95% recall, lowest latency.
147    Fast,
148    /// Balanced search (`ef_search=160`). ~99.5% recall, production default.
149    #[default]
150    Balanced,
151    /// Accurate search (`ef_search=512`). ~100% recall.
152    Accurate,
153    /// Perfect recall mode (`ef_search=4096`). Guaranteed 100% recall.
154    Perfect,
155    /// Custom `ef_search` value for fine-grained control.
156    Custom {
157        /// The `ef_search` expansion factor.
158        ef: u32,
159    },
160    /// Adaptive two-phase search that starts low and doubles if needed.
161    Adaptive {
162        /// Minimum `ef_search` (starting point).
163        min_ef: u32,
164        /// Maximum `ef_search` (cap).
165        max_ef: u32,
166    },
167    /// Auto-tuned adaptive search based on collection statistics.
168    AutoTune,
169}
170
171impl From<SearchQuality> for velesdb_core::SearchQuality {
172    fn from(quality: SearchQuality) -> Self {
173        match quality {
174            SearchQuality::Fast => velesdb_core::SearchQuality::Fast,
175            SearchQuality::Balanced => velesdb_core::SearchQuality::Balanced,
176            SearchQuality::Accurate => velesdb_core::SearchQuality::Accurate,
177            SearchQuality::Perfect => velesdb_core::SearchQuality::Perfect,
178            SearchQuality::Custom { ef } => {
179                velesdb_core::SearchQuality::Custom(usize::try_from(ef).unwrap_or(usize::MAX))
180            }
181            SearchQuality::Adaptive { min_ef, max_ef } => velesdb_core::SearchQuality::Adaptive {
182                min_ef: usize::try_from(min_ef).unwrap_or(usize::MAX),
183                max_ef: usize::try_from(max_ef).unwrap_or(usize::MAX),
184            },
185            SearchQuality::AutoTune => velesdb_core::SearchQuality::AutoTune,
186        }
187    }
188}
189
190/// Fusion strategy for combining results from multiple vector searches.
191#[derive(Debug, Clone, uniffi::Enum)]
192pub enum FusionStrategy {
193    /// Average scores across all queries.
194    Average,
195    /// Take the maximum score for each document.
196    Maximum,
197    /// Reciprocal Rank Fusion with configurable k parameter.
198    Rrf {
199        /// RRF k parameter (default: 60). Lower k emphasizes top ranks more.
200        k: u32,
201    },
202    /// Weighted combination of average, maximum, and hit ratio.
203    Weighted {
204        /// Weight for average score (0.0-1.0).
205        avg_weight: f32,
206        /// Weight for maximum score (0.0-1.0).
207        max_weight: f32,
208        /// Weight for hit ratio (0.0-1.0).
209        hit_weight: f32,
210    },
211    /// Relative Score Fusion for dense + sparse hybrid search.
212    RelativeScore {
213        /// Weight for the dense (vector) branch (0.0-1.0).
214        dense_weight: f32,
215        /// Weight for the sparse branch (0.0-1.0).
216        sparse_weight: f32,
217    },
218}
219
220impl From<FusionStrategy> for CoreFusionStrategy {
221    fn from(strategy: FusionStrategy) -> Self {
222        match strategy {
223            FusionStrategy::Average => CoreFusionStrategy::Average,
224            FusionStrategy::Maximum => CoreFusionStrategy::Maximum,
225            FusionStrategy::Rrf { k } => CoreFusionStrategy::RRF { k },
226            FusionStrategy::Weighted {
227                avg_weight,
228                max_weight,
229                hit_weight,
230            } => CoreFusionStrategy::Weighted {
231                avg_weight,
232                max_weight,
233                hit_weight,
234            },
235            FusionStrategy::RelativeScore {
236                dense_weight,
237                sparse_weight,
238            } => CoreFusionStrategy::RelativeScore {
239                dense_weight,
240                sparse_weight,
241            },
242        }
243    }
244}
245
246impl Default for FusionStrategy {
247    fn default() -> Self {
248        Self::Rrf { k: 60 }
249    }
250}
251
252// ============================================================================
253// Data Types
254// ============================================================================
255
256/// A sparse vector represented as parallel arrays of indices and values.
257///
258/// Uses parallel `Vec<u32>` / `Vec<f32>` instead of `HashMap` for safe FFI
259/// mapping to all mobile targets (Swift arrays, Kotlin IntArray/FloatArray).
260#[derive(Debug, Clone, uniffi::Record)]
261pub struct VelesSparseVector {
262    /// Dimension indices (must be sorted, unique).
263    pub indices: Vec<u32>,
264    /// Weights corresponding to each index.
265    pub values: Vec<f32>,
266}
267
268/// Configuration for Product Quantization training.
269#[derive(Debug, Clone, uniffi::Record)]
270pub struct PqTrainConfig {
271    /// Number of sub-quantizers (subspaces).
272    pub m: u32,
273    /// Number of centroids per sub-quantizer.
274    pub k: u32,
275    /// Whether to use Optimized Product Quantization.
276    pub opq: bool,
277}
278
279/// A search result containing an ID and similarity score.
280#[derive(Debug, Clone, uniffi::Record)]
281pub struct SearchResult {
282    /// Vector ID.
283    pub id: u64,
284    /// Similarity score.
285    pub score: f32,
286    /// Optional payload as JSON string (populated by `query()` method).
287    pub payload: Option<String>,
288}
289
290/// A point to insert into the database.
291#[derive(Debug, Clone, uniffi::Record)]
292pub struct VelesPoint {
293    /// Unique identifier.
294    pub id: u64,
295    /// Vector embedding.
296    pub vector: Vec<f32>,
297    /// Optional JSON payload as string.
298    pub payload: Option<String>,
299}
300
301/// Individual search request within a batch.
302#[derive(Debug, Clone, uniffi::Record)]
303pub struct IndividualSearchRequest {
304    /// Query vector.
305    pub vector: Vec<f32>,
306    /// Number of results.
307    pub top_k: u32,
308    /// Optional metadata filter as JSON string.
309    pub filter: Option<String>,
310}
311
312/// Public statistics snapshot for a collection.
313#[derive(Debug, Clone, uniffi::Record)]
314pub struct MobileCollectionStats {
315    /// Total number of points currently stored.
316    pub total_points: u64,
317    /// Total payload footprint in bytes.
318    pub payload_size_bytes: u64,
319    /// Number of rows in storage.
320    pub row_count: u64,
321    /// Number of deleted/tombstoned rows.
322    pub deleted_count: u64,
323    /// Mean row size estimate in bytes.
324    pub avg_row_size_bytes: u64,
325    /// Total collection size estimate in bytes.
326    pub total_size_bytes: u64,
327    /// Number of tracked fields.
328    pub field_stats_count: u32,
329    /// Number of tracked columns.
330    pub column_stats_count: u32,
331    /// Number of tracked indexes.
332    pub index_stats_count: u32,
333}
334
335impl From<velesdb_core::collection::stats::CollectionStats> for MobileCollectionStats {
336    fn from(stats: velesdb_core::collection::stats::CollectionStats) -> Self {
337        Self {
338            total_points: stats.total_points,
339            payload_size_bytes: stats.payload_size_bytes,
340            row_count: stats.row_count,
341            deleted_count: stats.deleted_count,
342            avg_row_size_bytes: stats.avg_row_size_bytes,
343            total_size_bytes: stats.total_size_bytes,
344            field_stats_count: u32::try_from(stats.field_stats.len()).unwrap_or(u32::MAX),
345            column_stats_count: u32::try_from(stats.column_stats.len()).unwrap_or(u32::MAX),
346            index_stats_count: u32::try_from(stats.index_stats.len()).unwrap_or(u32::MAX),
347        }
348    }
349}
350
351/// Diagnostic snapshot of a collection's health and search readiness.
352///
353/// FFI mirror of [`velesdb_core::collection::CollectionDiagnostics`]. The
354/// `index_health` enum is flattened to a stable lowercase string
355/// (`"healthy"`, `"empty"`, `"needs_rebuild"`, `"unknown"`) with an optional
356/// detail message, matching the REST and Python bindings.
357#[derive(Debug, Clone, uniffi::Record)]
358pub struct MobileCollectionDiagnostics {
359    /// Whether the collection contains at least one vector/point.
360    pub has_vectors: bool,
361    /// Whether the collection is ready to serve search queries.
362    pub search_ready: bool,
363    /// Whether a valid dimension is configured.
364    pub dimension_configured: bool,
365    /// Total number of points in the collection.
366    pub point_count: u64,
367    /// Health status of the primary search index.
368    pub index_health: String,
369    /// Optional detail message (e.g. the reason a rebuild is needed).
370    pub index_health_detail: Option<String>,
371}
372
373impl From<velesdb_core::collection::CollectionDiagnostics> for MobileCollectionDiagnostics {
374    fn from(diag: velesdb_core::collection::CollectionDiagnostics) -> Self {
375        use velesdb_core::collection::IndexHealth;
376        let (index_health, index_health_detail) = match diag.index_health {
377            IndexHealth::Healthy => ("healthy".to_string(), None),
378            IndexHealth::Empty => ("empty".to_string(), None),
379            IndexHealth::NeedsRebuild(reason) => ("needs_rebuild".to_string(), Some(reason)),
380            _ => ("unknown".to_string(), None),
381        };
382        Self {
383            has_vectors: diag.has_vectors,
384            search_ready: diag.search_ready,
385            dimension_configured: diag.dimension_configured,
386            point_count: u64::try_from(diag.point_count).unwrap_or(u64::MAX),
387            index_health,
388            index_health_detail,
389        }
390    }
391}
392
393/// Metadata and graph index details.
394#[derive(Debug, Clone, uniffi::Record)]
395pub struct MobileIndexInfo {
396    /// Node label.
397    pub label: String,
398    /// Property name.
399    pub property: String,
400    /// Index type name.
401    pub index_type: String,
402    /// Number of distinct values.
403    pub cardinality: u64,
404    /// Approximate memory usage in bytes.
405    pub memory_bytes: u64,
406}
407
408impl From<velesdb_core::IndexInfo> for MobileIndexInfo {
409    fn from(value: velesdb_core::IndexInfo) -> Self {
410        Self {
411            label: value.label,
412            property: value.property,
413            index_type: value.index_type,
414            cardinality: u64::try_from(value.cardinality).unwrap_or(u64::MAX),
415            memory_bytes: u64::try_from(value.memory_bytes).unwrap_or(u64::MAX),
416        }
417    }
418}
419
420/// Runtime query guardrail limits for a collection.
421#[derive(Debug, Clone, uniffi::Record)]
422pub struct MobileQueryLimits {
423    /// Maximum graph traversal depth.
424    pub max_depth: u32,
425    /// Maximum intermediate cardinality.
426    pub max_cardinality: u64,
427    /// Memory limit per query in bytes.
428    pub memory_limit_bytes: u64,
429    /// Query timeout in milliseconds (0 disables the timeout).
430    pub timeout_ms: u64,
431    /// Rate limit: max queries per second per client.
432    pub rate_limit_qps: u32,
433    /// Circuit breaker: failure threshold before tripping.
434    pub circuit_failure_threshold: u32,
435    /// Circuit breaker: recovery time in seconds.
436    pub circuit_recovery_seconds: u64,
437}
438
439impl From<velesdb_core::guardrails::QueryLimits> for MobileQueryLimits {
440    fn from(v: velesdb_core::guardrails::QueryLimits) -> Self {
441        Self {
442            max_depth: v.max_depth,
443            max_cardinality: u64::try_from(v.max_cardinality).unwrap_or(u64::MAX),
444            memory_limit_bytes: u64::try_from(v.memory_limit_bytes).unwrap_or(u64::MAX),
445            timeout_ms: v.timeout_ms,
446            rate_limit_qps: v.rate_limit_qps,
447            circuit_failure_threshold: v.circuit_failure_threshold,
448            circuit_recovery_seconds: v.circuit_recovery_seconds,
449        }
450    }
451}
452
453impl From<MobileQueryLimits> for velesdb_core::guardrails::QueryLimits {
454    fn from(v: MobileQueryLimits) -> Self {
455        Self {
456            max_depth: v.max_depth,
457            max_cardinality: usize::try_from(v.max_cardinality).unwrap_or(usize::MAX),
458            memory_limit_bytes: usize::try_from(v.memory_limit_bytes).unwrap_or(usize::MAX),
459            timeout_ms: v.timeout_ms,
460            rate_limit_qps: v.rate_limit_qps,
461            circuit_failure_threshold: v.circuit_failure_threshold,
462            circuit_recovery_seconds: v.circuit_recovery_seconds,
463        }
464    }
465}
466
467/// Deferred indexing configuration (buffers bulk inserts before merging
468/// into the HNSW index).
469#[derive(Debug, Clone, uniffi::Record)]
470pub struct MobileDeferredIndexerConfig {
471    /// Whether deferred indexing is enabled.
472    pub enabled: bool,
473    /// Number of buffered points before a merge is triggered.
474    pub merge_threshold: u64,
475    /// Maximum buffer age in milliseconds before a forced merge. Checked
476    /// at write time (no background timer): an expired buffer merges on
477    /// the next write. `0` makes every write trigger a merge.
478    pub max_buffer_age_ms: u64,
479}
480
481impl From<MobileDeferredIndexerConfig>
482    for velesdb_core::collection::streaming::DeferredIndexerConfig
483{
484    fn from(v: MobileDeferredIndexerConfig) -> Self {
485        Self {
486            enabled: v.enabled,
487            merge_threshold: usize::try_from(v.merge_threshold).unwrap_or(usize::MAX),
488            max_buffer_age_ms: v.max_buffer_age_ms,
489        }
490    }
491}
492
493/// Async index builder configuration (parallel segment construction for
494/// deferred bulk loads).
495#[derive(Debug, Clone, uniffi::Record)]
496pub struct MobileAsyncIndexBuilderConfig {
497    /// Number of buffered points before a segment merge is triggered.
498    pub merge_threshold: u64,
499    /// Reserved — parsed but not yet wired (core issue #488 Task 4):
500    /// flushes parallelize on the global thread pool and this knob
501    /// changes nothing today.
502    pub segment_count: Option<u32>,
503}
504
505impl From<MobileAsyncIndexBuilderConfig>
506    for velesdb_core::collection::streaming::AsyncIndexBuilderConfig
507{
508    fn from(v: MobileAsyncIndexBuilderConfig) -> Self {
509        Self {
510            merge_threshold: usize::try_from(v.merge_threshold).unwrap_or(usize::MAX),
511            segment_count: v.segment_count.map(|s| s as usize),
512        }
513    }
514}
515
516/// Configuration for streaming ingestion on a collection.
517///
518/// FFI mirror of [`velesdb_core::StreamingConfig`]. Fields are `u64` for
519/// portable mapping to Swift/Kotlin; they are narrowed to `usize`/`u64` when
520/// converted to the core type. Engine defaults are `buffer_size=10000`,
521/// `batch_size=128`, `flush_interval_ms=50`.
522#[derive(Debug, Clone, uniffi::Record)]
523pub struct MobileStreamingConfig {
524    /// Capacity of the bounded channel (backpressure threshold). Default 10000.
525    pub buffer_size: u64,
526    /// Number of points that trigger an immediate micro-batch flush. Default 128.
527    pub batch_size: u64,
528    /// Maximum time (ms) before a partial batch is flushed. Default 50.
529    pub flush_interval_ms: u64,
530}
531
532/// Post-creation overrides for advanced collection configuration.
533///
534/// Each field uses `Some` to set the value and `None` to leave it
535/// unchanged. Unlike the Python binding, mobile cannot express the
536/// "clear" state (it maps `None` to "leave unchanged").
537#[derive(Debug, Clone, uniffi::Record)]
538pub struct MobileAdvancedConfig {
539    /// PQ rescore oversampling factor; `None` leaves it unchanged.
540    pub pq_rescore_oversampling: Option<u32>,
541    /// Deferred indexing config; `None` leaves it unchanged.
542    pub deferred_indexing: Option<MobileDeferredIndexerConfig>,
543    /// Async index builder config; `None` leaves it unchanged.
544    pub async_index_builder: Option<MobileAsyncIndexBuilderConfig>,
545}
546
547#[cfg(test)]
548#[path = "types_tests.rs"]
549mod error_tests;