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