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    #[serde(default)]
111    pub vector_count: u64,
112    /// Vector dimensions
113    #[serde(alias = "dimension")]
114    pub dimensions: Option<u32>,
115    /// Index type used
116    pub index_type: Option<String>,
117    /// Whether the namespace was newly created (from PUT/configure response)
118    #[serde(default)]
119    pub created: Option<bool>,
120}
121
122/// List namespaces response
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ListNamespacesResponse {
125    /// List of namespace names
126    pub namespaces: Vec<String>,
127}
128
129// ============================================================================
130// Vector Types
131// ============================================================================
132
133/// A vector with ID and optional metadata
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Vector {
136    /// Unique vector identifier
137    pub id: String,
138    /// Vector values (embeddings)
139    pub values: Vec<f32>,
140    /// Optional metadata
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub metadata: Option<HashMap<String, serde_json::Value>>,
143}
144
145impl Vector {
146    /// Create a new vector with just ID and values
147    pub fn new(id: impl Into<String>, values: Vec<f32>) -> Self {
148        Self {
149            id: id.into(),
150            values,
151            metadata: None,
152        }
153    }
154
155    /// Create a new vector with metadata
156    pub fn with_metadata(
157        id: impl Into<String>,
158        values: Vec<f32>,
159        metadata: HashMap<String, serde_json::Value>,
160    ) -> Self {
161        Self {
162            id: id.into(),
163            values,
164            metadata: Some(metadata),
165        }
166    }
167}
168
169/// Upsert vectors request
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct UpsertRequest {
172    /// Vectors to upsert
173    pub vectors: Vec<Vector>,
174}
175
176impl UpsertRequest {
177    /// Create a new upsert request with a single vector
178    pub fn single(vector: Vector) -> Self {
179        Self {
180            vectors: vec![vector],
181        }
182    }
183
184    /// Create a new upsert request with multiple vectors
185    pub fn batch(vectors: Vec<Vector>) -> Self {
186        Self { vectors }
187    }
188}
189
190/// Upsert response
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct UpsertResponse {
193    /// Number of vectors upserted
194    pub upserted_count: u64,
195}
196
197/// Column-based upsert request (Turbopuffer-inspired)
198///
199/// This format is more efficient for bulk upserts as it avoids repeating
200/// field names for each vector. All arrays must have equal length.
201///
202/// # Example
203///
204/// ```rust
205/// use dakera_client::ColumnUpsertRequest;
206/// use std::collections::HashMap;
207///
208/// let request = ColumnUpsertRequest::new(
209///     vec!["id1".to_string(), "id2".to_string()],
210///     vec![vec![0.1, 0.2, 0.3], vec![0.4, 0.5, 0.6]],
211/// );
212/// ```
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ColumnUpsertRequest {
215    /// Array of vector IDs (required)
216    pub ids: Vec<String>,
217    /// Array of vectors (required for vector namespaces)
218    pub vectors: Vec<Vec<f32>>,
219    /// Additional attributes as columns (optional)
220    /// Each key is an attribute name, value is array of attribute values
221    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
222    pub attributes: HashMap<String, Vec<serde_json::Value>>,
223    /// TTL in seconds for all vectors (optional)
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub ttl_seconds: Option<u64>,
226    /// Expected dimension (optional, for validation)
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub dimension: Option<usize>,
229}
230
231impl ColumnUpsertRequest {
232    /// Create a new column upsert request
233    pub fn new(ids: Vec<String>, vectors: Vec<Vec<f32>>) -> Self {
234        Self {
235            ids,
236            vectors,
237            attributes: HashMap::new(),
238            ttl_seconds: None,
239            dimension: None,
240        }
241    }
242
243    /// Add an attribute column
244    pub fn with_attribute(
245        mut self,
246        name: impl Into<String>,
247        values: Vec<serde_json::Value>,
248    ) -> Self {
249        self.attributes.insert(name.into(), values);
250        self
251    }
252
253    /// Set TTL for all vectors
254    pub fn with_ttl(mut self, seconds: u64) -> Self {
255        self.ttl_seconds = Some(seconds);
256        self
257    }
258
259    /// Set expected dimension for validation
260    pub fn with_dimension(mut self, dim: usize) -> Self {
261        self.dimension = Some(dim);
262        self
263    }
264
265    /// Get the number of vectors in this request
266    pub fn len(&self) -> usize {
267        self.ids.len()
268    }
269
270    /// Check if the request is empty
271    pub fn is_empty(&self) -> bool {
272        self.ids.is_empty()
273    }
274}
275
276/// Delete vectors request
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct DeleteRequest {
279    /// Vector IDs to delete
280    pub ids: Vec<String>,
281}
282
283impl DeleteRequest {
284    /// Create a delete request for a single ID
285    pub fn single(id: impl Into<String>) -> Self {
286        Self {
287            ids: vec![id.into()],
288        }
289    }
290
291    /// Create a delete request for multiple IDs
292    pub fn batch(ids: Vec<String>) -> Self {
293        Self { ids }
294    }
295}
296
297/// Delete response
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct DeleteResponse {
300    /// Number of vectors deleted
301    pub deleted_count: u64,
302}
303
304// ============================================================================
305// Query Types
306// ============================================================================
307
308/// Read consistency level for queries (Turbopuffer-inspired)
309#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
310#[serde(rename_all = "snake_case")]
311pub enum ReadConsistency {
312    /// Always read from primary/leader node - guarantees latest data
313    Strong,
314    /// Read from any replica - may return slightly stale data but faster
315    #[default]
316    Eventual,
317    /// Read from replicas within staleness bounds
318    #[serde(rename = "bounded_staleness")]
319    BoundedStaleness,
320}
321
322/// Configuration for bounded staleness reads
323#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
324pub struct StalenessConfig {
325    /// Maximum acceptable staleness in milliseconds
326    #[serde(default = "default_max_staleness_ms")]
327    pub max_staleness_ms: u64,
328}
329
330fn default_max_staleness_ms() -> u64 {
331    5000 // 5 seconds default
332}
333
334impl StalenessConfig {
335    /// Create a new staleness config with specified max staleness
336    pub fn new(max_staleness_ms: u64) -> Self {
337        Self { max_staleness_ms }
338    }
339}
340
341/// Distance metric for similarity search
342#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
343#[serde(rename_all = "snake_case")]
344pub enum DistanceMetric {
345    /// Cosine similarity (default)
346    #[default]
347    Cosine,
348    /// Euclidean distance
349    Euclidean,
350    /// Dot product
351    DotProduct,
352}
353
354/// Query request for vector similarity search
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct QueryRequest {
357    /// Query vector
358    pub vector: Vec<f32>,
359    /// Number of results to return
360    pub top_k: u32,
361    /// Distance metric to use
362    #[serde(default)]
363    pub distance_metric: DistanceMetric,
364    /// Optional filter expression
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub filter: Option<serde_json::Value>,
367    /// Whether to include metadata in results
368    #[serde(default = "default_true")]
369    pub include_metadata: bool,
370    /// Whether to include vector values in results
371    #[serde(default)]
372    pub include_vectors: bool,
373    /// Read consistency level
374    #[serde(default)]
375    pub consistency: ReadConsistency,
376    /// Staleness configuration for bounded staleness reads
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub staleness_config: Option<StalenessConfig>,
379}
380
381fn default_true() -> bool {
382    true
383}
384
385impl QueryRequest {
386    /// Create a new query request
387    pub fn new(vector: Vec<f32>, top_k: u32) -> Self {
388        Self {
389            vector,
390            top_k,
391            distance_metric: DistanceMetric::default(),
392            filter: None,
393            include_metadata: true,
394            include_vectors: false,
395            consistency: ReadConsistency::default(),
396            staleness_config: None,
397        }
398    }
399
400    /// Add a filter to the query
401    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
402        self.filter = Some(filter);
403        self
404    }
405
406    /// Set whether to include metadata
407    pub fn include_metadata(mut self, include: bool) -> Self {
408        self.include_metadata = include;
409        self
410    }
411
412    /// Set whether to include vector values
413    pub fn include_vectors(mut self, include: bool) -> Self {
414        self.include_vectors = include;
415        self
416    }
417
418    /// Set distance metric
419    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
420        self.distance_metric = metric;
421        self
422    }
423
424    /// Set read consistency level
425    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
426        self.consistency = consistency;
427        self
428    }
429
430    /// Set bounded staleness with max staleness in ms
431    pub fn with_bounded_staleness(mut self, max_staleness_ms: u64) -> Self {
432        self.consistency = ReadConsistency::BoundedStaleness;
433        self.staleness_config = Some(StalenessConfig::new(max_staleness_ms));
434        self
435    }
436
437    /// Use strong consistency (always read from primary)
438    pub fn with_strong_consistency(mut self) -> Self {
439        self.consistency = ReadConsistency::Strong;
440        self
441    }
442}
443
444/// A match result from a query
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct Match {
447    /// Vector ID
448    pub id: String,
449    /// Similarity score
450    pub score: f32,
451    /// Optional metadata
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub metadata: Option<HashMap<String, serde_json::Value>>,
454}
455
456/// Query response
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct QueryResponse {
459    /// Search results
460    #[serde(alias = "matches")]
461    pub results: Vec<Match>,
462}
463
464// ============================================================================
465// Full-Text Search Types
466// ============================================================================
467
468/// A document for full-text indexing
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct Document {
471    /// Document ID
472    pub id: String,
473    /// Document text content
474    pub text: String,
475    /// Optional metadata
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub metadata: Option<HashMap<String, serde_json::Value>>,
478}
479
480impl Document {
481    /// Create a new document
482    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
483        Self {
484            id: id.into(),
485            text: text.into(),
486            metadata: None,
487        }
488    }
489
490    /// Create a new document with metadata
491    pub fn with_metadata(
492        id: impl Into<String>,
493        text: impl Into<String>,
494        metadata: HashMap<String, serde_json::Value>,
495    ) -> Self {
496        Self {
497            id: id.into(),
498            text: text.into(),
499            metadata: Some(metadata),
500        }
501    }
502}
503
504/// Index documents request
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct IndexDocumentsRequest {
507    /// Documents to index
508    pub documents: Vec<Document>,
509}
510
511/// Index documents response
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct IndexDocumentsResponse {
514    /// Number of documents indexed
515    pub indexed_count: u64,
516}
517
518/// Full-text search request
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct FullTextSearchRequest {
521    /// Search query
522    pub query: String,
523    /// Maximum number of results
524    pub top_k: u32,
525    /// Optional filter
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub filter: Option<serde_json::Value>,
528}
529
530impl FullTextSearchRequest {
531    /// Create a new full-text search request
532    pub fn new(query: impl Into<String>, top_k: u32) -> Self {
533        Self {
534            query: query.into(),
535            top_k,
536            filter: None,
537        }
538    }
539
540    /// Add a filter to the search
541    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
542        self.filter = Some(filter);
543        self
544    }
545}
546
547/// Full-text search result
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct FullTextMatch {
550    /// Document ID
551    pub id: String,
552    /// BM25 score
553    pub score: f32,
554    /// Document text
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub text: Option<String>,
557    /// Optional metadata
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub metadata: Option<HashMap<String, serde_json::Value>>,
560}
561
562/// Full-text search response
563#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct FullTextSearchResponse {
565    /// Matched documents
566    #[serde(alias = "matches")]
567    pub results: Vec<FullTextMatch>,
568}
569
570/// Full-text index statistics
571#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct FullTextStats {
573    /// Number of documents indexed
574    pub document_count: u64,
575    /// Number of unique terms
576    pub term_count: u64,
577}
578
579// ============================================================================
580// Hybrid Search Types
581// ============================================================================
582
583/// Hybrid search request combining vector and full-text search.
584///
585/// When `vector` is `None` the server falls back to BM25-only full-text search.
586/// When provided, results are blended with vector similarity according to `vector_weight`.
587#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct HybridSearchRequest {
589    /// Optional query vector. Omit for BM25-only search.
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub vector: Option<Vec<f32>>,
592    /// Text query
593    pub text: String,
594    /// Number of results to return
595    pub top_k: u32,
596    /// Weight for vector search (0.0-1.0)
597    #[serde(default = "default_vector_weight")]
598    pub vector_weight: f32,
599    /// Optional filter
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub filter: Option<serde_json::Value>,
602}
603
604fn default_vector_weight() -> f32 {
605    0.5
606}
607
608impl HybridSearchRequest {
609    /// Create a new hybrid search request with a query vector (hybrid mode).
610    pub fn new(vector: Vec<f32>, text: impl Into<String>, top_k: u32) -> Self {
611        Self {
612            vector: Some(vector),
613            text: text.into(),
614            top_k,
615            vector_weight: 0.5,
616            filter: None,
617        }
618    }
619
620    /// Create a BM25-only full-text search request (no vector required).
621    pub fn text_only(text: impl Into<String>, top_k: u32) -> Self {
622        Self {
623            vector: None,
624            text: text.into(),
625            top_k,
626            vector_weight: 0.5,
627            filter: None,
628        }
629    }
630
631    /// Set the vector weight (text weight is 1.0 - vector_weight)
632    pub fn with_vector_weight(mut self, weight: f32) -> Self {
633        self.vector_weight = weight.clamp(0.0, 1.0);
634        self
635    }
636
637    /// Add a filter to the search
638    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
639        self.filter = Some(filter);
640        self
641    }
642}
643
644/// Hybrid search response
645#[derive(Debug, Clone, Serialize, Deserialize)]
646pub struct HybridSearchResponse {
647    /// Matched results
648    #[serde(alias = "matches")]
649    pub results: Vec<Match>,
650}
651
652// ============================================================================
653// Operations Types
654// ============================================================================
655
656/// System diagnostics
657#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct SystemDiagnostics {
659    /// System information
660    pub system: SystemInfo,
661    /// Resource usage
662    pub resources: ResourceUsage,
663    /// Component health
664    pub components: ComponentHealth,
665    /// Number of active jobs
666    pub active_jobs: u64,
667}
668
669/// System information
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct SystemInfo {
672    /// Dakera version
673    pub version: String,
674    /// Rust version
675    pub rust_version: String,
676    /// Uptime in seconds
677    pub uptime_seconds: u64,
678    /// Process ID
679    pub pid: u32,
680}
681
682/// Resource usage metrics
683#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct ResourceUsage {
685    /// Memory usage in bytes
686    pub memory_bytes: u64,
687    /// Thread count
688    pub thread_count: u64,
689    /// Open file descriptors
690    pub open_fds: u64,
691    /// CPU usage percentage
692    pub cpu_percent: Option<f64>,
693}
694
695/// Component health status
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct ComponentHealth {
698    /// Storage health
699    pub storage: HealthStatus,
700    /// Search engine health
701    pub search_engine: HealthStatus,
702    /// Cache health
703    pub cache: HealthStatus,
704    /// gRPC health
705    pub grpc: HealthStatus,
706}
707
708/// Health status for a component
709#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct HealthStatus {
711    /// Is the component healthy
712    pub healthy: bool,
713    /// Status message
714    pub message: String,
715    /// Last check timestamp
716    pub last_check: u64,
717}
718
719/// Background job information
720#[derive(Debug, Clone, Serialize, Deserialize)]
721pub struct JobInfo {
722    /// Job ID
723    pub id: String,
724    /// Job type
725    pub job_type: String,
726    /// Current status
727    pub status: String,
728    /// Creation timestamp
729    pub created_at: u64,
730    /// Start timestamp
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub started_at: Option<u64>,
733    /// Completion timestamp
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub completed_at: Option<u64>,
736    /// Progress percentage
737    pub progress: u8,
738    /// Status message
739    #[serde(skip_serializing_if = "Option::is_none")]
740    pub message: Option<String>,
741    /// Job metadata
742    #[serde(default)]
743    pub metadata: std::collections::HashMap<String, String>,
744}
745
746/// Compaction request
747#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct CompactionRequest {
749    /// Namespace to compact (None = all)
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub namespace: Option<String>,
752    /// Force compaction
753    #[serde(default)]
754    pub force: bool,
755}
756
757/// Compaction response
758#[derive(Debug, Clone, Serialize, Deserialize)]
759pub struct CompactionResponse {
760    /// Job ID for tracking
761    pub job_id: String,
762    /// Status message
763    pub message: String,
764}
765
766// ============================================================================
767// Cache Warming Types (Turbopuffer-inspired)
768// ============================================================================
769
770/// Priority level for cache warming operations
771#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
772#[serde(rename_all = "snake_case")]
773pub enum WarmingPriority {
774    /// Highest priority - warm immediately, preempt other operations
775    Critical,
776    /// High priority - warm soon
777    High,
778    /// Normal priority (default)
779    #[default]
780    Normal,
781    /// Low priority - warm when resources available
782    Low,
783    /// Background priority - warm during idle time only
784    Background,
785}
786
787/// Target cache tier for warming
788#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
789#[serde(rename_all = "snake_case")]
790pub enum WarmingTargetTier {
791    /// L1 in-memory cache (Moka) - fastest, limited size
792    L1,
793    /// L2 local disk cache (RocksDB) - larger, persistent
794    #[default]
795    L2,
796    /// Both L1 and L2 caches
797    Both,
798}
799
800/// Access pattern hint for cache optimization
801#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
802#[serde(rename_all = "snake_case")]
803pub enum AccessPatternHint {
804    /// Random access pattern
805    #[default]
806    Random,
807    /// Sequential access pattern
808    Sequential,
809    /// Temporal locality (recently accessed items accessed again)
810    Temporal,
811    /// Spatial locality (nearby items accessed together)
812    Spatial,
813}
814
815/// Cache warming request with priority hints
816#[derive(Debug, Clone, Serialize, Deserialize)]
817pub struct WarmCacheRequest {
818    /// Namespace to warm
819    pub namespace: String,
820    /// Specific vector IDs to warm (None = all)
821    #[serde(skip_serializing_if = "Option::is_none")]
822    pub vector_ids: Option<Vec<String>>,
823    /// Warming priority level
824    #[serde(default)]
825    pub priority: WarmingPriority,
826    /// Target cache tier
827    #[serde(default)]
828    pub target_tier: WarmingTargetTier,
829    /// Run warming in background (non-blocking)
830    #[serde(default)]
831    pub background: bool,
832    /// TTL hint in seconds
833    #[serde(skip_serializing_if = "Option::is_none")]
834    pub ttl_hint_seconds: Option<u64>,
835    /// Access pattern hint for optimization
836    #[serde(default)]
837    pub access_pattern: AccessPatternHint,
838    /// Maximum vectors to warm
839    #[serde(skip_serializing_if = "Option::is_none")]
840    pub max_vectors: Option<usize>,
841}
842
843impl WarmCacheRequest {
844    /// Create a new cache warming request for a namespace
845    pub fn new(namespace: impl Into<String>) -> Self {
846        Self {
847            namespace: namespace.into(),
848            vector_ids: None,
849            priority: WarmingPriority::default(),
850            target_tier: WarmingTargetTier::default(),
851            background: false,
852            ttl_hint_seconds: None,
853            access_pattern: AccessPatternHint::default(),
854            max_vectors: None,
855        }
856    }
857
858    /// Warm specific vector IDs
859    pub fn with_vector_ids(mut self, ids: Vec<String>) -> Self {
860        self.vector_ids = Some(ids);
861        self
862    }
863
864    /// Set warming priority
865    pub fn with_priority(mut self, priority: WarmingPriority) -> Self {
866        self.priority = priority;
867        self
868    }
869
870    /// Set target cache tier
871    pub fn with_target_tier(mut self, tier: WarmingTargetTier) -> Self {
872        self.target_tier = tier;
873        self
874    }
875
876    /// Run warming in background
877    pub fn in_background(mut self) -> Self {
878        self.background = true;
879        self
880    }
881
882    /// Set TTL hint
883    pub fn with_ttl(mut self, seconds: u64) -> Self {
884        self.ttl_hint_seconds = Some(seconds);
885        self
886    }
887
888    /// Set access pattern hint
889    pub fn with_access_pattern(mut self, pattern: AccessPatternHint) -> Self {
890        self.access_pattern = pattern;
891        self
892    }
893
894    /// Limit number of vectors to warm
895    pub fn with_max_vectors(mut self, max: usize) -> Self {
896        self.max_vectors = Some(max);
897        self
898    }
899}
900
901/// Cache warming response
902#[derive(Debug, Clone, Serialize, Deserialize)]
903pub struct WarmCacheResponse {
904    /// Operation success
905    pub success: bool,
906    /// Number of entries warmed
907    pub entries_warmed: u64,
908    /// Number of entries already warm (skipped)
909    pub entries_skipped: u64,
910    /// Job ID for tracking background operations
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub job_id: Option<String>,
913    /// Status message
914    pub message: String,
915    /// Estimated completion time for background jobs (ISO 8601)
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub estimated_completion: Option<String>,
918    /// Target tier that was warmed
919    pub target_tier: WarmingTargetTier,
920    /// Priority that was used
921    pub priority: WarmingPriority,
922    /// Bytes warmed (approximate)
923    #[serde(skip_serializing_if = "Option::is_none")]
924    pub bytes_warmed: Option<u64>,
925}
926
927// ============================================================================
928// Export Types (Turbopuffer-inspired)
929// ============================================================================
930
931/// Request to export vectors from a namespace with pagination
932#[derive(Debug, Clone, Serialize, Deserialize)]
933pub struct ExportRequest {
934    /// Maximum number of vectors to return per page (default: 1000, max: 10000)
935    #[serde(default = "default_export_top_k")]
936    pub top_k: usize,
937    /// Cursor for pagination - the last vector ID from previous page
938    #[serde(skip_serializing_if = "Option::is_none")]
939    pub cursor: Option<String>,
940    /// Whether to include vector values in the response (default: true)
941    #[serde(default = "default_true")]
942    pub include_vectors: bool,
943    /// Whether to include metadata in the response (default: true)
944    #[serde(default = "default_true")]
945    pub include_metadata: bool,
946}
947
948fn default_export_top_k() -> usize {
949    1000
950}
951
952impl Default for ExportRequest {
953    fn default() -> Self {
954        Self {
955            top_k: 1000,
956            cursor: None,
957            include_vectors: true,
958            include_metadata: true,
959        }
960    }
961}
962
963impl ExportRequest {
964    /// Create a new export request with default settings
965    pub fn new() -> Self {
966        Self::default()
967    }
968
969    /// Set the maximum number of vectors to return per page
970    pub fn with_top_k(mut self, top_k: usize) -> Self {
971        self.top_k = top_k;
972        self
973    }
974
975    /// Set the pagination cursor
976    pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
977        self.cursor = Some(cursor.into());
978        self
979    }
980
981    /// Set whether to include vector values
982    pub fn include_vectors(mut self, include: bool) -> Self {
983        self.include_vectors = include;
984        self
985    }
986
987    /// Set whether to include metadata
988    pub fn include_metadata(mut self, include: bool) -> Self {
989        self.include_metadata = include;
990        self
991    }
992}
993
994/// A single exported vector record
995#[derive(Debug, Clone, Serialize, Deserialize)]
996pub struct ExportedVector {
997    /// Vector ID
998    pub id: String,
999    /// Vector values (optional based on include_vectors)
1000    #[serde(skip_serializing_if = "Option::is_none")]
1001    pub values: Option<Vec<f32>>,
1002    /// Metadata (optional based on include_metadata)
1003    #[serde(skip_serializing_if = "Option::is_none")]
1004    pub metadata: Option<serde_json::Value>,
1005    /// TTL in seconds if set
1006    #[serde(skip_serializing_if = "Option::is_none")]
1007    pub ttl_seconds: Option<u64>,
1008}
1009
1010/// Response from export operation
1011#[derive(Debug, Clone, Serialize, Deserialize)]
1012pub struct ExportResponse {
1013    /// Exported vectors for this page
1014    pub vectors: Vec<ExportedVector>,
1015    /// Cursor for next page (None if this is the last page)
1016    #[serde(skip_serializing_if = "Option::is_none")]
1017    pub next_cursor: Option<String>,
1018    /// Total vectors in namespace (for progress tracking)
1019    pub total_count: usize,
1020    /// Number of vectors returned in this page
1021    pub returned_count: usize,
1022}
1023
1024// ============================================================================
1025// Batch Query Types
1026// ============================================================================
1027
1028/// A single query within a batch request
1029#[derive(Debug, Clone, Serialize, Deserialize)]
1030pub struct BatchQueryItem {
1031    /// Unique identifier for this query within the batch
1032    #[serde(skip_serializing_if = "Option::is_none")]
1033    pub id: Option<String>,
1034    /// The query vector
1035    pub vector: Vec<f32>,
1036    /// Number of results to return
1037    #[serde(default = "default_batch_top_k")]
1038    pub top_k: u32,
1039    /// Optional filter expression
1040    #[serde(skip_serializing_if = "Option::is_none")]
1041    pub filter: Option<serde_json::Value>,
1042    /// Whether to include metadata in results
1043    #[serde(default)]
1044    pub include_metadata: bool,
1045    /// Read consistency level
1046    #[serde(default)]
1047    pub consistency: ReadConsistency,
1048    /// Staleness configuration for bounded staleness reads
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub staleness_config: Option<StalenessConfig>,
1051}
1052
1053fn default_batch_top_k() -> u32 {
1054    10
1055}
1056
1057impl BatchQueryItem {
1058    /// Create a new batch query item
1059    pub fn new(vector: Vec<f32>, top_k: u32) -> Self {
1060        Self {
1061            id: None,
1062            vector,
1063            top_k,
1064            filter: None,
1065            include_metadata: true,
1066            consistency: ReadConsistency::default(),
1067            staleness_config: None,
1068        }
1069    }
1070
1071    /// Set a unique identifier for this query
1072    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1073        self.id = Some(id.into());
1074        self
1075    }
1076
1077    /// Add a filter to the query
1078    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1079        self.filter = Some(filter);
1080        self
1081    }
1082
1083    /// Set whether to include metadata
1084    pub fn include_metadata(mut self, include: bool) -> Self {
1085        self.include_metadata = include;
1086        self
1087    }
1088
1089    /// Set read consistency level
1090    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
1091        self.consistency = consistency;
1092        self
1093    }
1094
1095    /// Set bounded staleness with max staleness in ms
1096    pub fn with_bounded_staleness(mut self, max_staleness_ms: u64) -> Self {
1097        self.consistency = ReadConsistency::BoundedStaleness;
1098        self.staleness_config = Some(StalenessConfig::new(max_staleness_ms));
1099        self
1100    }
1101}
1102
1103/// Batch query request - execute multiple queries in parallel
1104#[derive(Debug, Clone, Serialize, Deserialize)]
1105pub struct BatchQueryRequest {
1106    /// List of queries to execute
1107    pub queries: Vec<BatchQueryItem>,
1108}
1109
1110impl BatchQueryRequest {
1111    /// Create a new batch query request
1112    pub fn new(queries: Vec<BatchQueryItem>) -> Self {
1113        Self { queries }
1114    }
1115
1116    /// Create a batch query request from a single query
1117    pub fn single(query: BatchQueryItem) -> Self {
1118        Self {
1119            queries: vec![query],
1120        }
1121    }
1122}
1123
1124/// Results for a single query within a batch
1125#[derive(Debug, Clone, Serialize, Deserialize)]
1126pub struct BatchQueryResult {
1127    /// The query identifier (if provided in request)
1128    #[serde(skip_serializing_if = "Option::is_none")]
1129    pub id: Option<String>,
1130    /// Query results (empty if an error occurred)
1131    pub results: Vec<Match>,
1132    /// Query execution time in milliseconds
1133    pub latency_ms: f64,
1134    /// Error message if this individual query failed
1135    #[serde(skip_serializing_if = "Option::is_none")]
1136    pub error: Option<String>,
1137}
1138
1139/// Batch query response
1140#[derive(Debug, Clone, Serialize, Deserialize)]
1141pub struct BatchQueryResponse {
1142    /// Results for each query in the batch
1143    pub results: Vec<BatchQueryResult>,
1144    /// Total execution time in milliseconds
1145    pub total_latency_ms: f64,
1146    /// Number of queries executed
1147    pub query_count: usize,
1148}
1149
1150// ============================================================================
1151// Multi-Vector Search Types
1152// ============================================================================
1153
1154/// Request for multi-vector search with positive and negative vectors
1155#[derive(Debug, Clone, Serialize, Deserialize)]
1156pub struct MultiVectorSearchRequest {
1157    /// Positive vectors to search towards (required, at least one)
1158    pub positive_vectors: Vec<Vec<f32>>,
1159    /// Weights for positive vectors (optional, defaults to equal weights)
1160    #[serde(skip_serializing_if = "Option::is_none")]
1161    pub positive_weights: Option<Vec<f32>>,
1162    /// Negative vectors to search away from (optional)
1163    #[serde(skip_serializing_if = "Option::is_none")]
1164    pub negative_vectors: Option<Vec<Vec<f32>>>,
1165    /// Weights for negative vectors (optional, defaults to equal weights)
1166    #[serde(skip_serializing_if = "Option::is_none")]
1167    pub negative_weights: Option<Vec<f32>>,
1168    /// Number of results to return
1169    #[serde(default = "default_multi_vector_top_k")]
1170    pub top_k: u32,
1171    /// Distance metric to use
1172    #[serde(default)]
1173    pub distance_metric: DistanceMetric,
1174    /// Minimum score threshold
1175    #[serde(skip_serializing_if = "Option::is_none")]
1176    pub score_threshold: Option<f32>,
1177    /// Enable MMR (Maximal Marginal Relevance) for diversity
1178    #[serde(default)]
1179    pub enable_mmr: bool,
1180    /// Lambda parameter for MMR (0 = max diversity, 1 = max relevance)
1181    #[serde(default = "default_mmr_lambda")]
1182    pub mmr_lambda: f32,
1183    /// Include metadata in results
1184    #[serde(default = "default_true")]
1185    pub include_metadata: bool,
1186    /// Include vectors in results
1187    #[serde(default)]
1188    pub include_vectors: bool,
1189    /// Optional metadata filter
1190    #[serde(skip_serializing_if = "Option::is_none")]
1191    pub filter: Option<serde_json::Value>,
1192    /// Read consistency level
1193    #[serde(default)]
1194    pub consistency: ReadConsistency,
1195    /// Staleness configuration for bounded staleness reads
1196    #[serde(skip_serializing_if = "Option::is_none")]
1197    pub staleness_config: Option<StalenessConfig>,
1198}
1199
1200fn default_multi_vector_top_k() -> u32 {
1201    10
1202}
1203
1204fn default_mmr_lambda() -> f32 {
1205    0.5
1206}
1207
1208impl MultiVectorSearchRequest {
1209    /// Create a new multi-vector search request with positive vectors
1210    pub fn new(positive_vectors: Vec<Vec<f32>>) -> Self {
1211        Self {
1212            positive_vectors,
1213            positive_weights: None,
1214            negative_vectors: None,
1215            negative_weights: None,
1216            top_k: 10,
1217            distance_metric: DistanceMetric::default(),
1218            score_threshold: None,
1219            enable_mmr: false,
1220            mmr_lambda: 0.5,
1221            include_metadata: true,
1222            include_vectors: false,
1223            filter: None,
1224            consistency: ReadConsistency::default(),
1225            staleness_config: None,
1226        }
1227    }
1228
1229    /// Set the number of results to return
1230    pub fn with_top_k(mut self, top_k: u32) -> Self {
1231        self.top_k = top_k;
1232        self
1233    }
1234
1235    /// Add weights for positive vectors
1236    pub fn with_positive_weights(mut self, weights: Vec<f32>) -> Self {
1237        self.positive_weights = Some(weights);
1238        self
1239    }
1240
1241    /// Add negative vectors to search away from
1242    pub fn with_negative_vectors(mut self, vectors: Vec<Vec<f32>>) -> Self {
1243        self.negative_vectors = Some(vectors);
1244        self
1245    }
1246
1247    /// Add weights for negative vectors
1248    pub fn with_negative_weights(mut self, weights: Vec<f32>) -> Self {
1249        self.negative_weights = Some(weights);
1250        self
1251    }
1252
1253    /// Set distance metric
1254    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
1255        self.distance_metric = metric;
1256        self
1257    }
1258
1259    /// Set minimum score threshold
1260    pub fn with_score_threshold(mut self, threshold: f32) -> Self {
1261        self.score_threshold = Some(threshold);
1262        self
1263    }
1264
1265    /// Enable MMR for diversity
1266    pub fn with_mmr(mut self, lambda: f32) -> Self {
1267        self.enable_mmr = true;
1268        self.mmr_lambda = lambda.clamp(0.0, 1.0);
1269        self
1270    }
1271
1272    /// Set whether to include metadata
1273    pub fn include_metadata(mut self, include: bool) -> Self {
1274        self.include_metadata = include;
1275        self
1276    }
1277
1278    /// Set whether to include vectors
1279    pub fn include_vectors(mut self, include: bool) -> Self {
1280        self.include_vectors = include;
1281        self
1282    }
1283
1284    /// Add a filter
1285    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1286        self.filter = Some(filter);
1287        self
1288    }
1289
1290    /// Set read consistency level
1291    pub fn with_consistency(mut self, consistency: ReadConsistency) -> Self {
1292        self.consistency = consistency;
1293        self
1294    }
1295}
1296
1297/// Single result from multi-vector search
1298#[derive(Debug, Clone, Serialize, Deserialize)]
1299pub struct MultiVectorSearchResult {
1300    /// Vector ID
1301    pub id: String,
1302    /// Similarity score
1303    pub score: f32,
1304    /// MMR score (if MMR enabled)
1305    #[serde(skip_serializing_if = "Option::is_none")]
1306    pub mmr_score: Option<f32>,
1307    /// Original rank before reranking
1308    #[serde(skip_serializing_if = "Option::is_none")]
1309    pub original_rank: Option<usize>,
1310    /// Optional metadata
1311    #[serde(skip_serializing_if = "Option::is_none")]
1312    pub metadata: Option<HashMap<String, serde_json::Value>>,
1313    /// Optional vector values
1314    #[serde(skip_serializing_if = "Option::is_none")]
1315    pub vector: Option<Vec<f32>>,
1316}
1317
1318/// Response from multi-vector search
1319#[derive(Debug, Clone, Serialize, Deserialize)]
1320pub struct MultiVectorSearchResponse {
1321    /// Search results
1322    pub results: Vec<MultiVectorSearchResult>,
1323    /// The computed query vector (weighted combination of positive - negative)
1324    #[serde(skip_serializing_if = "Option::is_none")]
1325    pub computed_query_vector: Option<Vec<f32>>,
1326}
1327
1328// ============================================================================
1329// Aggregation Types (Turbopuffer-inspired)
1330// ============================================================================
1331
1332/// Aggregate function for computing values across documents
1333#[derive(Debug, Clone, Serialize, Deserialize)]
1334#[serde(untagged)]
1335pub enum AggregateFunction {
1336    /// Count matching documents
1337    Count,
1338    /// Sum numeric attribute values
1339    Sum { field: String },
1340    /// Average numeric attribute values
1341    Avg { field: String },
1342    /// Minimum numeric attribute value
1343    Min { field: String },
1344    /// Maximum numeric attribute value
1345    Max { field: String },
1346}
1347
1348/// Request for aggregation query (Turbopuffer-inspired)
1349#[derive(Debug, Clone, Serialize, Deserialize)]
1350pub struct AggregationRequest {
1351    /// Named aggregations to compute
1352    /// Example: {"my_count": ["Count"], "total_score": ["Sum", "score"]}
1353    pub aggregate_by: HashMap<String, serde_json::Value>,
1354    /// Fields to group results by (optional)
1355    /// Example: ["category", "status"]
1356    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1357    pub group_by: Vec<String>,
1358    /// Filter to apply before aggregation
1359    #[serde(skip_serializing_if = "Option::is_none")]
1360    pub filter: Option<serde_json::Value>,
1361    /// Maximum number of groups to return (default: 100)
1362    #[serde(default = "default_agg_limit")]
1363    pub limit: usize,
1364}
1365
1366fn default_agg_limit() -> usize {
1367    100
1368}
1369
1370impl AggregationRequest {
1371    /// Create a new aggregation request with a single aggregation
1372    pub fn new() -> Self {
1373        Self {
1374            aggregate_by: HashMap::new(),
1375            group_by: Vec::new(),
1376            filter: None,
1377            limit: 100,
1378        }
1379    }
1380
1381    /// Add a count aggregation
1382    pub fn with_count(mut self, name: impl Into<String>) -> Self {
1383        self.aggregate_by
1384            .insert(name.into(), serde_json::json!(["Count"]));
1385        self
1386    }
1387
1388    /// Add a sum aggregation
1389    pub fn with_sum(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1390        self.aggregate_by
1391            .insert(name.into(), serde_json::json!(["Sum", field.into()]));
1392        self
1393    }
1394
1395    /// Add an average aggregation
1396    pub fn with_avg(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1397        self.aggregate_by
1398            .insert(name.into(), serde_json::json!(["Avg", field.into()]));
1399        self
1400    }
1401
1402    /// Add a min aggregation
1403    pub fn with_min(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1404        self.aggregate_by
1405            .insert(name.into(), serde_json::json!(["Min", field.into()]));
1406        self
1407    }
1408
1409    /// Add a max aggregation
1410    pub fn with_max(mut self, name: impl Into<String>, field: impl Into<String>) -> Self {
1411        self.aggregate_by
1412            .insert(name.into(), serde_json::json!(["Max", field.into()]));
1413        self
1414    }
1415
1416    /// Set group by fields
1417    pub fn group_by(mut self, fields: Vec<String>) -> Self {
1418        self.group_by = fields;
1419        self
1420    }
1421
1422    /// Add a single group by field
1423    pub fn with_group_by(mut self, field: impl Into<String>) -> Self {
1424        self.group_by.push(field.into());
1425        self
1426    }
1427
1428    /// Set filter for aggregation
1429    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1430        self.filter = Some(filter);
1431        self
1432    }
1433
1434    /// Set maximum number of groups to return
1435    pub fn with_limit(mut self, limit: usize) -> Self {
1436        self.limit = limit;
1437        self
1438    }
1439}
1440
1441impl Default for AggregationRequest {
1442    fn default() -> Self {
1443        Self::new()
1444    }
1445}
1446
1447/// Response for aggregation query
1448#[derive(Debug, Clone, Serialize, Deserialize)]
1449pub struct AggregationResponse {
1450    /// Aggregation results (without grouping)
1451    #[serde(skip_serializing_if = "Option::is_none")]
1452    pub aggregations: Option<HashMap<String, serde_json::Value>>,
1453    /// Grouped aggregation results (with group_by)
1454    #[serde(skip_serializing_if = "Option::is_none")]
1455    pub aggregation_groups: Option<Vec<AggregationGroup>>,
1456}
1457
1458/// Single group in aggregation results
1459#[derive(Debug, Clone, Serialize, Deserialize)]
1460pub struct AggregationGroup {
1461    /// Group key values (flattened into object)
1462    #[serde(flatten)]
1463    pub group_key: HashMap<String, serde_json::Value>,
1464    /// Aggregation results for this group
1465    #[serde(flatten)]
1466    pub aggregations: HashMap<String, serde_json::Value>,
1467}
1468
1469// ============================================================================
1470// Unified Query Types (Turbopuffer-inspired)
1471// ============================================================================
1472
1473/// Vector search method for unified query
1474#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1475pub enum VectorSearchMethod {
1476    /// Approximate Nearest Neighbor (fast, default)
1477    #[default]
1478    ANN,
1479    /// Exact k-Nearest Neighbor (exhaustive, requires filters)
1480    #[serde(rename = "kNN")]
1481    KNN,
1482}
1483
1484/// Sort direction for attribute ordering
1485#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1486#[serde(rename_all = "lowercase")]
1487pub enum SortDirection {
1488    /// Ascending order
1489    Asc,
1490    /// Descending order
1491    #[default]
1492    Desc,
1493}
1494
1495/// Ranking function for unified query API
1496/// Supports vector search (ANN/kNN), full-text BM25, and attribute ordering
1497#[derive(Debug, Clone, Serialize, Deserialize)]
1498#[serde(untagged)]
1499pub enum RankBy {
1500    /// Vector search: uses field, method, and query_vector
1501    VectorSearch {
1502        field: String,
1503        method: VectorSearchMethod,
1504        query_vector: Vec<f32>,
1505    },
1506    /// Full-text BM25 search
1507    FullTextSearch {
1508        field: String,
1509        method: String, // Always "BM25"
1510        query: String,
1511    },
1512    /// Attribute ordering
1513    AttributeOrder {
1514        field: String,
1515        direction: SortDirection,
1516    },
1517    /// Sum of multiple ranking functions
1518    Sum(Vec<RankBy>),
1519    /// Max of multiple ranking functions
1520    Max(Vec<RankBy>),
1521    /// Product with weight
1522    Product { weight: f32, ranking: Box<RankBy> },
1523}
1524
1525impl RankBy {
1526    /// Create a vector search ranking using ANN
1527    pub fn vector_ann(field: impl Into<String>, query_vector: Vec<f32>) -> Self {
1528        RankBy::VectorSearch {
1529            field: field.into(),
1530            method: VectorSearchMethod::ANN,
1531            query_vector,
1532        }
1533    }
1534
1535    /// Create a vector search ranking using ANN on the default "vector" field
1536    pub fn ann(query_vector: Vec<f32>) -> Self {
1537        Self::vector_ann("vector", query_vector)
1538    }
1539
1540    /// Create a vector search ranking using exact kNN
1541    pub fn vector_knn(field: impl Into<String>, query_vector: Vec<f32>) -> Self {
1542        RankBy::VectorSearch {
1543            field: field.into(),
1544            method: VectorSearchMethod::KNN,
1545            query_vector,
1546        }
1547    }
1548
1549    /// Create a vector search ranking using kNN on the default "vector" field
1550    pub fn knn(query_vector: Vec<f32>) -> Self {
1551        Self::vector_knn("vector", query_vector)
1552    }
1553
1554    /// Create a BM25 full-text search ranking
1555    pub fn bm25(field: impl Into<String>, query: impl Into<String>) -> Self {
1556        RankBy::FullTextSearch {
1557            field: field.into(),
1558            method: "BM25".to_string(),
1559            query: query.into(),
1560        }
1561    }
1562
1563    /// Create an attribute ordering ranking (ascending)
1564    pub fn asc(field: impl Into<String>) -> Self {
1565        RankBy::AttributeOrder {
1566            field: field.into(),
1567            direction: SortDirection::Asc,
1568        }
1569    }
1570
1571    /// Create an attribute ordering ranking (descending)
1572    pub fn desc(field: impl Into<String>) -> Self {
1573        RankBy::AttributeOrder {
1574            field: field.into(),
1575            direction: SortDirection::Desc,
1576        }
1577    }
1578
1579    /// Sum multiple ranking functions together
1580    pub fn sum(rankings: Vec<RankBy>) -> Self {
1581        RankBy::Sum(rankings)
1582    }
1583
1584    /// Take the max of multiple ranking functions
1585    pub fn max(rankings: Vec<RankBy>) -> Self {
1586        RankBy::Max(rankings)
1587    }
1588
1589    /// Apply a weight multiplier to a ranking function
1590    pub fn product(weight: f32, ranking: RankBy) -> Self {
1591        RankBy::Product {
1592            weight,
1593            ranking: Box::new(ranking),
1594        }
1595    }
1596}
1597
1598/// Unified query request with flexible ranking options (Turbopuffer-inspired)
1599///
1600/// # Example
1601///
1602/// ```rust
1603/// use dakera_client::UnifiedQueryRequest;
1604///
1605/// // Vector ANN search
1606/// let request = UnifiedQueryRequest::vector_search(vec![0.1, 0.2, 0.3], 10);
1607///
1608/// // Full-text BM25 search
1609/// let request = UnifiedQueryRequest::fulltext_search("content", "hello world", 10);
1610///
1611/// // Custom rank_by with filters
1612/// let request = UnifiedQueryRequest::vector_search(vec![0.1, 0.2, 0.3], 10)
1613///     .with_filter(serde_json::json!({"category": {"$eq": "science"}}));
1614/// ```
1615#[derive(Debug, Clone, Serialize, Deserialize)]
1616pub struct UnifiedQueryRequest {
1617    /// How to rank documents (required)
1618    pub rank_by: serde_json::Value,
1619    /// Number of results to return
1620    #[serde(default = "default_unified_top_k")]
1621    pub top_k: usize,
1622    /// Optional metadata filter
1623    #[serde(skip_serializing_if = "Option::is_none")]
1624    pub filter: Option<serde_json::Value>,
1625    /// Include metadata in results
1626    #[serde(default = "default_true")]
1627    pub include_metadata: bool,
1628    /// Include vectors in results
1629    #[serde(default)]
1630    pub include_vectors: bool,
1631    /// Distance metric for vector search (default: cosine)
1632    #[serde(default)]
1633    pub distance_metric: DistanceMetric,
1634}
1635
1636fn default_unified_top_k() -> usize {
1637    10
1638}
1639
1640impl UnifiedQueryRequest {
1641    /// Create a new unified query request with vector ANN search
1642    pub fn vector_search(query_vector: Vec<f32>, top_k: usize) -> Self {
1643        Self {
1644            rank_by: serde_json::json!(["ANN", query_vector]),
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 vector kNN search
1654    pub fn vector_knn_search(query_vector: Vec<f32>, top_k: usize) -> Self {
1655        Self {
1656            rank_by: serde_json::json!(["kNN", query_vector]),
1657            top_k,
1658            filter: None,
1659            include_metadata: true,
1660            include_vectors: false,
1661            distance_metric: DistanceMetric::default(),
1662        }
1663    }
1664
1665    /// Create a new unified query request with full-text BM25 search
1666    pub fn fulltext_search(
1667        field: impl Into<String>,
1668        query: impl Into<String>,
1669        top_k: usize,
1670    ) -> Self {
1671        Self {
1672            rank_by: serde_json::json!([field.into(), "BM25", query.into()]),
1673            top_k,
1674            filter: None,
1675            include_metadata: true,
1676            include_vectors: false,
1677            distance_metric: DistanceMetric::default(),
1678        }
1679    }
1680
1681    /// Create a new unified query request with attribute ordering
1682    pub fn attribute_order(
1683        field: impl Into<String>,
1684        direction: SortDirection,
1685        top_k: usize,
1686    ) -> Self {
1687        let dir = match direction {
1688            SortDirection::Asc => "asc",
1689            SortDirection::Desc => "desc",
1690        };
1691        Self {
1692            rank_by: serde_json::json!([field.into(), dir]),
1693            top_k,
1694            filter: None,
1695            include_metadata: true,
1696            include_vectors: false,
1697            distance_metric: DistanceMetric::default(),
1698        }
1699    }
1700
1701    /// Create a unified query with a raw rank_by JSON value
1702    pub fn with_rank_by(rank_by: serde_json::Value, top_k: usize) -> Self {
1703        Self {
1704            rank_by,
1705            top_k,
1706            filter: None,
1707            include_metadata: true,
1708            include_vectors: false,
1709            distance_metric: DistanceMetric::default(),
1710        }
1711    }
1712
1713    /// Add a filter to the query
1714    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1715        self.filter = Some(filter);
1716        self
1717    }
1718
1719    /// Set whether to include metadata
1720    pub fn include_metadata(mut self, include: bool) -> Self {
1721        self.include_metadata = include;
1722        self
1723    }
1724
1725    /// Set whether to include vector values
1726    pub fn include_vectors(mut self, include: bool) -> Self {
1727        self.include_vectors = include;
1728        self
1729    }
1730
1731    /// Set the distance metric
1732    pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
1733        self.distance_metric = metric;
1734        self
1735    }
1736
1737    /// Set the number of results to return
1738    pub fn with_top_k(mut self, top_k: usize) -> Self {
1739        self.top_k = top_k;
1740        self
1741    }
1742}
1743
1744/// Single result from unified query
1745#[derive(Debug, Clone, Serialize, Deserialize)]
1746pub struct UnifiedSearchResult {
1747    /// Vector/document ID
1748    pub id: String,
1749    /// Ranking score (distance for vector search, BM25 score for text)
1750    /// Named $dist for Turbopuffer compatibility
1751    #[serde(rename = "$dist", skip_serializing_if = "Option::is_none")]
1752    pub dist: Option<f32>,
1753    /// Metadata if requested
1754    #[serde(skip_serializing_if = "Option::is_none")]
1755    pub metadata: Option<serde_json::Value>,
1756    /// Vector values if requested
1757    #[serde(skip_serializing_if = "Option::is_none")]
1758    pub vector: Option<Vec<f32>>,
1759}
1760
1761/// Unified query response
1762#[derive(Debug, Clone, Serialize, Deserialize)]
1763pub struct UnifiedQueryResponse {
1764    /// Search results ordered by rank_by score
1765    pub results: Vec<UnifiedSearchResult>,
1766    /// Cursor for pagination (if more results available)
1767    #[serde(skip_serializing_if = "Option::is_none")]
1768    pub next_cursor: Option<String>,
1769}
1770
1771// ============================================================================
1772// Query Explain Types
1773// ============================================================================
1774
1775fn default_explain_top_k() -> usize {
1776    10
1777}
1778
1779/// Query type for explain
1780#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1781#[serde(rename_all = "snake_case")]
1782#[derive(Default)]
1783pub enum ExplainQueryType {
1784    /// Vector similarity search
1785    #[default]
1786    VectorSearch,
1787    /// Full-text search
1788    FullTextSearch,
1789    /// Hybrid search combining vector and text
1790    HybridSearch,
1791    /// Multi-vector search with positive/negative vectors
1792    MultiVector,
1793    /// Batch query execution
1794    BatchQuery,
1795}
1796
1797/// Query explain request
1798#[derive(Debug, Clone, Serialize, Deserialize)]
1799pub struct QueryExplainRequest {
1800    /// Type of query to explain
1801    #[serde(default)]
1802    pub query_type: ExplainQueryType,
1803    /// Query vector (for vector searches)
1804    #[serde(skip_serializing_if = "Option::is_none")]
1805    pub vector: Option<Vec<f32>>,
1806    /// Number of results to return
1807    #[serde(default = "default_explain_top_k")]
1808    pub top_k: usize,
1809    /// Optional metadata filter
1810    #[serde(skip_serializing_if = "Option::is_none")]
1811    pub filter: Option<serde_json::Value>,
1812    /// Optional text query for hybrid/fulltext search
1813    #[serde(skip_serializing_if = "Option::is_none")]
1814    pub text_query: Option<String>,
1815    /// Distance metric
1816    #[serde(default = "default_distance_metric")]
1817    pub distance_metric: String,
1818    /// Whether to actually execute the query for actual stats
1819    #[serde(default)]
1820    pub execute: bool,
1821    /// Include verbose output
1822    #[serde(default)]
1823    pub verbose: bool,
1824}
1825
1826fn default_distance_metric() -> String {
1827    "cosine".to_string()
1828}
1829
1830impl QueryExplainRequest {
1831    /// Create a new explain request for a vector search
1832    pub fn vector_search(vector: Vec<f32>, top_k: usize) -> Self {
1833        Self {
1834            query_type: ExplainQueryType::VectorSearch,
1835            vector: Some(vector),
1836            top_k,
1837            filter: None,
1838            text_query: None,
1839            distance_metric: "cosine".to_string(),
1840            execute: false,
1841            verbose: false,
1842        }
1843    }
1844
1845    /// Create a new explain request for a full-text search
1846    pub fn fulltext_search(text_query: impl Into<String>, top_k: usize) -> Self {
1847        Self {
1848            query_type: ExplainQueryType::FullTextSearch,
1849            vector: None,
1850            top_k,
1851            filter: None,
1852            text_query: Some(text_query.into()),
1853            distance_metric: "bm25".to_string(),
1854            execute: false,
1855            verbose: false,
1856        }
1857    }
1858
1859    /// Create a new explain request for a hybrid search
1860    pub fn hybrid_search(vector: Vec<f32>, text_query: impl Into<String>, top_k: usize) -> Self {
1861        Self {
1862            query_type: ExplainQueryType::HybridSearch,
1863            vector: Some(vector),
1864            top_k,
1865            filter: None,
1866            text_query: Some(text_query.into()),
1867            distance_metric: "hybrid".to_string(),
1868            execute: false,
1869            verbose: false,
1870        }
1871    }
1872
1873    /// Add a filter to the explain request
1874    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
1875        self.filter = Some(filter);
1876        self
1877    }
1878
1879    /// Set the distance metric
1880    pub fn with_distance_metric(mut self, metric: impl Into<String>) -> Self {
1881        self.distance_metric = metric.into();
1882        self
1883    }
1884
1885    /// Execute the query to get actual stats
1886    pub fn with_execution(mut self) -> Self {
1887        self.execute = true;
1888        self
1889    }
1890
1891    /// Enable verbose output
1892    pub fn with_verbose(mut self) -> Self {
1893        self.verbose = true;
1894        self
1895    }
1896}
1897
1898/// A stage in query execution
1899#[derive(Debug, Clone, Serialize, Deserialize)]
1900pub struct ExecutionStage {
1901    /// Stage name
1902    pub name: String,
1903    /// Stage description
1904    pub description: String,
1905    /// Stage order (1-based)
1906    pub order: u32,
1907    /// Estimated input rows
1908    pub estimated_input: u64,
1909    /// Estimated output rows
1910    pub estimated_output: u64,
1911    /// Estimated cost for this stage
1912    pub estimated_cost: f64,
1913    /// Stage-specific details
1914    #[serde(default)]
1915    pub details: HashMap<String, serde_json::Value>,
1916}
1917
1918/// Cost estimation
1919#[derive(Debug, Clone, Serialize, Deserialize)]
1920pub struct CostEstimate {
1921    /// Total estimated cost (abstract units)
1922    pub total_cost: f64,
1923    /// Estimated execution time in milliseconds
1924    pub estimated_time_ms: u64,
1925    /// Estimated memory usage in bytes
1926    pub estimated_memory_bytes: u64,
1927    /// Estimated I/O operations
1928    pub estimated_io_ops: u64,
1929    /// Cost breakdown by component
1930    #[serde(default)]
1931    pub cost_breakdown: HashMap<String, f64>,
1932    /// Confidence level (0.0-1.0)
1933    pub confidence: f64,
1934}
1935
1936/// Actual execution statistics (when execute=true)
1937#[derive(Debug, Clone, Serialize, Deserialize)]
1938pub struct ActualStats {
1939    /// Actual execution time in milliseconds
1940    pub execution_time_ms: u64,
1941    /// Actual results returned
1942    pub results_returned: usize,
1943    /// Vectors scanned
1944    pub vectors_scanned: u64,
1945    /// Vectors after filter
1946    pub vectors_after_filter: u64,
1947    /// Index lookups performed
1948    pub index_lookups: u64,
1949    /// Cache hits
1950    pub cache_hits: u64,
1951    /// Cache misses
1952    pub cache_misses: u64,
1953    /// Memory used in bytes
1954    pub memory_used_bytes: u64,
1955}
1956
1957/// Performance recommendation
1958#[derive(Debug, Clone, Serialize, Deserialize)]
1959pub struct Recommendation {
1960    /// Recommendation type
1961    pub recommendation_type: String,
1962    /// Priority (high, medium, low)
1963    pub priority: String,
1964    /// Recommendation description
1965    pub description: String,
1966    /// Expected improvement
1967    pub expected_improvement: String,
1968    /// How to implement
1969    pub implementation: String,
1970}
1971
1972/// Index selection details
1973#[derive(Debug, Clone, Serialize, Deserialize)]
1974pub struct IndexSelection {
1975    /// Index type that will be used
1976    pub index_type: String,
1977    /// Why this index was selected
1978    pub selection_reason: String,
1979    /// Alternative indexes considered
1980    #[serde(default)]
1981    pub alternatives_considered: Vec<IndexAlternative>,
1982    /// Index configuration
1983    #[serde(default)]
1984    pub index_config: HashMap<String, serde_json::Value>,
1985    /// Index statistics
1986    pub index_stats: IndexStatistics,
1987}
1988
1989/// Alternative index that was considered
1990#[derive(Debug, Clone, Serialize, Deserialize)]
1991pub struct IndexAlternative {
1992    /// Index type
1993    pub index_type: String,
1994    /// Why it wasn't selected
1995    pub rejection_reason: String,
1996    /// Estimated cost if this index was used
1997    pub estimated_cost: f64,
1998}
1999
2000/// Index statistics
2001#[derive(Debug, Clone, Serialize, Deserialize)]
2002pub struct IndexStatistics {
2003    /// Total vectors in index
2004    pub vector_count: u64,
2005    /// Vector dimension
2006    pub dimension: usize,
2007    /// Index memory usage (estimated)
2008    pub memory_bytes: u64,
2009    /// Index build time (if available)
2010    #[serde(skip_serializing_if = "Option::is_none")]
2011    pub build_time_ms: Option<u64>,
2012    /// Last updated timestamp
2013    #[serde(skip_serializing_if = "Option::is_none")]
2014    pub last_updated: Option<u64>,
2015}
2016
2017/// Query parameters for reference
2018#[derive(Debug, Clone, Serialize, Deserialize)]
2019pub struct QueryParams {
2020    /// Number of results requested
2021    pub top_k: usize,
2022    /// Whether a filter was applied
2023    pub has_filter: bool,
2024    /// Filter complexity level
2025    pub filter_complexity: String,
2026    /// Vector dimension (if applicable)
2027    #[serde(skip_serializing_if = "Option::is_none")]
2028    pub vector_dimension: Option<usize>,
2029    /// Distance metric used
2030    pub distance_metric: String,
2031    /// Text query length (if applicable)
2032    #[serde(skip_serializing_if = "Option::is_none")]
2033    pub text_query_length: Option<usize>,
2034}
2035
2036/// Query explain response - detailed execution plan
2037#[derive(Debug, Clone, Serialize, Deserialize)]
2038pub struct QueryExplainResponse {
2039    /// Query type being explained
2040    pub query_type: ExplainQueryType,
2041    /// Namespace being queried
2042    pub namespace: String,
2043    /// Index selection information
2044    pub index_selection: IndexSelection,
2045    /// Query execution stages
2046    pub stages: Vec<ExecutionStage>,
2047    /// Cost estimates
2048    pub cost_estimate: CostEstimate,
2049    /// Actual execution stats (if execute=true)
2050    #[serde(skip_serializing_if = "Option::is_none")]
2051    pub actual_stats: Option<ActualStats>,
2052    /// Performance recommendations
2053    #[serde(default)]
2054    pub recommendations: Vec<Recommendation>,
2055    /// Query plan summary
2056    pub summary: String,
2057    /// Raw query parameters
2058    pub query_params: QueryParams,
2059}
2060
2061// ============================================================================
2062// Text Auto-Embedding Types
2063// ============================================================================
2064
2065/// Supported embedding models for text-based operations.
2066#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
2067#[serde(rename_all = "kebab-case")]
2068pub enum EmbeddingModel {
2069    /// BGE-large — Best quality, server default (1024 dimensions)
2070    #[default]
2071    BgeLarge,
2072    /// MiniLM-L6 — Fast, good quality (384 dimensions)
2073    Minilm,
2074    /// BGE-small — Balanced performance (384 dimensions)
2075    BgeSmall,
2076    /// E5-small — High quality (384 dimensions)
2077    E5Small,
2078}
2079
2080/// A text document to upsert with automatic embedding generation.
2081#[derive(Debug, Clone, Serialize, Deserialize)]
2082pub struct TextDocument {
2083    /// Unique identifier for the document.
2084    pub id: String,
2085    /// Raw text content to be embedded.
2086    pub text: String,
2087    /// Optional metadata for the document.
2088    #[serde(skip_serializing_if = "Option::is_none")]
2089    pub metadata: Option<HashMap<String, serde_json::Value>>,
2090    /// Optional TTL in seconds.
2091    #[serde(skip_serializing_if = "Option::is_none")]
2092    pub ttl_seconds: Option<u64>,
2093}
2094
2095impl TextDocument {
2096    /// Create a new text document with the given ID and text.
2097    pub fn new(id: impl Into<String>, text: impl Into<String>) -> Self {
2098        Self {
2099            id: id.into(),
2100            text: text.into(),
2101            metadata: None,
2102            ttl_seconds: None,
2103        }
2104    }
2105
2106    /// Add metadata to this document.
2107    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
2108        self.metadata = Some(metadata);
2109        self
2110    }
2111
2112    /// Set a TTL on this document.
2113    pub fn with_ttl(mut self, ttl_seconds: u64) -> Self {
2114        self.ttl_seconds = Some(ttl_seconds);
2115        self
2116    }
2117}
2118
2119/// Request to upsert text documents with automatic embedding.
2120#[derive(Debug, Clone, Serialize, Deserialize)]
2121pub struct UpsertTextRequest {
2122    /// Documents to upsert.
2123    pub documents: Vec<TextDocument>,
2124    /// Embedding model to use (default: minilm).
2125    #[serde(skip_serializing_if = "Option::is_none")]
2126    pub model: Option<EmbeddingModel>,
2127}
2128
2129impl UpsertTextRequest {
2130    /// Create a new upsert-text request.
2131    pub fn new(documents: Vec<TextDocument>) -> Self {
2132        Self {
2133            documents,
2134            model: None,
2135        }
2136    }
2137
2138    /// Set the embedding model.
2139    pub fn with_model(mut self, model: EmbeddingModel) -> Self {
2140        self.model = Some(model);
2141        self
2142    }
2143}
2144
2145/// Response from a text upsert operation.
2146#[derive(Debug, Clone, Serialize, Deserialize)]
2147pub struct TextUpsertResponse {
2148    /// Number of documents upserted.
2149    pub upserted_count: u64,
2150    /// Approximate number of tokens processed.
2151    pub tokens_processed: u64,
2152    /// Embedding model used.
2153    pub model: EmbeddingModel,
2154    /// Time spent generating embeddings in milliseconds.
2155    pub embedding_time_ms: u64,
2156}
2157
2158/// A single text search result.
2159#[derive(Debug, Clone, Serialize, Deserialize)]
2160pub struct TextSearchResult {
2161    /// Document ID.
2162    pub id: String,
2163    /// Similarity score.
2164    pub score: f32,
2165    /// Original text (if `include_text` was true).
2166    #[serde(skip_serializing_if = "Option::is_none")]
2167    pub text: Option<String>,
2168    /// Document metadata.
2169    #[serde(skip_serializing_if = "Option::is_none")]
2170    pub metadata: Option<HashMap<String, serde_json::Value>>,
2171    /// Vector values (if `include_vectors` was true).
2172    #[serde(skip_serializing_if = "Option::is_none")]
2173    pub vector: Option<Vec<f32>>,
2174}
2175
2176/// Request to query using natural language text with automatic embedding.
2177#[derive(Debug, Clone, Serialize, Deserialize)]
2178pub struct QueryTextRequest {
2179    /// Query text.
2180    pub text: String,
2181    /// Number of results to return.
2182    pub top_k: u32,
2183    /// Optional metadata filter.
2184    #[serde(skip_serializing_if = "Option::is_none")]
2185    pub filter: Option<serde_json::Value>,
2186    /// Whether to include the original text in results.
2187    pub include_text: bool,
2188    /// Whether to include vectors in results.
2189    pub include_vectors: bool,
2190    /// Embedding model to use (default: minilm).
2191    #[serde(skip_serializing_if = "Option::is_none")]
2192    pub model: Option<EmbeddingModel>,
2193}
2194
2195impl QueryTextRequest {
2196    /// Create a new text query request.
2197    pub fn new(text: impl Into<String>, top_k: u32) -> Self {
2198        Self {
2199            text: text.into(),
2200            top_k,
2201            filter: None,
2202            include_text: true,
2203            include_vectors: false,
2204            model: None,
2205        }
2206    }
2207
2208    /// Add a metadata filter.
2209    pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
2210        self.filter = Some(filter);
2211        self
2212    }
2213
2214    /// Set whether to include the original text in results.
2215    pub fn include_text(mut self, include: bool) -> Self {
2216        self.include_text = include;
2217        self
2218    }
2219
2220    /// Set whether to include vectors in results.
2221    pub fn include_vectors(mut self, include: bool) -> Self {
2222        self.include_vectors = include;
2223        self
2224    }
2225
2226    /// Set the embedding model.
2227    pub fn with_model(mut self, model: EmbeddingModel) -> Self {
2228        self.model = Some(model);
2229        self
2230    }
2231}
2232
2233/// Response from a text query operation.
2234#[derive(Debug, Clone, Serialize, Deserialize)]
2235pub struct TextQueryResponse {
2236    /// Search results.
2237    pub results: Vec<TextSearchResult>,
2238    /// Embedding model used.
2239    pub model: EmbeddingModel,
2240    /// Time spent generating the query embedding in milliseconds.
2241    pub embedding_time_ms: u64,
2242    /// Time spent searching in milliseconds.
2243    pub search_time_ms: u64,
2244}
2245
2246/// Request to execute multiple text queries with automatic embedding in a single call.
2247#[derive(Debug, Clone, Serialize, Deserialize)]
2248pub struct BatchQueryTextRequest {
2249    /// Text queries.
2250    pub queries: Vec<String>,
2251    /// Number of results per query.
2252    pub top_k: u32,
2253    /// Optional metadata filter applied to all queries.
2254    #[serde(skip_serializing_if = "Option::is_none")]
2255    pub filter: Option<serde_json::Value>,
2256    /// Whether to include vectors in results.
2257    pub include_vectors: bool,
2258    /// Embedding model to use (default: minilm).
2259    #[serde(skip_serializing_if = "Option::is_none")]
2260    pub model: Option<EmbeddingModel>,
2261}
2262
2263impl BatchQueryTextRequest {
2264    /// Create a new batch text query request.
2265    pub fn new(queries: Vec<String>, top_k: u32) -> Self {
2266        Self {
2267            queries,
2268            top_k,
2269            filter: None,
2270            include_vectors: false,
2271            model: None,
2272        }
2273    }
2274}
2275
2276/// Response from a batch text query operation.
2277#[derive(Debug, Clone, Serialize, Deserialize)]
2278pub struct BatchQueryTextResponse {
2279    /// Results for each query (in the same order as the request).
2280    pub results: Vec<Vec<TextSearchResult>>,
2281    /// Embedding model used.
2282    pub model: EmbeddingModel,
2283    /// Time spent generating all embeddings in milliseconds.
2284    pub embedding_time_ms: u64,
2285    /// Time spent on all searches in milliseconds.
2286    pub search_time_ms: u64,
2287}
2288
2289// ============================================================================
2290// Fetch by ID Types
2291// ============================================================================
2292
2293/// Request to fetch vectors by their IDs.
2294#[derive(Debug, Clone, Serialize, Deserialize)]
2295pub struct FetchRequest {
2296    /// IDs of vectors to fetch.
2297    pub ids: Vec<String>,
2298    /// Whether to include vector values.
2299    pub include_values: bool,
2300    /// Whether to include metadata.
2301    pub include_metadata: bool,
2302}
2303
2304impl FetchRequest {
2305    /// Create a new fetch request.
2306    pub fn new(ids: Vec<String>) -> Self {
2307        Self {
2308            ids,
2309            include_values: true,
2310            include_metadata: true,
2311        }
2312    }
2313}
2314
2315/// Response from a fetch-by-ID operation.
2316#[derive(Debug, Clone, Serialize, Deserialize)]
2317pub struct FetchResponse {
2318    /// Fetched vectors.
2319    pub vectors: Vec<Vector>,
2320}
2321
2322// ============================================================================
2323// Namespace Management Types
2324// ============================================================================
2325
2326/// Request to create a new namespace.
2327#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2328pub struct CreateNamespaceRequest {
2329    /// Vector dimensions (inferred from first upsert if omitted).
2330    #[serde(rename = "dimension", skip_serializing_if = "Option::is_none")]
2331    pub dimensions: Option<u32>,
2332    /// Index type (e.g. "hnsw", "flat").
2333    #[serde(skip_serializing_if = "Option::is_none")]
2334    pub index_type: Option<String>,
2335    /// Arbitrary namespace metadata.
2336    #[serde(skip_serializing_if = "Option::is_none")]
2337    pub metadata: Option<HashMap<String, serde_json::Value>>,
2338}
2339
2340impl CreateNamespaceRequest {
2341    /// Create a minimal request (server picks sensible defaults).
2342    pub fn new() -> Self {
2343        Self::default()
2344    }
2345
2346    /// Set the vector dimensions.
2347    pub fn with_dimensions(mut self, dimensions: u32) -> Self {
2348        self.dimensions = Some(dimensions);
2349        self
2350    }
2351
2352    /// Set the index type.
2353    pub fn with_index_type(mut self, index_type: impl Into<String>) -> Self {
2354        self.index_type = Some(index_type.into());
2355        self
2356    }
2357}
2358
2359/// Request body for `PUT /v1/namespaces/:namespace` — upsert semantics (v0.6.0).
2360///
2361/// Creates the namespace if it does not exist, or updates its configuration
2362/// if it already exists.  Requires `Scope::Write`.
2363#[derive(Debug, Clone, Serialize, Deserialize)]
2364pub struct ConfigureNamespaceRequest {
2365    /// Vector dimension.  Required on first creation; must match on subsequent calls.
2366    pub dimension: usize,
2367    /// Distance metric (defaults to cosine when omitted).
2368    #[serde(skip_serializing_if = "Option::is_none")]
2369    pub distance: Option<DistanceMetric>,
2370}
2371
2372impl ConfigureNamespaceRequest {
2373    /// Create a new configure-namespace request with the given dimension.
2374    pub fn new(dimension: usize) -> Self {
2375        Self {
2376            dimension,
2377            distance: None,
2378        }
2379    }
2380
2381    /// Set the distance metric.
2382    pub fn with_distance(mut self, distance: DistanceMetric) -> Self {
2383        self.distance = Some(distance);
2384        self
2385    }
2386}
2387
2388/// Response from `PUT /v1/namespaces/:namespace`.
2389#[derive(Debug, Clone, Serialize, Deserialize)]
2390pub struct ConfigureNamespaceResponse {
2391    /// Namespace name.
2392    pub namespace: String,
2393    /// Vector dimension.
2394    pub dimension: usize,
2395    /// Distance metric in use.
2396    pub distance: DistanceMetric,
2397    /// `true` if the namespace was newly created; `false` if it already existed.
2398    pub created: bool,
2399}
2400
2401// ============================================================================
2402// Memory Knowledge Graph Types (CE-5 / SDK-9)
2403// ============================================================================
2404
2405/// Edge type for memory knowledge graph relationships (CE-5).
2406#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2407#[serde(rename_all = "snake_case")]
2408pub enum EdgeType {
2409    /// Cosine similarity ≥ 0.85 — two memories are semantically similar.
2410    RelatedTo,
2411    /// Both memories reference the same named entity (CE-4 tags).
2412    SharesEntity,
2413    /// Temporal ordering — source was created before target.
2414    Precedes,
2415    /// Explicit user/agent-created link.
2416    #[default]
2417    LinkedBy,
2418}
2419
2420/// A directed edge in the memory knowledge graph.
2421#[derive(Debug, Clone, Serialize, Deserialize)]
2422pub struct GraphEdge {
2423    /// Unique edge identifier.
2424    pub id: String,
2425    /// Source memory ID.
2426    pub source_id: String,
2427    /// Target memory ID.
2428    pub target_id: String,
2429    /// Relationship type between the two memories.
2430    pub edge_type: EdgeType,
2431    /// Edge weight (0.0–1.0). For `RelatedTo` this is the cosine similarity score.
2432    pub weight: f64,
2433    /// Unix timestamp of edge creation.
2434    pub created_at: i64,
2435}
2436
2437/// A node (memory) in the knowledge graph traversal result.
2438#[derive(Debug, Clone, Serialize, Deserialize)]
2439pub struct GraphNode {
2440    /// Memory identifier.
2441    pub memory_id: String,
2442    /// First 200 characters of memory content.
2443    pub content_preview: String,
2444    /// Memory importance score.
2445    pub importance: f64,
2446    /// Traversal depth from the root node (root = 0).
2447    pub depth: u32,
2448}
2449
2450/// Graph traversal result from `GET /v1/memories/{id}/graph`.
2451#[derive(Debug, Clone, Serialize, Deserialize)]
2452pub struct MemoryGraph {
2453    /// The root memory ID from which traversal started.
2454    pub root_id: String,
2455    /// Maximum traversal depth used.
2456    pub depth: u32,
2457    /// All memory nodes reachable within the requested depth.
2458    pub nodes: Vec<GraphNode>,
2459    /// All edges connecting the returned nodes.
2460    pub edges: Vec<GraphEdge>,
2461}
2462
2463/// Shortest path between two memories from `GET /v1/memories/{id}/path`.
2464#[derive(Debug, Clone, Serialize, Deserialize)]
2465pub struct GraphPath {
2466    /// Starting memory ID.
2467    pub source_id: String,
2468    /// Destination memory ID.
2469    pub target_id: String,
2470    /// Ordered list of memory IDs from source to target (inclusive).
2471    pub path: Vec<String>,
2472    /// Number of edges traversed (`path.len() - 1`). `-1` if no path exists.
2473    pub hops: i32,
2474    /// Edges along the path, in traversal order.
2475    pub edges: Vec<GraphEdge>,
2476}
2477
2478/// Request body for `POST /v1/memories/{id}/links`.
2479#[derive(Debug, Clone, Serialize, Deserialize)]
2480pub struct GraphLinkRequest {
2481    /// Target memory ID to link to.
2482    pub target_id: String,
2483    /// Edge type — must be `LinkedBy` for explicit links.
2484    pub edge_type: EdgeType,
2485}
2486
2487/// Response from `POST /v1/memories/{id}/links`.
2488#[derive(Debug, Clone, Serialize, Deserialize)]
2489pub struct GraphLinkResponse {
2490    /// The newly created edge.
2491    pub edge: GraphEdge,
2492}
2493
2494/// Agent graph export from `GET /v1/agents/{id}/graph/export`.
2495#[derive(Debug, Clone, Serialize, Deserialize)]
2496pub struct GraphExport {
2497    /// Agent whose graph was exported.
2498    pub agent_id: String,
2499    /// Export format: `json`, `graphml`, or `csv`.
2500    pub format: String,
2501    /// Serialised graph in the requested format.
2502    pub data: String,
2503    /// Total number of memory nodes in the export.
2504    pub node_count: u64,
2505    /// Total number of edges in the export.
2506    pub edge_count: u64,
2507}
2508
2509/// Options for [`DakeraClient::memory_graph`].
2510#[derive(Debug, Clone, Default)]
2511pub struct GraphOptions {
2512    /// Maximum traversal depth (default: 1, max: 3).
2513    pub depth: Option<u32>,
2514    /// Filter by edge types. `None` returns all types.
2515    pub types: Option<Vec<EdgeType>>,
2516}
2517
2518impl GraphOptions {
2519    /// Create default options.
2520    pub fn new() -> Self {
2521        Self::default()
2522    }
2523
2524    /// Set traversal depth.
2525    pub fn depth(mut self, depth: u32) -> Self {
2526        self.depth = Some(depth);
2527        self
2528    }
2529
2530    /// Filter by edge types.
2531    pub fn types(mut self, types: Vec<EdgeType>) -> Self {
2532        self.types = Some(types);
2533        self
2534    }
2535}
2536
2537// ============================================================================
2538// CE-4: GLiNER Entity Extraction Types
2539// ============================================================================
2540
2541/// Configuration for namespace-level entity extraction (CE-4).
2542#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2543pub struct NamespaceNerConfig {
2544    pub extract_entities: bool,
2545    #[serde(skip_serializing_if = "Option::is_none")]
2546    pub entity_types: Option<Vec<String>>,
2547}
2548
2549/// A single extracted entity from GLiNER or rule-based pipeline.
2550#[derive(Debug, Clone, Serialize, Deserialize)]
2551pub struct ExtractedEntity {
2552    pub entity_type: String,
2553    pub value: String,
2554    pub score: f64,
2555}
2556
2557/// Response from POST /v1/memories/extract
2558#[derive(Debug, Clone, Serialize, Deserialize)]
2559pub struct EntityExtractionResponse {
2560    pub entities: Vec<ExtractedEntity>,
2561}
2562
2563/// Response from GET /v1/memory/entities/:id
2564#[derive(Debug, Clone, Serialize, Deserialize)]
2565pub struct MemoryEntitiesResponse {
2566    pub memory_id: String,
2567    pub entities: Vec<ExtractedEntity>,
2568}
2569
2570// ============================================================================
2571// Memory Feedback Loop (INT-1)
2572// ============================================================================
2573
2574/// Feedback signal for memory active learning (INT-1).
2575///
2576/// - `upvote`: Boost importance ×1.15, capped at 1.0.
2577/// - `downvote`: Penalise importance ×0.85, floor 0.0.
2578/// - `flag`: Mark as irrelevant — sets `decay_flag=true`, no immediate importance change.
2579/// - `positive`: Backward-compatible alias for `upvote`.
2580/// - `negative`: Backward-compatible alias for `downvote`.
2581#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2582#[serde(rename_all = "lowercase")]
2583pub enum FeedbackSignal {
2584    Upvote,
2585    Downvote,
2586    Flag,
2587    Positive,
2588    Negative,
2589}
2590
2591/// A single recorded feedback event stored in memory metadata (INT-1).
2592#[derive(Debug, Clone, Serialize, Deserialize)]
2593pub struct FeedbackHistoryEntry {
2594    pub signal: FeedbackSignal,
2595    /// Unix timestamp (seconds) when feedback was submitted.
2596    pub timestamp: u64,
2597    pub old_importance: f32,
2598    pub new_importance: f32,
2599}
2600
2601/// Request body for `POST /v1/memories/:id/feedback` (INT-1).
2602#[derive(Debug, Clone, Serialize, Deserialize)]
2603pub struct MemoryFeedbackBody {
2604    pub agent_id: String,
2605    pub signal: FeedbackSignal,
2606}
2607
2608/// Request body for `PATCH /v1/memories/:id/importance` (INT-1).
2609#[derive(Debug, Clone, Serialize, Deserialize)]
2610pub struct MemoryImportancePatch {
2611    pub agent_id: String,
2612    pub importance: f32,
2613}
2614
2615/// Response from `POST /v1/memories/:id/feedback` and `PATCH /v1/memories/:id/importance` (INT-1).
2616#[derive(Debug, Clone, Serialize, Deserialize)]
2617pub struct FeedbackResponse {
2618    pub memory_id: String,
2619    /// New importance score after the feedback was applied (0.0–1.0).
2620    pub new_importance: f32,
2621    pub signal: FeedbackSignal,
2622}
2623
2624/// Response from `GET /v1/memories/:id/feedback` (INT-1).
2625#[derive(Debug, Clone, Serialize, Deserialize)]
2626pub struct FeedbackHistoryResponse {
2627    pub memory_id: String,
2628    /// Ordered list of feedback events (oldest first, capped at 100).
2629    pub entries: Vec<FeedbackHistoryEntry>,
2630}
2631
2632/// Response from `GET /v1/agents/:id/feedback/summary` (INT-1).
2633#[derive(Debug, Clone, Serialize, Deserialize)]
2634pub struct AgentFeedbackSummary {
2635    pub agent_id: String,
2636    pub upvotes: u64,
2637    pub downvotes: u64,
2638    pub flags: u64,
2639    pub total_feedback: u64,
2640    /// Weighted-average importance across all non-expired memories (0.0–1.0).
2641    pub health_score: f32,
2642}
2643
2644/// Response from `GET /v1/feedback/health` (INT-1).
2645#[derive(Debug, Clone, Serialize, Deserialize)]
2646pub struct FeedbackHealthResponse {
2647    pub agent_id: String,
2648    /// Mean importance of all non-expired memories (0.0–1.0). Higher = healthier.
2649    pub health_score: f32,
2650    pub memory_count: usize,
2651    pub avg_importance: f32,
2652}
2653
2654// ============================================================================
2655// ODE-2: GLiNER Entity Extraction (dakera-ode sidecar)
2656// ============================================================================
2657
2658/// A single entity extracted by the GLiNER model (ODE-2).
2659#[derive(Debug, Clone, Serialize, Deserialize)]
2660pub struct OdeEntity {
2661    /// Span text as it appears in the input.
2662    pub text: String,
2663    /// Entity type label (e.g. `"person"`, `"organization"`).
2664    pub label: String,
2665    /// Start character offset (inclusive) within the input text.
2666    pub start: usize,
2667    /// End character offset (exclusive) within the input text.
2668    pub end: usize,
2669    /// Confidence score in the range [0, 1].
2670    pub score: f32,
2671}
2672
2673/// Request body for `POST /ode/extract` (ODE-2).
2674#[derive(Debug, Clone, Serialize, Deserialize)]
2675pub struct ExtractEntitiesRequest {
2676    /// The text to extract entities from.
2677    pub content: String,
2678    /// Agent context for the extraction.
2679    pub agent_id: String,
2680    /// Optional memory ID to associate with the extraction.
2681    #[serde(skip_serializing_if = "Option::is_none")]
2682    pub memory_id: Option<String>,
2683    /// Optional list of entity type labels to extract.
2684    /// When omitted the ODE sidecar uses its default set.
2685    #[serde(skip_serializing_if = "Option::is_none")]
2686    pub entity_types: Option<Vec<String>>,
2687}
2688
2689/// Response from `POST /ode/extract` on the ODE sidecar (ODE-2).
2690#[derive(Debug, Clone, Serialize, Deserialize)]
2691pub struct ExtractEntitiesResponse {
2692    /// Extracted entities ordered by their start offset.
2693    pub entities: Vec<OdeEntity>,
2694    /// GLiNER model variant used for extraction.
2695    pub model: String,
2696    /// Wall-clock time taken by the ODE sidecar in milliseconds.
2697    pub processing_time_ms: u64,
2698}
2699
2700// ============================================================================
2701// KG-2: Graph Query & Export — response types
2702// ============================================================================
2703
2704/// Response from `GET /v1/knowledge/query` (KG-2).
2705#[derive(Debug, Clone, Serialize, Deserialize)]
2706pub struct KgQueryResponse {
2707    /// Agent whose graph was queried.
2708    pub agent_id: String,
2709    /// Number of unique memory node IDs referenced by the returned edges.
2710    pub node_count: usize,
2711    /// Number of edges returned.
2712    pub edge_count: usize,
2713    /// Matching edges, up to `limit`.
2714    pub edges: Vec<GraphEdge>,
2715}
2716
2717/// Response from `GET /v1/knowledge/path` (KG-2).
2718#[derive(Debug, Clone, Serialize, Deserialize)]
2719pub struct KgPathResponse {
2720    /// Agent whose graph was traversed.
2721    pub agent_id: String,
2722    /// Source memory ID.
2723    pub from_id: String,
2724    /// Target memory ID.
2725    pub to_id: String,
2726    /// Number of edges in the shortest path (0 if source == target).
2727    pub hop_count: usize,
2728    /// Ordered list of memory IDs from source to target (inclusive).
2729    pub path: Vec<String>,
2730}
2731
2732/// Response from `GET /v1/knowledge/export` with `format=json` (KG-2).
2733#[derive(Debug, Clone, Serialize, Deserialize)]
2734pub struct KgExportResponse {
2735    /// Agent whose graph was exported.
2736    pub agent_id: String,
2737    /// Export format used (`"json"` when this struct is deserialized).
2738    pub format: String,
2739    /// Total number of unique memory node IDs in the export.
2740    pub node_count: usize,
2741    /// Total number of edges in the export.
2742    pub edge_count: usize,
2743    /// All graph edges for the agent.
2744    pub edges: Vec<GraphEdge>,
2745}
2746
2747// ============================================================================
2748// COG-1: Cognitive Memory Lifecycle — per-namespace memory policy
2749// ============================================================================
2750
2751/// Per-namespace memory lifecycle policy (COG-1).
2752///
2753/// Controls type-specific TTLs, decay curves, and spaced repetition behaviour.
2754/// All fields have sensible defaults; only override what you need.
2755///
2756/// Used by [`DakeraClient::get_memory_policy`] and
2757/// [`DakeraClient::set_memory_policy`].
2758#[derive(Debug, Clone, Serialize, Deserialize)]
2759pub struct MemoryPolicy {
2760    // Differential TTLs ------------------------------------------------------
2761    /// Default TTL for `working` memories in seconds (default: 14 400 = 4 h).
2762    #[serde(skip_serializing_if = "Option::is_none")]
2763    pub working_ttl_seconds: Option<u64>,
2764    /// Default TTL for `episodic` memories in seconds (default: 2 592 000 = 30 d).
2765    #[serde(skip_serializing_if = "Option::is_none")]
2766    pub episodic_ttl_seconds: Option<u64>,
2767    /// Default TTL for `semantic` memories in seconds (default: 31 536 000 = 365 d).
2768    #[serde(skip_serializing_if = "Option::is_none")]
2769    pub semantic_ttl_seconds: Option<u64>,
2770    /// Default TTL for `procedural` memories in seconds (default: 63 072 000 = 730 d).
2771    #[serde(skip_serializing_if = "Option::is_none")]
2772    pub procedural_ttl_seconds: Option<u64>,
2773
2774    // Decay curves ------------------------------------------------------------
2775    /// Decay strategy for `working` memories (default: `"exponential"`).
2776    #[serde(skip_serializing_if = "Option::is_none")]
2777    pub working_decay: Option<String>,
2778    /// Decay strategy for `episodic` memories (default: `"power_law"`).
2779    #[serde(skip_serializing_if = "Option::is_none")]
2780    pub episodic_decay: Option<String>,
2781    /// Decay strategy for `semantic` memories (default: `"logarithmic"`).
2782    #[serde(skip_serializing_if = "Option::is_none")]
2783    pub semantic_decay: Option<String>,
2784    /// Decay strategy for `procedural` memories (default: `"flat"` — no decay).
2785    #[serde(skip_serializing_if = "Option::is_none")]
2786    pub procedural_decay: Option<String>,
2787
2788    // Spaced repetition -------------------------------------------------------
2789    /// TTL extension multiplier per recall hit (default: 1.0; set to 0.0 to disable).
2790    /// Extension = `access_count × sr_factor × sr_base_interval_seconds`.
2791    #[serde(skip_serializing_if = "Option::is_none")]
2792    pub spaced_repetition_factor: Option<f64>,
2793    /// Base interval in seconds for spaced repetition TTL extension (default: 86 400 = 1 d).
2794    #[serde(skip_serializing_if = "Option::is_none")]
2795    pub spaced_repetition_base_interval_seconds: Option<u64>,
2796
2797    // Proactive consolidation (COG-3) -----------------------------------------
2798    /// Enable background DBSCAN deduplication for this namespace (default: `false`).
2799    /// When `true` the server merges semantically near-duplicate memories every
2800    /// [`consolidation_interval_hours`](Self::consolidation_interval_hours) hours.
2801    #[serde(skip_serializing_if = "Option::is_none")]
2802    pub consolidation_enabled: Option<bool>,
2803    /// DBSCAN epsilon — cosine-similarity threshold to consider two memories
2804    /// duplicates (default: `0.92`; higher = only merge very close neighbours).
2805    #[serde(skip_serializing_if = "Option::is_none")]
2806    pub consolidation_threshold: Option<f32>,
2807    /// How often (in hours) the background consolidation job runs (default: `24`).
2808    #[serde(skip_serializing_if = "Option::is_none")]
2809    pub consolidation_interval_hours: Option<u32>,
2810    /// **Read-only.** Lifetime count of memories merged by the consolidation engine.
2811    /// The server manages this field; any value sent via [`set_memory_policy`] is ignored.
2812    ///
2813    /// [`set_memory_policy`]: crate::DakeraClient::set_memory_policy
2814    #[serde(skip_serializing_if = "Option::is_none")]
2815    pub consolidated_count: Option<u64>,
2816
2817    // Per-namespace rate limiting (SEC-5) -----------------------------------------
2818    /// Enable per-namespace store/recall rate limiting (default: `false`).
2819    #[serde(skip_serializing_if = "Option::is_none")]
2820    pub rate_limit_enabled: Option<bool>,
2821    /// Max store operations per minute for this namespace. `None` = unlimited (default).
2822    #[serde(skip_serializing_if = "Option::is_none")]
2823    pub rate_limit_stores_per_minute: Option<u32>,
2824    /// Max recall operations per minute for this namespace. `None` = unlimited (default).
2825    #[serde(skip_serializing_if = "Option::is_none")]
2826    pub rate_limit_recalls_per_minute: Option<u32>,
2827
2828    // Store-time deduplication (CE-10) -----------------------------------------
2829    /// Deduplicate against existing memories at store time (CE-10, default: `false`).
2830    ///
2831    /// When `true` the server computes a similarity check before persisting a new
2832    /// memory and drops it if a near-duplicate already exists (threshold controlled
2833    /// by [`dedup_threshold`](Self::dedup_threshold)).
2834    #[serde(skip_serializing_if = "Option::is_none")]
2835    pub dedup_on_store: Option<bool>,
2836    /// Cosine-similarity threshold for store-time deduplication (default: `0.92`).
2837    ///
2838    /// Memories with similarity ≥ this value are considered duplicates and the
2839    /// incoming memory is dropped. Only active when `dedup_on_store` is `true`.
2840    #[serde(skip_serializing_if = "Option::is_none")]
2841    pub dedup_threshold: Option<f32>,
2842}
2843
2844impl Default for MemoryPolicy {
2845    fn default() -> Self {
2846        Self {
2847            working_ttl_seconds: Some(14_400),
2848            episodic_ttl_seconds: Some(2_592_000),
2849            semantic_ttl_seconds: Some(31_536_000),
2850            procedural_ttl_seconds: Some(63_072_000),
2851            working_decay: Some("exponential".to_string()),
2852            episodic_decay: Some("power_law".to_string()),
2853            semantic_decay: Some("logarithmic".to_string()),
2854            procedural_decay: Some("flat".to_string()),
2855            spaced_repetition_factor: Some(1.0),
2856            spaced_repetition_base_interval_seconds: Some(86_400),
2857            consolidation_enabled: Some(false),
2858            consolidation_threshold: Some(0.92),
2859            consolidation_interval_hours: Some(24),
2860            consolidated_count: Some(0),
2861            rate_limit_enabled: Some(false),
2862            rate_limit_stores_per_minute: None,
2863            rate_limit_recalls_per_minute: None,
2864            dedup_on_store: Some(false),
2865            dedup_threshold: Some(0.92),
2866        }
2867    }
2868}
2869
2870// =============================================================================
2871// Engine Parity — Vector Bulk Ops, Agent Consolidation, Namespace Config
2872// =============================================================================
2873
2874/// Request for `POST /v1/namespaces/{ns}/vectors/bulk-update`.
2875#[derive(Debug, Clone, Serialize, Deserialize)]
2876pub struct BulkUpdateRequest {
2877    pub filter: serde_json::Value,
2878    pub update: serde_json::Value,
2879}
2880
2881/// Response from `POST /v1/namespaces/{ns}/vectors/bulk-update`.
2882#[derive(Debug, Clone, Serialize, Deserialize)]
2883pub struct BulkUpdateResponse {
2884    pub updated: u64,
2885    pub failed: u64,
2886    pub errors: Vec<String>,
2887}
2888
2889/// Request for `POST /v1/namespaces/{ns}/vectors/bulk-delete`.
2890#[derive(Debug, Clone, Serialize, Deserialize)]
2891pub struct BulkDeleteRequest {
2892    pub filter: serde_json::Value,
2893}
2894
2895/// Response from `POST /v1/namespaces/{ns}/vectors/bulk-delete`.
2896#[derive(Debug, Clone, Serialize, Deserialize)]
2897pub struct BulkDeleteResponse {
2898    pub deleted: u64,
2899    pub failed: u64,
2900    pub errors: Vec<String>,
2901}
2902
2903/// Request for `POST /v1/namespaces/{ns}/vectors/count`.
2904#[derive(Debug, Clone, Serialize, Deserialize)]
2905pub struct CountVectorsRequest {
2906    #[serde(skip_serializing_if = "Option::is_none")]
2907    pub filter: Option<serde_json::Value>,
2908}
2909
2910/// Response from `POST /v1/namespaces/{ns}/vectors/count`.
2911#[derive(Debug, Clone, Serialize, Deserialize)]
2912pub struct CountVectorsResponse {
2913    pub count: u64,
2914    pub namespace: String,
2915}
2916
2917/// Response from `POST /v1/agents/{agent_id}/consolidate`.
2918#[derive(Debug, Clone, Serialize, Deserialize)]
2919pub struct AgentConsolidateResponse {
2920    pub agent_id: String,
2921    pub memories_scanned: u64,
2922    pub clusters_found: u64,
2923    pub memories_deprecated: u64,
2924    pub anchor_ids: Vec<String>,
2925    pub deprecated_ids: Vec<String>,
2926    #[serde(skip_serializing_if = "Option::is_none")]
2927    pub skipped: Option<bool>,
2928    #[serde(skip_serializing_if = "Option::is_none")]
2929    pub reason: Option<String>,
2930}
2931
2932/// One entry in the agent consolidation log.
2933#[derive(Debug, Clone, Serialize, Deserialize)]
2934pub struct AgentConsolidationLogEntry {
2935    pub timestamp: u64,
2936    pub clusters_found: u64,
2937    pub memories_deprecated: u64,
2938    pub anchor_ids: Vec<String>,
2939    pub deprecated_ids: Vec<String>,
2940}
2941
2942/// Request for `PATCH /v1/agents/{agent_id}/consolidation/config`.
2943#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2944pub struct ConsolidationConfigPatch {
2945    #[serde(skip_serializing_if = "Option::is_none")]
2946    pub enabled: Option<bool>,
2947    #[serde(skip_serializing_if = "Option::is_none")]
2948    pub epsilon: Option<f64>,
2949    #[serde(skip_serializing_if = "Option::is_none")]
2950    pub min_samples: Option<u32>,
2951    #[serde(skip_serializing_if = "Option::is_none")]
2952    pub soft_deprecation_days: Option<u32>,
2953}
2954
2955/// Response from consolidation config endpoints.
2956#[derive(Debug, Clone, Serialize, Deserialize)]
2957pub struct AgentConsolidationConfig {
2958    pub enabled: bool,
2959    pub epsilon: f64,
2960    pub min_samples: u32,
2961    pub soft_deprecation_days: u32,
2962}
2963
2964/// Response from `GET /v1/namespaces/{ns}/config`.
2965#[derive(Debug, Clone, Serialize, Deserialize)]
2966pub struct NamespaceEntityConfig {
2967    pub namespace: String,
2968    pub extract_entities: bool,
2969    pub entity_types: Vec<String>,
2970}
2971
2972/// Response from `GET /v1/namespaces/{ns}/extractor`.
2973#[derive(Debug, Clone, Serialize, Deserialize)]
2974pub struct NamespaceExtractorConfig {
2975    pub provider: String,
2976    #[serde(skip_serializing_if = "Option::is_none")]
2977    pub model: Option<String>,
2978    #[serde(skip_serializing_if = "Option::is_none")]
2979    pub base_url: Option<String>,
2980}
2981
2982// ============================================================================
2983// Phase 2 Types — Cluster, Quotas, Backups, Ops
2984// ============================================================================
2985
2986/// Per-node replication lag entry.
2987#[derive(Debug, Clone, Serialize, Deserialize)]
2988pub struct NodeReplicationLag {
2989    pub node_id: String,
2990    pub lag_ms: u64,
2991    pub status: String,
2992}
2993
2994/// Response from `GET /admin/cluster/replication`.
2995#[derive(Debug, Clone, Serialize, Deserialize)]
2996pub struct ReplicationStatus {
2997    pub replication_factor: u32,
2998    pub healthy_replicas: u32,
2999    pub total_nodes: u32,
3000    #[serde(default)]
3001    pub replication_lag: Vec<NodeReplicationLag>,
3002}
3003
3004/// Shard information.
3005#[derive(Debug, Clone, Serialize, Deserialize)]
3006pub struct ShardInfo {
3007    pub shard_id: String,
3008    pub namespace: String,
3009    pub primary_node: String,
3010    #[serde(default)]
3011    pub replica_nodes: Vec<String>,
3012    pub state: String,
3013    pub vector_count: u64,
3014    pub size_bytes: u64,
3015}
3016
3017/// Response from `GET /admin/cluster/shards`.
3018#[derive(Debug, Clone, Serialize, Deserialize)]
3019pub struct ShardListResponse {
3020    pub shards: Vec<ShardInfo>,
3021    pub total: u32,
3022}
3023
3024/// Request for `POST /admin/cluster/shards/rebalance`.
3025#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3026pub struct ShardRebalanceRequest {
3027    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3028    pub shard_ids: Vec<String>,
3029    #[serde(default)]
3030    pub dry_run: bool,
3031}
3032
3033/// A planned shard move.
3034#[derive(Debug, Clone, Serialize, Deserialize)]
3035pub struct ShardMove {
3036    pub shard_id: String,
3037    pub from_node: String,
3038    pub to_node: String,
3039}
3040
3041/// Response from `POST /admin/cluster/shards/rebalance`.
3042#[derive(Debug, Clone, Serialize, Deserialize)]
3043pub struct ShardRebalanceResponse {
3044    pub initiated: bool,
3045    pub operation_id: String,
3046    pub shards_affected: u32,
3047    #[serde(skip_serializing_if = "Option::is_none")]
3048    pub estimated_seconds: Option<u64>,
3049    #[serde(default)]
3050    pub planned_moves: Vec<ShardMove>,
3051}
3052
3053/// Response from `GET /admin/cluster/maintenance`.
3054#[derive(Debug, Clone, Serialize, Deserialize)]
3055pub struct MaintenanceStatus {
3056    pub enabled: bool,
3057    #[serde(skip_serializing_if = "Option::is_none")]
3058    pub reason: Option<String>,
3059    #[serde(skip_serializing_if = "Option::is_none")]
3060    pub enabled_at: Option<u64>,
3061    #[serde(skip_serializing_if = "Option::is_none")]
3062    pub scheduled_end: Option<u64>,
3063    #[serde(default)]
3064    pub nodes_in_maintenance: Vec<String>,
3065    pub rejecting_requests: bool,
3066}
3067
3068/// Request for `POST /admin/cluster/maintenance/enable`.
3069#[derive(Debug, Clone, Serialize, Deserialize)]
3070pub struct EnableMaintenanceRequest {
3071    pub reason: String,
3072    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3073    pub node_ids: Vec<String>,
3074    #[serde(default)]
3075    pub reject_requests: bool,
3076    #[serde(skip_serializing_if = "Option::is_none")]
3077    pub duration_minutes: Option<u32>,
3078}
3079
3080/// Request for `POST /admin/cluster/maintenance/disable`.
3081#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3082pub struct DisableMaintenanceRequest {
3083    #[serde(skip_serializing_if = "Option::is_none")]
3084    pub force: Option<bool>,
3085}
3086
3087/// Quota configuration for a namespace.
3088#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3089pub struct QuotaConfig {
3090    #[serde(skip_serializing_if = "Option::is_none")]
3091    pub max_vectors: Option<u64>,
3092    #[serde(skip_serializing_if = "Option::is_none")]
3093    pub max_storage_bytes: Option<u64>,
3094    #[serde(skip_serializing_if = "Option::is_none")]
3095    pub max_dimensions: Option<usize>,
3096    #[serde(skip_serializing_if = "Option::is_none")]
3097    pub max_metadata_bytes: Option<usize>,
3098    #[serde(default)]
3099    pub enforcement: String,
3100}
3101
3102/// Quota usage for a namespace.
3103#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3104pub struct QuotaUsage {
3105    pub vector_count: u64,
3106    pub storage_bytes: u64,
3107    #[serde(skip_serializing_if = "Option::is_none")]
3108    pub avg_dimensions: Option<usize>,
3109    #[serde(skip_serializing_if = "Option::is_none")]
3110    pub avg_metadata_bytes: Option<usize>,
3111    pub last_updated: u64,
3112}
3113
3114/// Combined quota status.
3115#[derive(Debug, Clone, Serialize, Deserialize)]
3116pub struct QuotaStatus {
3117    pub namespace: String,
3118    pub config: QuotaConfig,
3119    pub usage: QuotaUsage,
3120    #[serde(skip_serializing_if = "Option::is_none")]
3121    pub vector_usage_percent: Option<f32>,
3122    #[serde(skip_serializing_if = "Option::is_none")]
3123    pub storage_usage_percent: Option<f32>,
3124    pub is_exceeded: bool,
3125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3126    pub exceeded_quotas: Vec<String>,
3127}
3128
3129/// Response from `GET /admin/quotas`.
3130#[derive(Debug, Clone, Serialize, Deserialize)]
3131pub struct QuotaListResponse {
3132    pub quotas: Vec<QuotaStatus>,
3133    pub total: u64,
3134    #[serde(skip_serializing_if = "Option::is_none")]
3135    pub default_config: Option<QuotaConfig>,
3136}
3137
3138/// Response from `GET /admin/quotas/default`.
3139#[derive(Debug, Clone, Serialize, Deserialize)]
3140pub struct DefaultQuotaResponse {
3141    pub config: Option<QuotaConfig>,
3142}
3143
3144/// Request for `PUT /admin/quotas/default`.
3145#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3146pub struct SetDefaultQuotaRequest {
3147    pub config: Option<QuotaConfig>,
3148}
3149
3150/// Request for `PUT /admin/quotas/{namespace}`.
3151#[derive(Debug, Clone, Serialize, Deserialize)]
3152pub struct SetQuotaRequest {
3153    pub config: QuotaConfig,
3154}
3155
3156/// Response from `PUT /admin/quotas`.
3157#[derive(Debug, Clone, Serialize, Deserialize)]
3158pub struct SetQuotaResponse {
3159    pub success: bool,
3160    pub namespace: String,
3161    pub config: QuotaConfig,
3162    pub message: String,
3163}
3164
3165/// Request for `POST /admin/quotas/{namespace}/check`.
3166#[derive(Debug, Clone, Serialize, Deserialize)]
3167pub struct QuotaCheckRequest {
3168    pub vector_ids: Vec<String>,
3169    #[serde(skip_serializing_if = "Option::is_none")]
3170    pub dimensions: Option<usize>,
3171    #[serde(skip_serializing_if = "Option::is_none")]
3172    pub metadata_bytes: Option<usize>,
3173}
3174
3175/// Response from `POST /admin/quotas/{namespace}/check`.
3176#[derive(Debug, Clone, Serialize, Deserialize)]
3177pub struct QuotaCheckResult {
3178    pub allowed: bool,
3179    #[serde(skip_serializing_if = "Option::is_none")]
3180    pub reason: Option<String>,
3181    pub usage: QuotaUsage,
3182    #[serde(skip_serializing_if = "Option::is_none")]
3183    pub exceeded_quota: Option<String>,
3184}
3185
3186/// Backup information.
3187#[derive(Debug, Clone, Serialize, Deserialize)]
3188pub struct AdminBackupInfo {
3189    pub backup_id: String,
3190    pub name: String,
3191    pub backup_type: String,
3192    pub status: String,
3193    #[serde(default)]
3194    pub namespaces: Vec<String>,
3195    pub vector_count: u64,
3196    pub size_bytes: u64,
3197    pub created_at: u64,
3198    #[serde(skip_serializing_if = "Option::is_none")]
3199    pub completed_at: Option<u64>,
3200    #[serde(skip_serializing_if = "Option::is_none")]
3201    pub duration_seconds: Option<u64>,
3202    #[serde(skip_serializing_if = "Option::is_none")]
3203    pub storage_path: Option<String>,
3204    #[serde(skip_serializing_if = "Option::is_none")]
3205    pub error: Option<String>,
3206    pub encrypted: bool,
3207    #[serde(skip_serializing_if = "Option::is_none")]
3208    pub compression: Option<String>,
3209}
3210
3211/// Response from `GET /admin/backups`.
3212#[derive(Debug, Clone, Serialize, Deserialize)]
3213pub struct BackupListResponse {
3214    pub backups: Vec<AdminBackupInfo>,
3215    pub total: u64,
3216}
3217
3218/// Request for `POST /admin/backups`.
3219#[derive(Debug, Clone, Serialize, Deserialize)]
3220pub struct CreateBackupRequest {
3221    pub name: String,
3222    #[serde(skip_serializing_if = "Option::is_none")]
3223    pub backup_type: Option<String>,
3224    #[serde(skip_serializing_if = "Option::is_none")]
3225    pub namespaces: Option<Vec<String>>,
3226    #[serde(skip_serializing_if = "Option::is_none")]
3227    pub encrypt: Option<bool>,
3228    #[serde(skip_serializing_if = "Option::is_none")]
3229    pub compression: Option<String>,
3230}
3231
3232/// Response from `POST /admin/backups`.
3233#[derive(Debug, Clone, Serialize, Deserialize)]
3234pub struct CreateBackupResponse {
3235    pub backup: AdminBackupInfo,
3236    #[serde(skip_serializing_if = "Option::is_none")]
3237    pub estimated_completion: Option<u64>,
3238}
3239
3240/// Request for `POST /admin/backups/restore`.
3241#[derive(Debug, Clone, Serialize, Deserialize)]
3242pub struct RestoreBackupRequest {
3243    pub backup_id: String,
3244    #[serde(skip_serializing_if = "Option::is_none")]
3245    pub target_namespaces: Option<Vec<String>>,
3246    #[serde(skip_serializing_if = "Option::is_none")]
3247    pub overwrite: Option<bool>,
3248    #[serde(skip_serializing_if = "Option::is_none")]
3249    pub point_in_time: Option<u64>,
3250}
3251
3252/// Response from `POST /admin/backups/restore`.
3253#[derive(Debug, Clone, Serialize, Deserialize)]
3254pub struct RestoreBackupResponse {
3255    pub restore_id: String,
3256    pub status: String,
3257    pub backup_id: String,
3258    #[serde(default)]
3259    pub namespaces: Vec<String>,
3260    pub started_at: u64,
3261    #[serde(skip_serializing_if = "Option::is_none")]
3262    pub estimated_completion: Option<u64>,
3263    #[serde(skip_serializing_if = "Option::is_none")]
3264    pub progress_percent: Option<u8>,
3265    #[serde(skip_serializing_if = "Option::is_none")]
3266    pub vectors_restored: Option<u64>,
3267    #[serde(skip_serializing_if = "Option::is_none")]
3268    pub completed_at: Option<u64>,
3269    #[serde(skip_serializing_if = "Option::is_none")]
3270    pub duration_seconds: Option<u64>,
3271    #[serde(skip_serializing_if = "Option::is_none")]
3272    pub error: Option<String>,
3273}
3274
3275/// Backup schedule configuration.
3276#[derive(Debug, Clone, Serialize, Deserialize)]
3277pub struct BackupSchedule {
3278    pub enabled: bool,
3279    #[serde(skip_serializing_if = "Option::is_none")]
3280    pub cron: Option<String>,
3281    pub backup_type: String,
3282    pub retention_days: u32,
3283    pub max_backups: u32,
3284    #[serde(default)]
3285    pub namespaces: Vec<String>,
3286    pub encrypt: bool,
3287    #[serde(skip_serializing_if = "Option::is_none")]
3288    pub compression: Option<String>,
3289    #[serde(skip_serializing_if = "Option::is_none")]
3290    pub last_backup_at: Option<u64>,
3291    #[serde(skip_serializing_if = "Option::is_none")]
3292    pub next_backup_at: Option<u64>,
3293}
3294
3295/// Request for `POST /admin/backups/schedule`.
3296#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3297pub struct UpdateBackupScheduleRequest {
3298    #[serde(skip_serializing_if = "Option::is_none")]
3299    pub enabled: Option<bool>,
3300    #[serde(skip_serializing_if = "Option::is_none")]
3301    pub cron: Option<String>,
3302    #[serde(skip_serializing_if = "Option::is_none")]
3303    pub backup_type: Option<String>,
3304    #[serde(skip_serializing_if = "Option::is_none")]
3305    pub retention_days: Option<u32>,
3306    #[serde(skip_serializing_if = "Option::is_none")]
3307    pub max_backups: Option<u32>,
3308    #[serde(skip_serializing_if = "Option::is_none")]
3309    pub namespaces: Option<Vec<String>>,
3310    #[serde(skip_serializing_if = "Option::is_none")]
3311    pub encrypt: Option<bool>,
3312    #[serde(skip_serializing_if = "Option::is_none")]
3313    pub compression: Option<String>,
3314}
3315
3316// ============================================================================
3317// Route Query (POST /v1/route)
3318// ============================================================================
3319
3320fn default_route_top_k() -> usize {
3321    3
3322}
3323
3324fn default_route_min_similarity() -> f32 {
3325    0.3
3326}
3327
3328/// Request for `POST /v1/route`.
3329#[derive(Debug, Clone, Serialize, Deserialize)]
3330pub struct RouteRequest {
3331    /// The query string to route.
3332    pub query: String,
3333    /// Maximum number of matching routes to return.
3334    #[serde(default = "default_route_top_k")]
3335    pub top_k: usize,
3336    /// Minimum similarity threshold for route matches.
3337    #[serde(default = "default_route_min_similarity")]
3338    pub min_similarity: f32,
3339    /// Optional embedding model override.
3340    #[serde(skip_serializing_if = "Option::is_none")]
3341    pub model: Option<String>,
3342}
3343
3344/// A single route match returned by `POST /v1/route`.
3345#[derive(Debug, Clone, Serialize, Deserialize)]
3346pub struct RouteMatch {
3347    /// Matched namespace name.
3348    pub namespace: String,
3349    /// Cosine similarity score.
3350    pub similarity: f64,
3351    /// Optional namespace description.
3352    #[serde(skip_serializing_if = "Option::is_none")]
3353    pub description: Option<String>,
3354}
3355
3356/// Response from `POST /v1/route`.
3357#[derive(Debug, Clone, Serialize, Deserialize)]
3358pub struct RouteResponse {
3359    /// Ordered list of route matches.
3360    pub routes: Vec<RouteMatch>,
3361    /// Embedding model used.
3362    pub model: String,
3363    /// Time spent computing embeddings (milliseconds).
3364    pub embedding_time_ms: u64,
3365}
3366
3367// ============================================================================
3368// Import Job Status (GET /v1/import/{job_id}/status)
3369// ============================================================================
3370
3371/// Status of an import job returned by `GET /v1/import/{job_id}/status`.
3372#[derive(Debug, Clone, Serialize, Deserialize)]
3373pub struct ImportJobStatus {
3374    /// Unique job identifier.
3375    pub job_id: String,
3376    /// Current job status (e.g. "running", "completed", "failed").
3377    pub status: String,
3378    /// Import file format (e.g. "jsonl", "csv").
3379    pub format: String,
3380    /// Total records in the import.
3381    pub total: usize,
3382    /// Records successfully imported.
3383    pub imported: usize,
3384    /// Records skipped (duplicates, invalid).
3385    pub skipped: usize,
3386    /// Per-record error messages, if any.
3387    #[serde(default)]
3388    pub errors: Vec<String>,
3389    /// Unix timestamp when the job started.
3390    pub started_at: u64,
3391    /// Unix timestamp when the job finished, if completed.
3392    #[serde(skip_serializing_if = "Option::is_none")]
3393    pub finished_at: Option<u64>,
3394}
3395
3396// ============================================================================
3397// Storage Tier Overview (GET /admin/storage/tiers)
3398// ============================================================================
3399
3400/// Information about a single storage tier.
3401#[derive(Debug, Clone, Serialize, Deserialize)]
3402pub struct TierInfo {
3403    /// Tier name (e.g. "hot", "warm", "cold").
3404    pub name: String,
3405    /// Tier classification.
3406    pub tier_type: String,
3407    /// Underlying storage technology.
3408    pub technology: String,
3409    /// Human-readable tier description.
3410    pub description: String,
3411    /// Target access latency.
3412    pub target_latency: String,
3413    /// Optional capacity limit.
3414    #[serde(skip_serializing_if = "Option::is_none")]
3415    pub capacity: Option<String>,
3416    /// Current tier status.
3417    pub status: String,
3418    /// Number of items currently in this tier.
3419    pub current_count: u64,
3420    /// Number of cache/access hits.
3421    pub hit_count: u64,
3422    /// Hit rate as a fraction (0.0–1.0).
3423    pub hit_rate: f64,
3424}
3425
3426/// Tiered storage configuration parameters.
3427#[derive(Debug, Clone, Serialize, Deserialize)]
3428pub struct TierConfig {
3429    /// Maximum number of items in the hot tier.
3430    pub hot_tier_capacity: usize,
3431    /// Seconds of inactivity before promoting from hot to warm.
3432    pub hot_to_warm_threshold_secs: u64,
3433    /// Seconds of inactivity before demoting from warm to cold.
3434    pub warm_to_cold_threshold_secs: u64,
3435    /// Whether automatic tiering is enabled.
3436    pub auto_tier_enabled: bool,
3437    /// Interval between tier-check cycles (seconds).
3438    pub tier_check_interval_secs: u64,
3439}
3440
3441/// Tier movement activity counters.
3442#[derive(Debug, Clone, Serialize, Deserialize)]
3443pub struct TierActivity {
3444    /// Total promotions across all tiers.
3445    pub promotions: u64,
3446    /// Total demotions across all tiers.
3447    pub demotions: u64,
3448    /// Overall cache hit rate.
3449    pub cache_hit_rate: f64,
3450    /// Active storage backend name.
3451    pub storage_backend: String,
3452    /// Promotions specifically to the hot tier.
3453    pub promotions_to_hot: u64,
3454    /// Demotions specifically to warm.
3455    pub demotions_to_warm: u64,
3456    /// Demotions specifically to cold.
3457    pub demotions_to_cold: u64,
3458}
3459
3460/// Overview of the tiered storage system from `GET /admin/storage/tiers`.
3461#[derive(Debug, Clone, Serialize, Deserialize)]
3462pub struct StorageTierOverview {
3463    /// Whether tiered storage is enabled.
3464    pub tiers_enabled: bool,
3465    /// Description of each tier.
3466    pub architecture: Vec<TierInfo>,
3467    /// Current tier configuration.
3468    pub config: TierConfig,
3469    /// Tier movement activity.
3470    pub activity: TierActivity,
3471}
3472
3473// ============================================================================
3474// Memory Type Stats (GET /admin/memory-type-stats)
3475// ============================================================================
3476
3477/// Per-type memory statistics from `GET /admin/memory-type-stats`.
3478#[derive(Debug, Clone, Serialize, Deserialize)]
3479pub struct MemoryTypeStatsResponse {
3480    /// Total number of memories.
3481    pub total: u64,
3482    /// Working memory count.
3483    pub working: u64,
3484    /// Episodic memory count.
3485    pub episodic: u64,
3486    /// Semantic memory count.
3487    pub semantic: u64,
3488    /// Procedural memory count.
3489    pub procedural: u64,
3490    /// Number of distinct agent namespaces.
3491    pub agent_namespaces: u64,
3492}
3493
3494// ============================================================================
3495// Migrate Namespace Dimensions (POST /admin/namespaces/migrate-dimensions)
3496// ============================================================================
3497
3498fn default_target_dimension() -> usize {
3499    1024
3500}
3501
3502/// Request for `POST /admin/namespaces/migrate-dimensions`.
3503#[derive(Debug, Clone, Serialize, Deserialize)]
3504pub struct MigrateNamespaceDimensionsRequest {
3505    /// Namespaces to migrate (empty = all).
3506    #[serde(default)]
3507    pub namespaces: Vec<String>,
3508    /// Target embedding dimension.
3509    #[serde(default = "default_target_dimension")]
3510    pub target_dimension: usize,
3511}
3512
3513/// Per-namespace migration result.
3514#[derive(Debug, Clone, Serialize, Deserialize)]
3515pub struct NamespaceMigrationResult {
3516    /// Namespace that was migrated.
3517    pub namespace: String,
3518    /// Original embedding dimension.
3519    pub original_dimension: usize,
3520    /// Vectors successfully re-embedded.
3521    pub vectors_migrated: usize,
3522    /// Vectors skipped (already at target dimension).
3523    pub vectors_skipped: usize,
3524    /// Migration status for this namespace.
3525    pub status: String,
3526    /// Error message if migration failed for this namespace.
3527    #[serde(skip_serializing_if = "Option::is_none")]
3528    pub error: Option<String>,
3529}
3530
3531/// Response from `POST /admin/namespaces/migrate-dimensions`.
3532#[derive(Debug, Clone, Serialize, Deserialize)]
3533pub struct MigrateDimensionsResponse {
3534    /// Number of namespaces successfully migrated.
3535    pub migrated: usize,
3536    /// Number of namespaces that failed migration.
3537    pub failed: usize,
3538    /// Namespaces already at the target dimension.
3539    pub already_current: usize,
3540    /// Per-namespace results.
3541    pub results: Vec<NamespaceMigrationResult>,
3542}