Skip to main content

dakera_client/
types.rs

1//! Types for the Dakera client SDK
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6// ============================================================================
7// Retry & Timeout Configuration
8// ============================================================================
9
10/// Configuration for request retry behavior with exponential backoff.
11#[derive(Debug, Clone)]
12pub struct RetryConfig {
13    /// Maximum number of retry attempts (default: 3).
14    pub max_retries: u32,
15    /// Base delay before the first retry (default: 100ms).
16    pub base_delay: std::time::Duration,
17    /// Maximum delay between retries (default: 60s).
18    pub max_delay: std::time::Duration,
19    /// Whether to add random jitter to backoff delay (default: true).
20    pub jitter: bool,
21}
22
23impl Default for RetryConfig {
24    fn default() -> Self {
25        Self {
26            max_retries: 3,
27            base_delay: std::time::Duration::from_millis(100),
28            max_delay: std::time::Duration::from_secs(60),
29            jitter: true,
30        }
31    }
32}
33
34// ============================================================================
35// OPS-1: Rate-Limit Headers
36// ============================================================================
37
38/// Rate-limit and quota headers present on every API response (OPS-1).
39///
40/// Fields are `None` when the server does not include the header (e.g.
41/// non-namespaced endpoints where quota does not apply).
42#[derive(Debug, Clone, Default)]
43pub struct RateLimitHeaders {
44    /// `X-RateLimit-Limit` — max requests allowed in the current window.
45    pub limit: Option<u64>,
46    /// `X-RateLimit-Remaining` — requests left in the current window.
47    pub remaining: Option<u64>,
48    /// `X-RateLimit-Reset` — Unix timestamp (seconds) when the window resets.
49    pub reset: Option<u64>,
50    /// `X-Quota-Used` — namespace vectors / storage consumed.
51    pub quota_used: Option<u64>,
52    /// `X-Quota-Limit` — namespace quota ceiling.
53    pub quota_limit: Option<u64>,
54}
55
56impl RateLimitHeaders {
57    /// Parse rate-limit headers from a `reqwest::Response`.
58    pub fn from_response(response: &reqwest::Response) -> Self {
59        let headers = response.headers();
60        fn parse(h: &reqwest::header::HeaderMap, name: &str) -> Option<u64> {
61            h.get(name)
62                .and_then(|v| v.to_str().ok())
63                .and_then(|s| s.parse().ok())
64        }
65        Self {
66            limit: parse(headers, "X-RateLimit-Limit"),
67            remaining: parse(headers, "X-RateLimit-Remaining"),
68            reset: parse(headers, "X-RateLimit-Reset"),
69            quota_used: parse(headers, "X-Quota-Used"),
70            quota_limit: parse(headers, "X-Quota-Limit"),
71        }
72    }
73}
74
75// ============================================================================
76// Health & Status Types
77// ============================================================================
78
79/// Health check response
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct HealthResponse {
82    /// Overall health status
83    pub healthy: bool,
84    /// Service version
85    pub version: Option<String>,
86    /// Uptime in seconds
87    pub uptime_seconds: Option<u64>,
88}
89
90/// Readiness check response
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ReadinessResponse {
93    /// Is the service ready to accept requests
94    pub ready: bool,
95    /// Component status details
96    pub components: Option<HashMap<String, bool>>,
97}
98
99// ============================================================================
100// Namespace Types
101// ============================================================================
102
103/// Namespace information
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct NamespaceInfo {
106    /// Namespace name
107    #[serde(alias = "namespace")]
108    pub name: String,
109    /// Number of vectors in the namespace
110    pub vector_count: u64,
111    /// Vector dimensions
112    #[serde(alias = "dimension")]
113    pub dimensions: Option<u32>,
114    /// Index type used
115    pub index_type: Option<String>,
116}
117
118/// List namespaces response
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ListNamespacesResponse {
121    /// List of namespace names
122    pub namespaces: Vec<String>,
123}
124
125// ============================================================================
126// Vector Types
127// ============================================================================
128
129/// A vector with ID and optional metadata
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct Vector {
132    /// Unique vector identifier
133    pub id: String,
134    /// Vector values (embeddings)
135    pub values: Vec<f32>,
136    /// Optional metadata
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub metadata: Option<HashMap<String, serde_json::Value>>,
139}
140
141impl Vector {
142    /// Create a new vector with just ID and values
143    pub fn new(id: impl Into<String>, values: Vec<f32>) -> Self {
144        Self {
145            id: id.into(),
146            values,
147            metadata: None,
148        }
149    }
150
151    /// Create a new vector with metadata
152    pub fn with_metadata(
153        id: impl Into<String>,
154        values: Vec<f32>,
155        metadata: HashMap<String, serde_json::Value>,
156    ) -> Self {
157        Self {
158            id: id.into(),
159            values,
160            metadata: Some(metadata),
161        }
162    }
163}
164
165/// Upsert vectors request
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct UpsertRequest {
168    /// Vectors to upsert
169    pub vectors: Vec<Vector>,
170}
171
172impl UpsertRequest {
173    /// Create a new upsert request with a single vector
174    pub fn single(vector: Vector) -> Self {
175        Self {
176            vectors: vec![vector],
177        }
178    }
179
180    /// Create a new upsert request with multiple vectors
181    pub fn batch(vectors: Vec<Vector>) -> Self {
182        Self { vectors }
183    }
184}
185
186/// Upsert response
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct UpsertResponse {
189    /// Number of vectors upserted
190    pub upserted_count: u64,
191}
192
193/// Column-based upsert request (Turbopuffer-inspired)
194///
195/// This format is more efficient for bulk upserts as it avoids repeating
196/// field names for each vector. All arrays must have equal length.
197///
198/// # Example
199///
200/// ```rust
201/// use dakera_client::ColumnUpsertRequest;
202/// use std::collections::HashMap;
203///
204/// let request = ColumnUpsertRequest::new(
205///     vec!["id1".to_string(), "id2".to_string()],
206///     vec![vec![0.1, 0.2, 0.3], vec![0.4, 0.5, 0.6]],
207/// );
208/// ```
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ColumnUpsertRequest {
211    /// Array of vector IDs (required)
212    pub ids: Vec<String>,
213    /// Array of vectors (required for vector namespaces)
214    pub vectors: Vec<Vec<f32>>,
215    /// Additional attributes as columns (optional)
216    /// Each key is an attribute name, value is array of attribute values
217    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
218    pub attributes: HashMap<String, Vec<serde_json::Value>>,
219    /// TTL in seconds for all vectors (optional)
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub ttl_seconds: Option<u64>,
222    /// Expected dimension (optional, for validation)
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub dimension: Option<usize>,
225}
226
227impl ColumnUpsertRequest {
228    /// Create a new column upsert request
229    pub fn new(ids: Vec<String>, vectors: Vec<Vec<f32>>) -> Self {
230        Self {
231            ids,
232            vectors,
233            attributes: HashMap::new(),
234            ttl_seconds: None,
235            dimension: None,
236        }
237    }
238
239    /// Add an attribute column
240    pub fn with_attribute(
241        mut self,
242        name: impl Into<String>,
243        values: Vec<serde_json::Value>,
244    ) -> Self {
245        self.attributes.insert(name.into(), values);
246        self
247    }
248
249    /// Set TTL for all vectors
250    pub fn with_ttl(mut self, seconds: u64) -> Self {
251        self.ttl_seconds = Some(seconds);
252        self
253    }
254
255    /// Set expected dimension for validation
256    pub fn with_dimension(mut self, dim: usize) -> Self {
257        self.dimension = Some(dim);
258        self
259    }
260
261    /// Get the number of vectors in this request
262    pub fn len(&self) -> usize {
263        self.ids.len()
264    }
265
266    /// Check if the request is empty
267    pub fn is_empty(&self) -> bool {
268        self.ids.is_empty()
269    }
270}
271
272/// Delete vectors request
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct DeleteRequest {
275    /// Vector IDs to delete
276    pub ids: Vec<String>,
277}
278
279impl DeleteRequest {
280    /// Create a delete request for a single ID
281    pub fn single(id: impl Into<String>) -> Self {
282        Self {
283            ids: vec![id.into()],
284        }
285    }
286
287    /// Create a delete request for multiple IDs
288    pub fn batch(ids: Vec<String>) -> Self {
289        Self { ids }
290    }
291}
292
293/// Delete response
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct DeleteResponse {
296    /// Number of vectors deleted
297    pub deleted_count: u64,
298}
299
300// ============================================================================
301// Query Types
302// ============================================================================
303
304/// Read consistency level for queries (Turbopuffer-inspired)
305#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
306#[serde(rename_all = "snake_case")]
307pub enum ReadConsistency {
308    /// Always read from primary/leader node - guarantees latest data
309    Strong,
310    /// Read from any replica - may return slightly stale data but faster
311    #[default]
312    Eventual,
313    /// Read from replicas within staleness bounds
314    #[serde(rename = "bounded_staleness")]
315    BoundedStaleness,
316}
317
318/// Configuration for bounded staleness reads
319#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
320pub struct StalenessConfig {
321    /// Maximum acceptable staleness in milliseconds
322    #[serde(default = "default_max_staleness_ms")]
323    pub max_staleness_ms: u64,
324}
325
326fn default_max_staleness_ms() -> u64 {
327    5000 // 5 seconds default
328}
329
330impl StalenessConfig {
331    /// Create a new staleness config with specified max staleness
332    pub fn new(max_staleness_ms: u64) -> Self {
333        Self { max_staleness_ms }
334    }
335}
336
337/// Distance metric for similarity search
338#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
339#[serde(rename_all = "snake_case")]
340pub enum DistanceMetric {
341    /// Cosine similarity (default)
342    #[default]
343    Cosine,
344    /// Euclidean distance
345    Euclidean,
346    /// Dot product
347    DotProduct,
348}
349
350/// Query request for vector similarity search
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct QueryRequest {
353    /// Query vector
354    pub vector: Vec<f32>,
355    /// Number of results to return
356    pub top_k: u32,
357    /// Distance metric to use
358    #[serde(default)]
359    pub distance_metric: DistanceMetric,
360    /// Optional filter expression
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub filter: Option<serde_json::Value>,
363    /// Whether to include metadata in results
364    #[serde(default = "default_true")]
365    pub include_metadata: bool,
366    /// Whether to include vector values in results
367    #[serde(default)]
368    pub include_vectors: bool,
369    /// Read consistency level
370    #[serde(default)]
371    pub consistency: ReadConsistency,
372    /// Staleness configuration for bounded staleness reads
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub staleness_config: Option<StalenessConfig>,
375}
376
377fn default_true() -> bool {
378    true
379}
380
381impl QueryRequest {
382    /// Create a new query request
383    pub fn new(vector: Vec<f32>, top_k: u32) -> Self {
384        Self {
385            vector,
386            top_k,
387            distance_metric: DistanceMetric::default(),
388            filter: None,
389            include_metadata: true,
390            include_vectors: false,
391            consistency: ReadConsistency::default(),
392            staleness_config: None,
393        }
394    }
395
396    /// Add a filter to the query
397    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
398        self.filter = Some(filter);
399        self
400    }
401
402    /// Set whether to include metadata
403    pub fn include_metadata(mut self, include: bool) -> Self {
404        self.include_metadata = include;
405        self
406    }
407
408    /// Set whether to include vector values
409    pub fn include_vectors(mut self, include: bool) -> Self {
410        self.include_vectors = include;
411        self
412    }
413
414    /// Set distance metric
415    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
416        self.distance_metric = metric;
417        self
418    }
419
420    /// Set read consistency level
421    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
422        self.consistency = consistency;
423        self
424    }
425
426    /// Set bounded staleness with max staleness in ms
427    pub fn with_bounded_staleness(mut self, max_staleness_ms: u64) -> Self {
428        self.consistency = ReadConsistency::BoundedStaleness;
429        self.staleness_config = Some(StalenessConfig::new(max_staleness_ms));
430        self
431    }
432
433    /// Use strong consistency (always read from primary)
434    pub fn with_strong_consistency(mut self) -> Self {
435        self.consistency = ReadConsistency::Strong;
436        self
437    }
438}
439
440/// A match result from a query
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct Match {
443    /// Vector ID
444    pub id: String,
445    /// Similarity score
446    pub score: f32,
447    /// Optional metadata
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub metadata: Option<HashMap<String, serde_json::Value>>,
450}
451
452/// Query response
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct QueryResponse {
455    /// Matched vectors
456    pub matches: Vec<Match>,
457}
458
459// ============================================================================
460// Full-Text Search Types
461// ============================================================================
462
463/// A document for full-text indexing
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct Document {
466    /// Document ID
467    pub id: String,
468    /// Document text content
469    pub text: String,
470    /// Optional metadata
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub metadata: Option<HashMap<String, serde_json::Value>>,
473}
474
475impl Document {
476    /// Create a new document
477    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
478        Self {
479            id: id.into(),
480            text: text.into(),
481            metadata: None,
482        }
483    }
484
485    /// Create a new document with metadata
486    pub fn with_metadata(
487        id: impl Into<String>,
488        text: impl Into<String>,
489        metadata: HashMap<String, serde_json::Value>,
490    ) -> Self {
491        Self {
492            id: id.into(),
493            text: text.into(),
494            metadata: Some(metadata),
495        }
496    }
497}
498
499/// Index documents request
500#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct IndexDocumentsRequest {
502    /// Documents to index
503    pub documents: Vec<Document>,
504}
505
506/// Index documents response
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct IndexDocumentsResponse {
509    /// Number of documents indexed
510    pub indexed_count: u64,
511}
512
513/// Full-text search request
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct FullTextSearchRequest {
516    /// Search query
517    pub query: String,
518    /// Maximum number of results
519    pub top_k: u32,
520    /// Optional filter
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub filter: Option<serde_json::Value>,
523}
524
525impl FullTextSearchRequest {
526    /// Create a new full-text search request
527    pub fn new(query: impl Into<String>, top_k: u32) -> Self {
528        Self {
529            query: query.into(),
530            top_k,
531            filter: None,
532        }
533    }
534
535    /// Add a filter to the search
536    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
537        self.filter = Some(filter);
538        self
539    }
540}
541
542/// Full-text search result
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct FullTextMatch {
545    /// Document ID
546    pub id: String,
547    /// BM25 score
548    pub score: f32,
549    /// Document text
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub text: Option<String>,
552    /// Optional metadata
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub metadata: Option<HashMap<String, serde_json::Value>>,
555}
556
557/// Full-text search response
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct FullTextSearchResponse {
560    /// Matched documents
561    pub matches: Vec<FullTextMatch>,
562}
563
564/// Full-text index statistics
565#[derive(Debug, Clone, Serialize, Deserialize)]
566pub struct FullTextStats {
567    /// Number of documents indexed
568    pub document_count: u64,
569    /// Number of unique terms
570    pub term_count: u64,
571}
572
573// ============================================================================
574// Hybrid Search Types
575// ============================================================================
576
577/// Hybrid search request combining vector and full-text search
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct HybridSearchRequest {
580    /// Query vector
581    pub vector: Vec<f32>,
582    /// Text query
583    pub text: String,
584    /// Number of results to return
585    pub top_k: u32,
586    /// Weight for vector search (0.0-1.0)
587    #[serde(default = "default_vector_weight")]
588    pub vector_weight: f32,
589    /// Optional filter
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub filter: Option<serde_json::Value>,
592}
593
594fn default_vector_weight() -> f32 {
595    0.5
596}
597
598impl HybridSearchRequest {
599    /// Create a new hybrid search request
600    pub fn new(vector: Vec<f32>, text: impl Into<String>, top_k: u32) -> Self {
601        Self {
602            vector,
603            text: text.into(),
604            top_k,
605            vector_weight: 0.5,
606            filter: None,
607        }
608    }
609
610    /// Set the vector weight (text weight is 1.0 - vector_weight)
611    pub fn with_vector_weight(mut self, weight: f32) -> Self {
612        self.vector_weight = weight.clamp(0.0, 1.0);
613        self
614    }
615
616    /// Add a filter to the search
617    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
618        self.filter = Some(filter);
619        self
620    }
621}
622
623/// Hybrid search response
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct HybridSearchResponse {
626    /// Matched results
627    pub matches: Vec<Match>,
628}
629
630// ============================================================================
631// Operations Types
632// ============================================================================
633
634/// System diagnostics
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct SystemDiagnostics {
637    /// System information
638    pub system: SystemInfo,
639    /// Resource usage
640    pub resources: ResourceUsage,
641    /// Component health
642    pub components: ComponentHealth,
643    /// Number of active jobs
644    pub active_jobs: u64,
645}
646
647/// System information
648#[derive(Debug, Clone, Serialize, Deserialize)]
649pub struct SystemInfo {
650    /// Dakera version
651    pub version: String,
652    /// Rust version
653    pub rust_version: String,
654    /// Uptime in seconds
655    pub uptime_seconds: u64,
656    /// Process ID
657    pub pid: u32,
658}
659
660/// Resource usage metrics
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct ResourceUsage {
663    /// Memory usage in bytes
664    pub memory_bytes: u64,
665    /// Thread count
666    pub thread_count: u64,
667    /// Open file descriptors
668    pub open_fds: u64,
669    /// CPU usage percentage
670    pub cpu_percent: Option<f64>,
671}
672
673/// Component health status
674#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct ComponentHealth {
676    /// Storage health
677    pub storage: HealthStatus,
678    /// Search engine health
679    pub search_engine: HealthStatus,
680    /// Cache health
681    pub cache: HealthStatus,
682    /// gRPC health
683    pub grpc: HealthStatus,
684}
685
686/// Health status for a component
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct HealthStatus {
689    /// Is the component healthy
690    pub healthy: bool,
691    /// Status message
692    pub message: String,
693    /// Last check timestamp
694    pub last_check: u64,
695}
696
697/// Background job information
698#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct JobInfo {
700    /// Job ID
701    pub id: String,
702    /// Job type
703    pub job_type: String,
704    /// Current status
705    pub status: String,
706    /// Creation timestamp
707    pub created_at: u64,
708    /// Start timestamp
709    pub started_at: Option<u64>,
710    /// Completion timestamp
711    pub completed_at: Option<u64>,
712    /// Progress percentage
713    pub progress: u8,
714    /// Status message
715    pub message: Option<String>,
716}
717
718/// Compaction request
719#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct CompactionRequest {
721    /// Namespace to compact (None = all)
722    #[serde(skip_serializing_if = "Option::is_none")]
723    pub namespace: Option<String>,
724    /// Force compaction
725    #[serde(default)]
726    pub force: bool,
727}
728
729/// Compaction response
730#[derive(Debug, Clone, Serialize, Deserialize)]
731pub struct CompactionResponse {
732    /// Job ID for tracking
733    pub job_id: String,
734    /// Status message
735    pub message: String,
736}
737
738// ============================================================================
739// Cache Warming Types (Turbopuffer-inspired)
740// ============================================================================
741
742/// Priority level for cache warming operations
743#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
744#[serde(rename_all = "snake_case")]
745pub enum WarmingPriority {
746    /// Highest priority - warm immediately, preempt other operations
747    Critical,
748    /// High priority - warm soon
749    High,
750    /// Normal priority (default)
751    #[default]
752    Normal,
753    /// Low priority - warm when resources available
754    Low,
755    /// Background priority - warm during idle time only
756    Background,
757}
758
759/// Target cache tier for warming
760#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
761#[serde(rename_all = "snake_case")]
762pub enum WarmingTargetTier {
763    /// L1 in-memory cache (Moka) - fastest, limited size
764    L1,
765    /// L2 local disk cache (RocksDB) - larger, persistent
766    #[default]
767    L2,
768    /// Both L1 and L2 caches
769    Both,
770}
771
772/// Access pattern hint for cache optimization
773#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
774#[serde(rename_all = "snake_case")]
775pub enum AccessPatternHint {
776    /// Random access pattern
777    #[default]
778    Random,
779    /// Sequential access pattern
780    Sequential,
781    /// Temporal locality (recently accessed items accessed again)
782    Temporal,
783    /// Spatial locality (nearby items accessed together)
784    Spatial,
785}
786
787/// Cache warming request with priority hints
788#[derive(Debug, Clone, Serialize, Deserialize)]
789pub struct WarmCacheRequest {
790    /// Namespace to warm
791    pub namespace: String,
792    /// Specific vector IDs to warm (None = all)
793    #[serde(skip_serializing_if = "Option::is_none")]
794    pub vector_ids: Option<Vec<String>>,
795    /// Warming priority level
796    #[serde(default)]
797    pub priority: WarmingPriority,
798    /// Target cache tier
799    #[serde(default)]
800    pub target_tier: WarmingTargetTier,
801    /// Run warming in background (non-blocking)
802    #[serde(default)]
803    pub background: bool,
804    /// TTL hint in seconds
805    #[serde(skip_serializing_if = "Option::is_none")]
806    pub ttl_hint_seconds: Option<u64>,
807    /// Access pattern hint for optimization
808    #[serde(default)]
809    pub access_pattern: AccessPatternHint,
810    /// Maximum vectors to warm
811    #[serde(skip_serializing_if = "Option::is_none")]
812    pub max_vectors: Option<usize>,
813}
814
815impl WarmCacheRequest {
816    /// Create a new cache warming request for a namespace
817    pub fn new(namespace: impl Into<String>) -> Self {
818        Self {
819            namespace: namespace.into(),
820            vector_ids: None,
821            priority: WarmingPriority::default(),
822            target_tier: WarmingTargetTier::default(),
823            background: false,
824            ttl_hint_seconds: None,
825            access_pattern: AccessPatternHint::default(),
826            max_vectors: None,
827        }
828    }
829
830    /// Warm specific vector IDs
831    pub fn with_vector_ids(mut self, ids: Vec<String>) -> Self {
832        self.vector_ids = Some(ids);
833        self
834    }
835
836    /// Set warming priority
837    pub fn with_priority(mut self, priority: WarmingPriority) -> Self {
838        self.priority = priority;
839        self
840    }
841
842    /// Set target cache tier
843    pub fn with_target_tier(mut self, tier: WarmingTargetTier) -> Self {
844        self.target_tier = tier;
845        self
846    }
847
848    /// Run warming in background
849    pub fn in_background(mut self) -> Self {
850        self.background = true;
851        self
852    }
853
854    /// Set TTL hint
855    pub fn with_ttl(mut self, seconds: u64) -> Self {
856        self.ttl_hint_seconds = Some(seconds);
857        self
858    }
859
860    /// Set access pattern hint
861    pub fn with_access_pattern(mut self, pattern: AccessPatternHint) -> Self {
862        self.access_pattern = pattern;
863        self
864    }
865
866    /// Limit number of vectors to warm
867    pub fn with_max_vectors(mut self, max: usize) -> Self {
868        self.max_vectors = Some(max);
869        self
870    }
871}
872
873/// Cache warming response
874#[derive(Debug, Clone, Serialize, Deserialize)]
875pub struct WarmCacheResponse {
876    /// Operation success
877    pub success: bool,
878    /// Number of entries warmed
879    pub entries_warmed: u64,
880    /// Number of entries already warm (skipped)
881    pub entries_skipped: u64,
882    /// Job ID for tracking background operations
883    #[serde(skip_serializing_if = "Option::is_none")]
884    pub job_id: Option<String>,
885    /// Status message
886    pub message: String,
887    /// Estimated completion time for background jobs (ISO 8601)
888    #[serde(skip_serializing_if = "Option::is_none")]
889    pub estimated_completion: Option<String>,
890    /// Target tier that was warmed
891    pub target_tier: WarmingTargetTier,
892    /// Priority that was used
893    pub priority: WarmingPriority,
894    /// Bytes warmed (approximate)
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub bytes_warmed: Option<u64>,
897}
898
899// ============================================================================
900// Export Types (Turbopuffer-inspired)
901// ============================================================================
902
903/// Request to export vectors from a namespace with pagination
904#[derive(Debug, Clone, Serialize, Deserialize)]
905pub struct ExportRequest {
906    /// Maximum number of vectors to return per page (default: 1000, max: 10000)
907    #[serde(default = "default_export_top_k")]
908    pub top_k: usize,
909    /// Cursor for pagination - the last vector ID from previous page
910    #[serde(skip_serializing_if = "Option::is_none")]
911    pub cursor: Option<String>,
912    /// Whether to include vector values in the response (default: true)
913    #[serde(default = "default_true")]
914    pub include_vectors: bool,
915    /// Whether to include metadata in the response (default: true)
916    #[serde(default = "default_true")]
917    pub include_metadata: bool,
918}
919
920fn default_export_top_k() -> usize {
921    1000
922}
923
924impl Default for ExportRequest {
925    fn default() -> Self {
926        Self {
927            top_k: 1000,
928            cursor: None,
929            include_vectors: true,
930            include_metadata: true,
931        }
932    }
933}
934
935impl ExportRequest {
936    /// Create a new export request with default settings
937    pub fn new() -> Self {
938        Self::default()
939    }
940
941    /// Set the maximum number of vectors to return per page
942    pub fn with_top_k(mut self, top_k: usize) -> Self {
943        self.top_k = top_k;
944        self
945    }
946
947    /// Set the pagination cursor
948    pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
949        self.cursor = Some(cursor.into());
950        self
951    }
952
953    /// Set whether to include vector values
954    pub fn include_vectors(mut self, include: bool) -> Self {
955        self.include_vectors = include;
956        self
957    }
958
959    /// Set whether to include metadata
960    pub fn include_metadata(mut self, include: bool) -> Self {
961        self.include_metadata = include;
962        self
963    }
964}
965
966/// A single exported vector record
967#[derive(Debug, Clone, Serialize, Deserialize)]
968pub struct ExportedVector {
969    /// Vector ID
970    pub id: String,
971    /// Vector values (optional based on include_vectors)
972    #[serde(skip_serializing_if = "Option::is_none")]
973    pub values: Option<Vec<f32>>,
974    /// Metadata (optional based on include_metadata)
975    #[serde(skip_serializing_if = "Option::is_none")]
976    pub metadata: Option<serde_json::Value>,
977    /// TTL in seconds if set
978    #[serde(skip_serializing_if = "Option::is_none")]
979    pub ttl_seconds: Option<u64>,
980}
981
982/// Response from export operation
983#[derive(Debug, Clone, Serialize, Deserialize)]
984pub struct ExportResponse {
985    /// Exported vectors for this page
986    pub vectors: Vec<ExportedVector>,
987    /// Cursor for next page (None if this is the last page)
988    #[serde(skip_serializing_if = "Option::is_none")]
989    pub next_cursor: Option<String>,
990    /// Total vectors in namespace (for progress tracking)
991    pub total_count: usize,
992    /// Number of vectors returned in this page
993    pub returned_count: usize,
994}
995
996// ============================================================================
997// Batch Query Types
998// ============================================================================
999
1000/// A single query within a batch request
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1002pub struct BatchQueryItem {
1003    /// Unique identifier for this query within the batch
1004    #[serde(skip_serializing_if = "Option::is_none")]
1005    pub id: Option<String>,
1006    /// The query vector
1007    pub vector: Vec<f32>,
1008    /// Number of results to return
1009    #[serde(default = "default_batch_top_k")]
1010    pub top_k: u32,
1011    /// Optional filter expression
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub filter: Option<serde_json::Value>,
1014    /// Whether to include metadata in results
1015    #[serde(default)]
1016    pub include_metadata: bool,
1017    /// Read consistency level
1018    #[serde(default)]
1019    pub consistency: ReadConsistency,
1020    /// Staleness configuration for bounded staleness reads
1021    #[serde(skip_serializing_if = "Option::is_none")]
1022    pub staleness_config: Option<StalenessConfig>,
1023}
1024
1025fn default_batch_top_k() -> u32 {
1026    10
1027}
1028
1029impl BatchQueryItem {
1030    /// Create a new batch query item
1031    pub fn new(vector: Vec<f32>, top_k: u32) -> Self {
1032        Self {
1033            id: None,
1034            vector,
1035            top_k,
1036            filter: None,
1037            include_metadata: true,
1038            consistency: ReadConsistency::default(),
1039            staleness_config: None,
1040        }
1041    }
1042
1043    /// Set a unique identifier for this query
1044    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1045        self.id = Some(id.into());
1046        self
1047    }
1048
1049    /// Add a filter to the query
1050    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1051        self.filter = Some(filter);
1052        self
1053    }
1054
1055    /// Set whether to include metadata
1056    pub fn include_metadata(mut self, include: bool) -> Self {
1057        self.include_metadata = include;
1058        self
1059    }
1060
1061    /// Set read consistency level
1062    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
1063        self.consistency = consistency;
1064        self
1065    }
1066
1067    /// Set bounded staleness with max staleness in ms
1068    pub fn with_bounded_staleness(mut self, max_staleness_ms: u64) -> Self {
1069        self.consistency = ReadConsistency::BoundedStaleness;
1070        self.staleness_config = Some(StalenessConfig::new(max_staleness_ms));
1071        self
1072    }
1073}
1074
1075/// Batch query request - execute multiple queries in parallel
1076#[derive(Debug, Clone, Serialize, Deserialize)]
1077pub struct BatchQueryRequest {
1078    /// List of queries to execute
1079    pub queries: Vec<BatchQueryItem>,
1080}
1081
1082impl BatchQueryRequest {
1083    /// Create a new batch query request
1084    pub fn new(queries: Vec<BatchQueryItem>) -> Self {
1085        Self { queries }
1086    }
1087
1088    /// Create a batch query request from a single query
1089    pub fn single(query: BatchQueryItem) -> Self {
1090        Self {
1091            queries: vec![query],
1092        }
1093    }
1094}
1095
1096/// Results for a single query within a batch
1097#[derive(Debug, Clone, Serialize, Deserialize)]
1098pub struct BatchQueryResult {
1099    /// The query identifier (if provided in request)
1100    #[serde(skip_serializing_if = "Option::is_none")]
1101    pub id: Option<String>,
1102    /// Query results (empty if an error occurred)
1103    pub results: Vec<Match>,
1104    /// Query execution time in milliseconds
1105    pub latency_ms: f64,
1106    /// Error message if this individual query failed
1107    #[serde(skip_serializing_if = "Option::is_none")]
1108    pub error: Option<String>,
1109}
1110
1111/// Batch query response
1112#[derive(Debug, Clone, Serialize, Deserialize)]
1113pub struct BatchQueryResponse {
1114    /// Results for each query in the batch
1115    pub results: Vec<BatchQueryResult>,
1116    /// Total execution time in milliseconds
1117    pub total_latency_ms: f64,
1118    /// Number of queries executed
1119    pub query_count: usize,
1120}
1121
1122// ============================================================================
1123// Multi-Vector Search Types
1124// ============================================================================
1125
1126/// Request for multi-vector search with positive and negative vectors
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1128pub struct MultiVectorSearchRequest {
1129    /// Positive vectors to search towards (required, at least one)
1130    pub positive_vectors: Vec<Vec<f32>>,
1131    /// Weights for positive vectors (optional, defaults to equal weights)
1132    #[serde(skip_serializing_if = "Option::is_none")]
1133    pub positive_weights: Option<Vec<f32>>,
1134    /// Negative vectors to search away from (optional)
1135    #[serde(skip_serializing_if = "Option::is_none")]
1136    pub negative_vectors: Option<Vec<Vec<f32>>>,
1137    /// Weights for negative vectors (optional, defaults to equal weights)
1138    #[serde(skip_serializing_if = "Option::is_none")]
1139    pub negative_weights: Option<Vec<f32>>,
1140    /// Number of results to return
1141    #[serde(default = "default_multi_vector_top_k")]
1142    pub top_k: u32,
1143    /// Distance metric to use
1144    #[serde(default)]
1145    pub distance_metric: DistanceMetric,
1146    /// Minimum score threshold
1147    #[serde(skip_serializing_if = "Option::is_none")]
1148    pub score_threshold: Option<f32>,
1149    /// Enable MMR (Maximal Marginal Relevance) for diversity
1150    #[serde(default)]
1151    pub enable_mmr: bool,
1152    /// Lambda parameter for MMR (0 = max diversity, 1 = max relevance)
1153    #[serde(default = "default_mmr_lambda")]
1154    pub mmr_lambda: f32,
1155    /// Include metadata in results
1156    #[serde(default = "default_true")]
1157    pub include_metadata: bool,
1158    /// Include vectors in results
1159    #[serde(default)]
1160    pub include_vectors: bool,
1161    /// Optional metadata filter
1162    #[serde(skip_serializing_if = "Option::is_none")]
1163    pub filter: Option<serde_json::Value>,
1164    /// Read consistency level
1165    #[serde(default)]
1166    pub consistency: ReadConsistency,
1167    /// Staleness configuration for bounded staleness reads
1168    #[serde(skip_serializing_if = "Option::is_none")]
1169    pub staleness_config: Option<StalenessConfig>,
1170}
1171
1172fn default_multi_vector_top_k() -> u32 {
1173    10
1174}
1175
1176fn default_mmr_lambda() -> f32 {
1177    0.5
1178}
1179
1180impl MultiVectorSearchRequest {
1181    /// Create a new multi-vector search request with positive vectors
1182    pub fn new(positive_vectors: Vec<Vec<f32>>) -> Self {
1183        Self {
1184            positive_vectors,
1185            positive_weights: None,
1186            negative_vectors: None,
1187            negative_weights: None,
1188            top_k: 10,
1189            distance_metric: DistanceMetric::default(),
1190            score_threshold: None,
1191            enable_mmr: false,
1192            mmr_lambda: 0.5,
1193            include_metadata: true,
1194            include_vectors: false,
1195            filter: None,
1196            consistency: ReadConsistency::default(),
1197            staleness_config: None,
1198        }
1199    }
1200
1201    /// Set the number of results to return
1202    pub fn with_top_k(mut self, top_k: u32) -> Self {
1203        self.top_k = top_k;
1204        self
1205    }
1206
1207    /// Add weights for positive vectors
1208    pub fn with_positive_weights(mut self, weights: Vec<f32>) -> Self {
1209        self.positive_weights = Some(weights);
1210        self
1211    }
1212
1213    /// Add negative vectors to search away from
1214    pub fn with_negative_vectors(mut self, vectors: Vec<Vec<f32>>) -> Self {
1215        self.negative_vectors = Some(vectors);
1216        self
1217    }
1218
1219    /// Add weights for negative vectors
1220    pub fn with_negative_weights(mut self, weights: Vec<f32>) -> Self {
1221        self.negative_weights = Some(weights);
1222        self
1223    }
1224
1225    /// Set distance metric
1226    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
1227        self.distance_metric = metric;
1228        self
1229    }
1230
1231    /// Set minimum score threshold
1232    pub fn with_score_threshold(mut self, threshold: f32) -> Self {
1233        self.score_threshold = Some(threshold);
1234        self
1235    }
1236
1237    /// Enable MMR for diversity
1238    pub fn with_mmr(mut self, lambda: f32) -> Self {
1239        self.enable_mmr = true;
1240        self.mmr_lambda = lambda.clamp(0.0, 1.0);
1241        self
1242    }
1243
1244    /// Set whether to include metadata
1245    pub fn include_metadata(mut self, include: bool) -> Self {
1246        self.include_metadata = include;
1247        self
1248    }
1249
1250    /// Set whether to include vectors
1251    pub fn include_vectors(mut self, include: bool) -> Self {
1252        self.include_vectors = include;
1253        self
1254    }
1255
1256    /// Add a filter
1257    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1258        self.filter = Some(filter);
1259        self
1260    }
1261
1262    /// Set read consistency level
1263    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
1264        self.consistency = consistency;
1265        self
1266    }
1267}
1268
1269/// Single result from multi-vector search
1270#[derive(Debug, Clone, Serialize, Deserialize)]
1271pub struct MultiVectorSearchResult {
1272    /// Vector ID
1273    pub id: String,
1274    /// Similarity score
1275    pub score: f32,
1276    /// MMR score (if MMR enabled)
1277    #[serde(skip_serializing_if = "Option::is_none")]
1278    pub mmr_score: Option<f32>,
1279    /// Original rank before reranking
1280    #[serde(skip_serializing_if = "Option::is_none")]
1281    pub original_rank: Option<usize>,
1282    /// Optional metadata
1283    #[serde(skip_serializing_if = "Option::is_none")]
1284    pub metadata: Option<HashMap<String, serde_json::Value>>,
1285    /// Optional vector values
1286    #[serde(skip_serializing_if = "Option::is_none")]
1287    pub vector: Option<Vec<f32>>,
1288}
1289
1290/// Response from multi-vector search
1291#[derive(Debug, Clone, Serialize, Deserialize)]
1292pub struct MultiVectorSearchResponse {
1293    /// Search results
1294    pub results: Vec<MultiVectorSearchResult>,
1295    /// The computed query vector (weighted combination of positive - negative)
1296    #[serde(skip_serializing_if = "Option::is_none")]
1297    pub computed_query_vector: Option<Vec<f32>>,
1298}
1299
1300// ============================================================================
1301// Aggregation Types (Turbopuffer-inspired)
1302// ============================================================================
1303
1304/// Aggregate function for computing values across documents
1305#[derive(Debug, Clone, Serialize, Deserialize)]
1306#[serde(untagged)]
1307pub enum AggregateFunction {
1308    /// Count matching documents
1309    Count,
1310    /// Sum numeric attribute values
1311    Sum { field: String },
1312    /// Average numeric attribute values
1313    Avg { field: String },
1314    /// Minimum numeric attribute value
1315    Min { field: String },
1316    /// Maximum numeric attribute value
1317    Max { field: String },
1318}
1319
1320/// Request for aggregation query (Turbopuffer-inspired)
1321#[derive(Debug, Clone, Serialize, Deserialize)]
1322pub struct AggregationRequest {
1323    /// Named aggregations to compute
1324    /// Example: {"my_count": ["Count"], "total_score": ["Sum", "score"]}
1325    pub aggregate_by: HashMap<String, serde_json::Value>,
1326    /// Fields to group results by (optional)
1327    /// Example: ["category", "status"]
1328    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1329    pub group_by: Vec<String>,
1330    /// Filter to apply before aggregation
1331    #[serde(skip_serializing_if = "Option::is_none")]
1332    pub filter: Option<serde_json::Value>,
1333    /// Maximum number of groups to return (default: 100)
1334    #[serde(default = "default_agg_limit")]
1335    pub limit: usize,
1336}
1337
1338fn default_agg_limit() -> usize {
1339    100
1340}
1341
1342impl AggregationRequest {
1343    /// Create a new aggregation request with a single aggregation
1344    pub fn new() -> Self {
1345        Self {
1346            aggregate_by: HashMap::new(),
1347            group_by: Vec::new(),
1348            filter: None,
1349            limit: 100,
1350        }
1351    }
1352
1353    /// Add a count aggregation
1354    pub fn with_count(mut self, name: impl Into<String>) -> Self {
1355        self.aggregate_by
1356            .insert(name.into(), serde_json::json!(["Count"]));
1357        self
1358    }
1359
1360    /// Add a sum aggregation
1361    pub fn with_sum(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1362        self.aggregate_by
1363            .insert(name.into(), serde_json::json!(["Sum", field.into()]));
1364        self
1365    }
1366
1367    /// Add an average aggregation
1368    pub fn with_avg(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1369        self.aggregate_by
1370            .insert(name.into(), serde_json::json!(["Avg", field.into()]));
1371        self
1372    }
1373
1374    /// Add a min aggregation
1375    pub fn with_min(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1376        self.aggregate_by
1377            .insert(name.into(), serde_json::json!(["Min", field.into()]));
1378        self
1379    }
1380
1381    /// Add a max aggregation
1382    pub fn with_max(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1383        self.aggregate_by
1384            .insert(name.into(), serde_json::json!(["Max", field.into()]));
1385        self
1386    }
1387
1388    /// Set group by fields
1389    pub fn group_by(mut self, fields: Vec<String>) -> Self {
1390        self.group_by = fields;
1391        self
1392    }
1393
1394    /// Add a single group by field
1395    pub fn with_group_by(mut self, field: impl Into<String>) -> Self {
1396        self.group_by.push(field.into());
1397        self
1398    }
1399
1400    /// Set filter for aggregation
1401    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1402        self.filter = Some(filter);
1403        self
1404    }
1405
1406    /// Set maximum number of groups to return
1407    pub fn with_limit(mut self, limit: usize) -> Self {
1408        self.limit = limit;
1409        self
1410    }
1411}
1412
1413impl Default for AggregationRequest {
1414    fn default() -> Self {
1415        Self::new()
1416    }
1417}
1418
1419/// Response for aggregation query
1420#[derive(Debug, Clone, Serialize, Deserialize)]
1421pub struct AggregationResponse {
1422    /// Aggregation results (without grouping)
1423    #[serde(skip_serializing_if = "Option::is_none")]
1424    pub aggregations: Option<HashMap<String, serde_json::Value>>,
1425    /// Grouped aggregation results (with group_by)
1426    #[serde(skip_serializing_if = "Option::is_none")]
1427    pub aggregation_groups: Option<Vec<AggregationGroup>>,
1428}
1429
1430/// Single group in aggregation results
1431#[derive(Debug, Clone, Serialize, Deserialize)]
1432pub struct AggregationGroup {
1433    /// Group key values (flattened into object)
1434    #[serde(flatten)]
1435    pub group_key: HashMap<String, serde_json::Value>,
1436    /// Aggregation results for this group
1437    #[serde(flatten)]
1438    pub aggregations: HashMap<String, serde_json::Value>,
1439}
1440
1441// ============================================================================
1442// Unified Query Types (Turbopuffer-inspired)
1443// ============================================================================
1444
1445/// Vector search method for unified query
1446#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1447pub enum VectorSearchMethod {
1448    /// Approximate Nearest Neighbor (fast, default)
1449    #[default]
1450    ANN,
1451    /// Exact k-Nearest Neighbor (exhaustive, requires filters)
1452    #[serde(rename = "kNN")]
1453    KNN,
1454}
1455
1456/// Sort direction for attribute ordering
1457#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1458#[serde(rename_all = "lowercase")]
1459pub enum SortDirection {
1460    /// Ascending order
1461    Asc,
1462    /// Descending order
1463    #[default]
1464    Desc,
1465}
1466
1467/// Ranking function for unified query API
1468/// Supports vector search (ANN/kNN), full-text BM25, and attribute ordering
1469#[derive(Debug, Clone, Serialize, Deserialize)]
1470#[serde(untagged)]
1471pub enum RankBy {
1472    /// Vector search: uses field, method, and query_vector
1473    VectorSearch {
1474        field: String,
1475        method: VectorSearchMethod,
1476        query_vector: Vec<f32>,
1477    },
1478    /// Full-text BM25 search
1479    FullTextSearch {
1480        field: String,
1481        method: String, // Always "BM25"
1482        query: String,
1483    },
1484    /// Attribute ordering
1485    AttributeOrder {
1486        field: String,
1487        direction: SortDirection,
1488    },
1489    /// Sum of multiple ranking functions
1490    Sum(Vec<RankBy>),
1491    /// Max of multiple ranking functions
1492    Max(Vec<RankBy>),
1493    /// Product with weight
1494    Product { weight: f32, ranking: Box<RankBy> },
1495}
1496
1497impl RankBy {
1498    /// Create a vector search ranking using ANN
1499    pub fn vector_ann(field: impl Into<String>, query_vector: Vec<f32>) -> Self {
1500        RankBy::VectorSearch {
1501            field: field.into(),
1502            method: VectorSearchMethod::ANN,
1503            query_vector,
1504        }
1505    }
1506
1507    /// Create a vector search ranking using ANN on the default "vector" field
1508    pub fn ann(query_vector: Vec<f32>) -> Self {
1509        Self::vector_ann("vector", query_vector)
1510    }
1511
1512    /// Create a vector search ranking using exact kNN
1513    pub fn vector_knn(field: impl Into<String>, query_vector: Vec<f32>) -> Self {
1514        RankBy::VectorSearch {
1515            field: field.into(),
1516            method: VectorSearchMethod::KNN,
1517            query_vector,
1518        }
1519    }
1520
1521    /// Create a vector search ranking using kNN on the default "vector" field
1522    pub fn knn(query_vector: Vec<f32>) -> Self {
1523        Self::vector_knn("vector", query_vector)
1524    }
1525
1526    /// Create a BM25 full-text search ranking
1527    pub fn bm25(field: impl Into<String>, query: impl Into<String>) -> Self {
1528        RankBy::FullTextSearch {
1529            field: field.into(),
1530            method: "BM25".to_string(),
1531            query: query.into(),
1532        }
1533    }
1534
1535    /// Create an attribute ordering ranking (ascending)
1536    pub fn asc(field: impl Into<String>) -> Self {
1537        RankBy::AttributeOrder {
1538            field: field.into(),
1539            direction: SortDirection::Asc,
1540        }
1541    }
1542
1543    /// Create an attribute ordering ranking (descending)
1544    pub fn desc(field: impl Into<String>) -> Self {
1545        RankBy::AttributeOrder {
1546            field: field.into(),
1547            direction: SortDirection::Desc,
1548        }
1549    }
1550
1551    /// Sum multiple ranking functions together
1552    pub fn sum(rankings: Vec<RankBy>) -> Self {
1553        RankBy::Sum(rankings)
1554    }
1555
1556    /// Take the max of multiple ranking functions
1557    pub fn max(rankings: Vec<RankBy>) -> Self {
1558        RankBy::Max(rankings)
1559    }
1560
1561    /// Apply a weight multiplier to a ranking function
1562    pub fn product(weight: f32, ranking: RankBy) -> Self {
1563        RankBy::Product {
1564            weight,
1565            ranking: Box::new(ranking),
1566        }
1567    }
1568}
1569
1570/// Unified query request with flexible ranking options (Turbopuffer-inspired)
1571///
1572/// # Example
1573///
1574/// ```rust
1575/// use dakera_client::UnifiedQueryRequest;
1576///
1577/// // Vector ANN search
1578/// let request = UnifiedQueryRequest::vector_search(vec![0.1, 0.2, 0.3], 10);
1579///
1580/// // Full-text BM25 search
1581/// let request = UnifiedQueryRequest::fulltext_search("content", "hello world", 10);
1582///
1583/// // Custom rank_by with filters
1584/// let request = UnifiedQueryRequest::vector_search(vec![0.1, 0.2, 0.3], 10)
1585///     .with_filter(serde_json::json!({"category": {"$eq": "science"}}));
1586/// ```
1587#[derive(Debug, Clone, Serialize, Deserialize)]
1588pub struct UnifiedQueryRequest {
1589    /// How to rank documents (required)
1590    pub rank_by: serde_json::Value,
1591    /// Number of results to return
1592    #[serde(default = "default_unified_top_k")]
1593    pub top_k: usize,
1594    /// Optional metadata filter
1595    #[serde(skip_serializing_if = "Option::is_none")]
1596    pub filter: Option<serde_json::Value>,
1597    /// Include metadata in results
1598    #[serde(default = "default_true")]
1599    pub include_metadata: bool,
1600    /// Include vectors in results
1601    #[serde(default)]
1602    pub include_vectors: bool,
1603    /// Distance metric for vector search (default: cosine)
1604    #[serde(default)]
1605    pub distance_metric: DistanceMetric,
1606}
1607
1608fn default_unified_top_k() -> usize {
1609    10
1610}
1611
1612impl UnifiedQueryRequest {
1613    /// Create a new unified query request with vector ANN search
1614    pub fn vector_search(query_vector: Vec<f32>, top_k: usize) -> Self {
1615        Self {
1616            rank_by: serde_json::json!(["ANN", query_vector]),
1617            top_k,
1618            filter: None,
1619            include_metadata: true,
1620            include_vectors: false,
1621            distance_metric: DistanceMetric::default(),
1622        }
1623    }
1624
1625    /// Create a new unified query request with vector kNN search
1626    pub fn vector_knn_search(query_vector: Vec<f32>, top_k: usize) -> Self {
1627        Self {
1628            rank_by: serde_json::json!(["kNN", query_vector]),
1629            top_k,
1630            filter: None,
1631            include_metadata: true,
1632            include_vectors: false,
1633            distance_metric: DistanceMetric::default(),
1634        }
1635    }
1636
1637    /// Create a new unified query request with full-text BM25 search
1638    pub fn fulltext_search(
1639        field: impl Into<String>,
1640        query: impl Into<String>,
1641        top_k: usize,
1642    ) -> Self {
1643        Self {
1644            rank_by: serde_json::json!([field.into(), "BM25", query.into()]),
1645            top_k,
1646            filter: None,
1647            include_metadata: true,
1648            include_vectors: false,
1649            distance_metric: DistanceMetric::default(),
1650        }
1651    }
1652
1653    /// Create a new unified query request with attribute ordering
1654    pub fn attribute_order(
1655        field: impl Into<String>,
1656        direction: SortDirection,
1657        top_k: usize,
1658    ) -> Self {
1659        let dir = match direction {
1660            SortDirection::Asc => "asc",
1661            SortDirection::Desc => "desc",
1662        };
1663        Self {
1664            rank_by: serde_json::json!([field.into(), dir]),
1665            top_k,
1666            filter: None,
1667            include_metadata: true,
1668            include_vectors: false,
1669            distance_metric: DistanceMetric::default(),
1670        }
1671    }
1672
1673    /// Create a unified query with a raw rank_by JSON value
1674    pub fn with_rank_by(rank_by: serde_json::Value, top_k: usize) -> Self {
1675        Self {
1676            rank_by,
1677            top_k,
1678            filter: None,
1679            include_metadata: true,
1680            include_vectors: false,
1681            distance_metric: DistanceMetric::default(),
1682        }
1683    }
1684
1685    /// Add a filter to the query
1686    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1687        self.filter = Some(filter);
1688        self
1689    }
1690
1691    /// Set whether to include metadata
1692    pub fn include_metadata(mut self, include: bool) -> Self {
1693        self.include_metadata = include;
1694        self
1695    }
1696
1697    /// Set whether to include vector values
1698    pub fn include_vectors(mut self, include: bool) -> Self {
1699        self.include_vectors = include;
1700        self
1701    }
1702
1703    /// Set the distance metric
1704    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
1705        self.distance_metric = metric;
1706        self
1707    }
1708
1709    /// Set the number of results to return
1710    pub fn with_top_k(mut self, top_k: usize) -> Self {
1711        self.top_k = top_k;
1712        self
1713    }
1714}
1715
1716/// Single result from unified query
1717#[derive(Debug, Clone, Serialize, Deserialize)]
1718pub struct UnifiedSearchResult {
1719    /// Vector/document ID
1720    pub id: String,
1721    /// Ranking score (distance for vector search, BM25 score for text)
1722    /// Named $dist for Turbopuffer compatibility
1723    #[serde(rename = "$dist", skip_serializing_if = "Option::is_none")]
1724    pub dist: Option<f32>,
1725    /// Metadata if requested
1726    #[serde(skip_serializing_if = "Option::is_none")]
1727    pub metadata: Option<serde_json::Value>,
1728    /// Vector values if requested
1729    #[serde(skip_serializing_if = "Option::is_none")]
1730    pub vector: Option<Vec<f32>>,
1731}
1732
1733/// Unified query response
1734#[derive(Debug, Clone, Serialize, Deserialize)]
1735pub struct UnifiedQueryResponse {
1736    /// Search results ordered by rank_by score
1737    pub results: Vec<UnifiedSearchResult>,
1738    /// Cursor for pagination (if more results available)
1739    #[serde(skip_serializing_if = "Option::is_none")]
1740    pub next_cursor: Option<String>,
1741}
1742
1743// ============================================================================
1744// Query Explain Types
1745// ============================================================================
1746
1747fn default_explain_top_k() -> usize {
1748    10
1749}
1750
1751/// Query type for explain
1752#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1753#[serde(rename_all = "snake_case")]
1754#[derive(Default)]
1755pub enum ExplainQueryType {
1756    /// Vector similarity search
1757    #[default]
1758    VectorSearch,
1759    /// Full-text search
1760    FullTextSearch,
1761    /// Hybrid search combining vector and text
1762    HybridSearch,
1763    /// Multi-vector search with positive/negative vectors
1764    MultiVector,
1765    /// Batch query execution
1766    BatchQuery,
1767}
1768
1769/// Query explain request
1770#[derive(Debug, Clone, Serialize, Deserialize)]
1771pub struct QueryExplainRequest {
1772    /// Type of query to explain
1773    #[serde(default)]
1774    pub query_type: ExplainQueryType,
1775    /// Query vector (for vector searches)
1776    #[serde(skip_serializing_if = "Option::is_none")]
1777    pub vector: Option<Vec<f32>>,
1778    /// Number of results to return
1779    #[serde(default = "default_explain_top_k")]
1780    pub top_k: usize,
1781    /// Optional metadata filter
1782    #[serde(skip_serializing_if = "Option::is_none")]
1783    pub filter: Option<serde_json::Value>,
1784    /// Optional text query for hybrid/fulltext search
1785    #[serde(skip_serializing_if = "Option::is_none")]
1786    pub text_query: Option<String>,
1787    /// Distance metric
1788    #[serde(default = "default_distance_metric")]
1789    pub distance_metric: String,
1790    /// Whether to actually execute the query for actual stats
1791    #[serde(default)]
1792    pub execute: bool,
1793    /// Include verbose output
1794    #[serde(default)]
1795    pub verbose: bool,
1796}
1797
1798fn default_distance_metric() -> String {
1799    "cosine".to_string()
1800}
1801
1802impl QueryExplainRequest {
1803    /// Create a new explain request for a vector search
1804    pub fn vector_search(vector: Vec<f32>, top_k: usize) -> Self {
1805        Self {
1806            query_type: ExplainQueryType::VectorSearch,
1807            vector: Some(vector),
1808            top_k,
1809            filter: None,
1810            text_query: None,
1811            distance_metric: "cosine".to_string(),
1812            execute: false,
1813            verbose: false,
1814        }
1815    }
1816
1817    /// Create a new explain request for a full-text search
1818    pub fn fulltext_search(text_query: impl Into<String>, top_k: usize) -> Self {
1819        Self {
1820            query_type: ExplainQueryType::FullTextSearch,
1821            vector: None,
1822            top_k,
1823            filter: None,
1824            text_query: Some(text_query.into()),
1825            distance_metric: "bm25".to_string(),
1826            execute: false,
1827            verbose: false,
1828        }
1829    }
1830
1831    /// Create a new explain request for a hybrid search
1832    pub fn hybrid_search(vector: Vec<f32>, text_query: impl Into<String>, top_k: usize) -> Self {
1833        Self {
1834            query_type: ExplainQueryType::HybridSearch,
1835            vector: Some(vector),
1836            top_k,
1837            filter: None,
1838            text_query: Some(text_query.into()),
1839            distance_metric: "hybrid".to_string(),
1840            execute: false,
1841            verbose: false,
1842        }
1843    }
1844
1845    /// Add a filter to the explain request
1846    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1847        self.filter = Some(filter);
1848        self
1849    }
1850
1851    /// Set the distance metric
1852    pub fn with_distance_metric(mut self, metric: impl Into<String>) -> Self {
1853        self.distance_metric = metric.into();
1854        self
1855    }
1856
1857    /// Execute the query to get actual stats
1858    pub fn with_execution(mut self) -> Self {
1859        self.execute = true;
1860        self
1861    }
1862
1863    /// Enable verbose output
1864    pub fn with_verbose(mut self) -> Self {
1865        self.verbose = true;
1866        self
1867    }
1868}
1869
1870/// A stage in query execution
1871#[derive(Debug, Clone, Serialize, Deserialize)]
1872pub struct ExecutionStage {
1873    /// Stage name
1874    pub name: String,
1875    /// Stage description
1876    pub description: String,
1877    /// Stage order (1-based)
1878    pub order: u32,
1879    /// Estimated input rows
1880    pub estimated_input: u64,
1881    /// Estimated output rows
1882    pub estimated_output: u64,
1883    /// Estimated cost for this stage
1884    pub estimated_cost: f64,
1885    /// Stage-specific details
1886    #[serde(default)]
1887    pub details: HashMap<String, serde_json::Value>,
1888}
1889
1890/// Cost estimation
1891#[derive(Debug, Clone, Serialize, Deserialize)]
1892pub struct CostEstimate {
1893    /// Total estimated cost (abstract units)
1894    pub total_cost: f64,
1895    /// Estimated execution time in milliseconds
1896    pub estimated_time_ms: u64,
1897    /// Estimated memory usage in bytes
1898    pub estimated_memory_bytes: u64,
1899    /// Estimated I/O operations
1900    pub estimated_io_ops: u64,
1901    /// Cost breakdown by component
1902    #[serde(default)]
1903    pub cost_breakdown: HashMap<String, f64>,
1904    /// Confidence level (0.0-1.0)
1905    pub confidence: f64,
1906}
1907
1908/// Actual execution statistics (when execute=true)
1909#[derive(Debug, Clone, Serialize, Deserialize)]
1910pub struct ActualStats {
1911    /// Actual execution time in milliseconds
1912    pub execution_time_ms: u64,
1913    /// Actual results returned
1914    pub results_returned: usize,
1915    /// Vectors scanned
1916    pub vectors_scanned: u64,
1917    /// Vectors after filter
1918    pub vectors_after_filter: u64,
1919    /// Index lookups performed
1920    pub index_lookups: u64,
1921    /// Cache hits
1922    pub cache_hits: u64,
1923    /// Cache misses
1924    pub cache_misses: u64,
1925    /// Memory used in bytes
1926    pub memory_used_bytes: u64,
1927}
1928
1929/// Performance recommendation
1930#[derive(Debug, Clone, Serialize, Deserialize)]
1931pub struct Recommendation {
1932    /// Recommendation type
1933    pub recommendation_type: String,
1934    /// Priority (high, medium, low)
1935    pub priority: String,
1936    /// Recommendation description
1937    pub description: String,
1938    /// Expected improvement
1939    pub expected_improvement: String,
1940    /// How to implement
1941    pub implementation: String,
1942}
1943
1944/// Index selection details
1945#[derive(Debug, Clone, Serialize, Deserialize)]
1946pub struct IndexSelection {
1947    /// Index type that will be used
1948    pub index_type: String,
1949    /// Why this index was selected
1950    pub selection_reason: String,
1951    /// Alternative indexes considered
1952    #[serde(default)]
1953    pub alternatives_considered: Vec<IndexAlternative>,
1954    /// Index configuration
1955    #[serde(default)]
1956    pub index_config: HashMap<String, serde_json::Value>,
1957    /// Index statistics
1958    pub index_stats: IndexStatistics,
1959}
1960
1961/// Alternative index that was considered
1962#[derive(Debug, Clone, Serialize, Deserialize)]
1963pub struct IndexAlternative {
1964    /// Index type
1965    pub index_type: String,
1966    /// Why it wasn't selected
1967    pub rejection_reason: String,
1968    /// Estimated cost if this index was used
1969    pub estimated_cost: f64,
1970}
1971
1972/// Index statistics
1973#[derive(Debug, Clone, Serialize, Deserialize)]
1974pub struct IndexStatistics {
1975    /// Total vectors in index
1976    pub vector_count: u64,
1977    /// Vector dimension
1978    pub dimension: usize,
1979    /// Index memory usage (estimated)
1980    pub memory_bytes: u64,
1981    /// Index build time (if available)
1982    #[serde(skip_serializing_if = "Option::is_none")]
1983    pub build_time_ms: Option<u64>,
1984    /// Last updated timestamp
1985    #[serde(skip_serializing_if = "Option::is_none")]
1986    pub last_updated: Option<u64>,
1987}
1988
1989/// Query parameters for reference
1990#[derive(Debug, Clone, Serialize, Deserialize)]
1991pub struct QueryParams {
1992    /// Number of results requested
1993    pub top_k: usize,
1994    /// Whether a filter was applied
1995    pub has_filter: bool,
1996    /// Filter complexity level
1997    pub filter_complexity: String,
1998    /// Vector dimension (if applicable)
1999    #[serde(skip_serializing_if = "Option::is_none")]
2000    pub vector_dimension: Option<usize>,
2001    /// Distance metric used
2002    pub distance_metric: String,
2003    /// Text query length (if applicable)
2004    #[serde(skip_serializing_if = "Option::is_none")]
2005    pub text_query_length: Option<usize>,
2006}
2007
2008/// Query explain response - detailed execution plan
2009#[derive(Debug, Clone, Serialize, Deserialize)]
2010pub struct QueryExplainResponse {
2011    /// Query type being explained
2012    pub query_type: ExplainQueryType,
2013    /// Namespace being queried
2014    pub namespace: String,
2015    /// Index selection information
2016    pub index_selection: IndexSelection,
2017    /// Query execution stages
2018    pub stages: Vec<ExecutionStage>,
2019    /// Cost estimates
2020    pub cost_estimate: CostEstimate,
2021    /// Actual execution stats (if execute=true)
2022    #[serde(skip_serializing_if = "Option::is_none")]
2023    pub actual_stats: Option<ActualStats>,
2024    /// Performance recommendations
2025    #[serde(default)]
2026    pub recommendations: Vec<Recommendation>,
2027    /// Query plan summary
2028    pub summary: String,
2029    /// Raw query parameters
2030    pub query_params: QueryParams,
2031}
2032
2033// ============================================================================
2034// Text Auto-Embedding Types
2035// ============================================================================
2036
2037/// Supported embedding models for text-based operations.
2038#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
2039#[serde(rename_all = "kebab-case")]
2040pub enum EmbeddingModel {
2041    /// MiniLM-L6 — Fast, good quality (384 dimensions)
2042    #[default]
2043    Minilm,
2044    /// BGE-small — Balanced performance (384 dimensions)
2045    BgeSmall,
2046    /// E5-small — High quality (384 dimensions)
2047    E5Small,
2048}
2049
2050/// A text document to upsert with automatic embedding generation.
2051#[derive(Debug, Clone, Serialize, Deserialize)]
2052pub struct TextDocument {
2053    /// Unique identifier for the document.
2054    pub id: String,
2055    /// Raw text content to be embedded.
2056    pub text: String,
2057    /// Optional metadata for the document.
2058    #[serde(skip_serializing_if = "Option::is_none")]
2059    pub metadata: Option<HashMap<String, serde_json::Value>>,
2060    /// Optional TTL in seconds.
2061    #[serde(skip_serializing_if = "Option::is_none")]
2062    pub ttl_seconds: Option<u64>,
2063}
2064
2065impl TextDocument {
2066    /// Create a new text document with the given ID and text.
2067    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
2068        Self {
2069            id: id.into(),
2070            text: text.into(),
2071            metadata: None,
2072            ttl_seconds: None,
2073        }
2074    }
2075
2076    /// Add metadata to this document.
2077    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
2078        self.metadata = Some(metadata);
2079        self
2080    }
2081
2082    /// Set a TTL on this document.
2083    pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
2084        self.ttl_seconds = Some(ttl_seconds);
2085        self
2086    }
2087}
2088
2089/// Request to upsert text documents with automatic embedding.
2090#[derive(Debug, Clone, Serialize, Deserialize)]
2091pub struct UpsertTextRequest {
2092    /// Documents to upsert.
2093    pub documents: Vec<TextDocument>,
2094    /// Embedding model to use (default: minilm).
2095    #[serde(skip_serializing_if = "Option::is_none")]
2096    pub model: Option<EmbeddingModel>,
2097}
2098
2099impl UpsertTextRequest {
2100    /// Create a new upsert-text request.
2101    pub fn new(documents: Vec<TextDocument>) -> Self {
2102        Self {
2103            documents,
2104            model: None,
2105        }
2106    }
2107
2108    /// Set the embedding model.
2109    pub fn with_model(mut self, model: EmbeddingModel) -> Self {
2110        self.model = Some(model);
2111        self
2112    }
2113}
2114
2115/// Response from a text upsert operation.
2116#[derive(Debug, Clone, Serialize, Deserialize)]
2117pub struct TextUpsertResponse {
2118    /// Number of documents upserted.
2119    pub upserted_count: u64,
2120    /// Approximate number of tokens processed.
2121    pub tokens_processed: u64,
2122    /// Embedding model used.
2123    pub model: EmbeddingModel,
2124    /// Time spent generating embeddings in milliseconds.
2125    pub embedding_time_ms: u64,
2126}
2127
2128/// A single text search result.
2129#[derive(Debug, Clone, Serialize, Deserialize)]
2130pub struct TextSearchResult {
2131    /// Document ID.
2132    pub id: String,
2133    /// Similarity score.
2134    pub score: f32,
2135    /// Original text (if `include_text` was true).
2136    #[serde(skip_serializing_if = "Option::is_none")]
2137    pub text: Option<String>,
2138    /// Document metadata.
2139    #[serde(skip_serializing_if = "Option::is_none")]
2140    pub metadata: Option<HashMap<String, serde_json::Value>>,
2141    /// Vector values (if `include_vectors` was true).
2142    #[serde(skip_serializing_if = "Option::is_none")]
2143    pub vector: Option<Vec<f32>>,
2144}
2145
2146/// Request to query using natural language text with automatic embedding.
2147#[derive(Debug, Clone, Serialize, Deserialize)]
2148pub struct QueryTextRequest {
2149    /// Query text.
2150    pub text: String,
2151    /// Number of results to return.
2152    pub top_k: u32,
2153    /// Optional metadata filter.
2154    #[serde(skip_serializing_if = "Option::is_none")]
2155    pub filter: Option<serde_json::Value>,
2156    /// Whether to include the original text in results.
2157    pub include_text: bool,
2158    /// Whether to include vectors in results.
2159    pub include_vectors: bool,
2160    /// Embedding model to use (default: minilm).
2161    #[serde(skip_serializing_if = "Option::is_none")]
2162    pub model: Option<EmbeddingModel>,
2163}
2164
2165impl QueryTextRequest {
2166    /// Create a new text query request.
2167    pub fn new(text: impl Into<String>, top_k: u32) -> Self {
2168        Self {
2169            text: text.into(),
2170            top_k,
2171            filter: None,
2172            include_text: true,
2173            include_vectors: false,
2174            model: None,
2175        }
2176    }
2177
2178    /// Add a metadata filter.
2179    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
2180        self.filter = Some(filter);
2181        self
2182    }
2183
2184    /// Set whether to include the original text in results.
2185    pub fn include_text(mut self, include: bool) -> Self {
2186        self.include_text = include;
2187        self
2188    }
2189
2190    /// Set whether to include vectors in results.
2191    pub fn include_vectors(mut self, include: bool) -> Self {
2192        self.include_vectors = include;
2193        self
2194    }
2195
2196    /// Set the embedding model.
2197    pub fn with_model(mut self, model: EmbeddingModel) -> Self {
2198        self.model = Some(model);
2199        self
2200    }
2201}
2202
2203/// Response from a text query operation.
2204#[derive(Debug, Clone, Serialize, Deserialize)]
2205pub struct TextQueryResponse {
2206    /// Search results.
2207    pub results: Vec<TextSearchResult>,
2208    /// Embedding model used.
2209    pub model: EmbeddingModel,
2210    /// Time spent generating the query embedding in milliseconds.
2211    pub embedding_time_ms: u64,
2212    /// Time spent searching in milliseconds.
2213    pub search_time_ms: u64,
2214}
2215
2216/// Request to execute multiple text queries with automatic embedding in a single call.
2217#[derive(Debug, Clone, Serialize, Deserialize)]
2218pub struct BatchQueryTextRequest {
2219    /// Text queries.
2220    pub queries: Vec<String>,
2221    /// Number of results per query.
2222    pub top_k: u32,
2223    /// Optional metadata filter applied to all queries.
2224    #[serde(skip_serializing_if = "Option::is_none")]
2225    pub filter: Option<serde_json::Value>,
2226    /// Whether to include vectors in results.
2227    pub include_vectors: bool,
2228    /// Embedding model to use (default: minilm).
2229    #[serde(skip_serializing_if = "Option::is_none")]
2230    pub model: Option<EmbeddingModel>,
2231}
2232
2233impl BatchQueryTextRequest {
2234    /// Create a new batch text query request.
2235    pub fn new(queries: Vec<String>, top_k: u32) -> Self {
2236        Self {
2237            queries,
2238            top_k,
2239            filter: None,
2240            include_vectors: false,
2241            model: None,
2242        }
2243    }
2244}
2245
2246/// Response from a batch text query operation.
2247#[derive(Debug, Clone, Serialize, Deserialize)]
2248pub struct BatchQueryTextResponse {
2249    /// Results for each query (in the same order as the request).
2250    pub results: Vec<Vec<TextSearchResult>>,
2251    /// Embedding model used.
2252    pub model: EmbeddingModel,
2253    /// Time spent generating all embeddings in milliseconds.
2254    pub embedding_time_ms: u64,
2255    /// Time spent on all searches in milliseconds.
2256    pub search_time_ms: u64,
2257}
2258
2259// ============================================================================
2260// Fetch by ID Types
2261// ============================================================================
2262
2263/// Request to fetch vectors by their IDs.
2264#[derive(Debug, Clone, Serialize, Deserialize)]
2265pub struct FetchRequest {
2266    /// IDs of vectors to fetch.
2267    pub ids: Vec<String>,
2268    /// Whether to include vector values.
2269    pub include_values: bool,
2270    /// Whether to include metadata.
2271    pub include_metadata: bool,
2272}
2273
2274impl FetchRequest {
2275    /// Create a new fetch request.
2276    pub fn new(ids: Vec<String>) -> Self {
2277        Self {
2278            ids,
2279            include_values: true,
2280            include_metadata: true,
2281        }
2282    }
2283}
2284
2285/// Response from a fetch-by-ID operation.
2286#[derive(Debug, Clone, Serialize, Deserialize)]
2287pub struct FetchResponse {
2288    /// Fetched vectors.
2289    pub vectors: Vec<Vector>,
2290}
2291
2292// ============================================================================
2293// Namespace Management Types
2294// ============================================================================
2295
2296/// Request to create a new namespace.
2297#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2298pub struct CreateNamespaceRequest {
2299    /// Vector dimensions (inferred from first upsert if omitted).
2300    #[serde(skip_serializing_if = "Option::is_none")]
2301    pub dimensions: Option<u32>,
2302    /// Index type (e.g. "hnsw", "flat").
2303    #[serde(skip_serializing_if = "Option::is_none")]
2304    pub index_type: Option<String>,
2305    /// Arbitrary namespace metadata.
2306    #[serde(skip_serializing_if = "Option::is_none")]
2307    pub metadata: Option<HashMap<String, serde_json::Value>>,
2308}
2309
2310impl CreateNamespaceRequest {
2311    /// Create a minimal request (server picks sensible defaults).
2312    pub fn new() -> Self {
2313        Self::default()
2314    }
2315
2316    /// Set the vector dimensions.
2317    pub fn with_dimensions(mut self, dimensions: u32) -> Self {
2318        self.dimensions = Some(dimensions);
2319        self
2320    }
2321
2322    /// Set the index type.
2323    pub fn with_index_type(mut self, index_type: impl Into<String>) -> Self {
2324        self.index_type = Some(index_type.into());
2325        self
2326    }
2327}
2328
2329/// Request body for `PUT /v1/namespaces/:namespace` — upsert semantics (v0.6.0).
2330///
2331/// Creates the namespace if it does not exist, or updates its configuration
2332/// if it already exists.  Requires `Scope::Write`.
2333#[derive(Debug, Clone, Serialize, Deserialize)]
2334pub struct ConfigureNamespaceRequest {
2335    /// Vector dimension.  Required on first creation; must match on subsequent calls.
2336    pub dimension: usize,
2337    /// Distance metric (defaults to cosine when omitted).
2338    #[serde(skip_serializing_if = "Option::is_none")]
2339    pub distance: Option<DistanceMetric>,
2340}
2341
2342impl ConfigureNamespaceRequest {
2343    /// Create a new configure-namespace request with the given dimension.
2344    pub fn new(dimension: usize) -> Self {
2345        Self {
2346            dimension,
2347            distance: None,
2348        }
2349    }
2350
2351    /// Set the distance metric.
2352    pub fn with_distance(mut self, distance: DistanceMetric) -> Self {
2353        self.distance = Some(distance);
2354        self
2355    }
2356}
2357
2358/// Response from `PUT /v1/namespaces/:namespace`.
2359#[derive(Debug, Clone, Serialize, Deserialize)]
2360pub struct ConfigureNamespaceResponse {
2361    /// Namespace name.
2362    pub namespace: String,
2363    /// Vector dimension.
2364    pub dimension: usize,
2365    /// Distance metric in use.
2366    pub distance: DistanceMetric,
2367    /// `true` if the namespace was newly created; `false` if it already existed.
2368    pub created: bool,
2369}