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// T-I-F Reliability Scoring (Phase 3 T-I-F RFC)
2658// ============================================================================
2659
2660/// Reliability classification label from a [`TifScore`].
2661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2662#[serde(rename_all = "snake_case")]
2663pub enum TifClassification {
2664    /// Majority of feedback is negative — the memory likely contains incorrect information.
2665    SurfaceContradiction,
2666    /// Majority of feedback is uncertain — ask the user for clarification before reusing.
2667    AskClarification,
2668    /// Strong positive feedback signal — safe to reuse without additional verification.
2669    ConfidentReuse,
2670    /// Mixed or weak signals — verify the memory before acting on it.
2671    VerifyBeforeUse,
2672}
2673
2674impl TifClassification {
2675    /// Stable string label matching the Python/JS/Go SDK classification strings.
2676    pub fn as_str(&self) -> &'static str {
2677        match self {
2678            Self::SurfaceContradiction => "surface_contradiction",
2679            Self::AskClarification => "ask_clarification",
2680            Self::ConfidentReuse => "confident_reuse",
2681            Self::VerifyBeforeUse => "verify_before_use",
2682        }
2683    }
2684}
2685
2686/// Truth-Indeterminacy-Falsity reliability score for a memory (T-I-F RFC Phase 3).
2687///
2688/// All three proportions (`truth`, `indeterminacy`, `falsity`) sum to 1.0.
2689/// Build via [`TifScore::from_feedback_history`] or [`TifScore::from_metadata`].
2690#[derive(Debug, Clone, Serialize, Deserialize)]
2691pub struct TifScore {
2692    /// Proportion of positive feedback signals (`upvote` / `positive`).
2693    pub truth: f64,
2694    /// Proportion of uncertainty signals (`flag`).
2695    pub indeterminacy: f64,
2696    /// Proportion of negative feedback signals (`downvote` / `negative`).
2697    pub falsity: f64,
2698    /// Total feedback events used to compute this score.
2699    pub feedback_count: u64,
2700    /// Human-readable reliability classification.
2701    pub classification: TifClassification,
2702}
2703
2704fn classify_tif(truth: f64, indeterminacy: f64, falsity: f64) -> TifClassification {
2705    if falsity >= 0.5 {
2706        TifClassification::SurfaceContradiction
2707    } else if indeterminacy >= 0.5 {
2708        TifClassification::AskClarification
2709    } else if truth >= 0.7 {
2710        TifClassification::ConfidentReuse
2711    } else {
2712        TifClassification::VerifyBeforeUse
2713    }
2714}
2715
2716impl TifScore {
2717    /// Compute a [`TifScore`] from a memory's [`FeedbackHistoryResponse`].
2718    ///
2719    /// Signals are bucketed as:
2720    /// - [`FeedbackSignal::Upvote`] / [`FeedbackSignal::Positive`] → truth
2721    /// - [`FeedbackSignal::Downvote`] / [`FeedbackSignal::Negative`] → falsity
2722    /// - [`FeedbackSignal::Flag`] → indeterminacy
2723    ///
2724    /// With no feedback the score is `{ truth: 0.0, indeterminacy: 1.0, falsity: 0.0, feedback_count: 0 }`.
2725    pub fn from_feedback_history(history: &FeedbackHistoryResponse) -> Self {
2726        let mut upvotes: u64 = 0;
2727        let mut downvotes: u64 = 0;
2728        let mut flags: u64 = 0;
2729        for entry in &history.entries {
2730            match entry.signal {
2731                FeedbackSignal::Upvote | FeedbackSignal::Positive => upvotes += 1,
2732                FeedbackSignal::Downvote | FeedbackSignal::Negative => downvotes += 1,
2733                FeedbackSignal::Flag => flags += 1,
2734            }
2735        }
2736        let total = upvotes + downvotes + flags;
2737        if total == 0 {
2738            return Self {
2739                truth: 0.0,
2740                indeterminacy: 1.0,
2741                falsity: 0.0,
2742                feedback_count: 0,
2743                classification: TifClassification::AskClarification,
2744            };
2745        }
2746        let total_f = total as f64;
2747        let base_indeterminacy = if total < 3 {
2748            (3 - total) as f64 * 0.25
2749        } else {
2750            0.0
2751        };
2752        let mut truth = upvotes as f64 / total_f;
2753        let mut falsity = downvotes as f64 / total_f;
2754        let mut indeterminacy = flags as f64 / total_f + base_indeterminacy;
2755        let sum = truth + falsity + indeterminacy;
2756        truth /= sum;
2757        falsity /= sum;
2758        indeterminacy /= sum;
2759        Self {
2760            truth,
2761            indeterminacy,
2762            falsity,
2763            feedback_count: total,
2764            classification: classify_tif(truth, indeterminacy, falsity),
2765        }
2766    }
2767
2768    /// Deserialise a [`TifScore`] from a `metadata["reliability"]` map.
2769    ///
2770    /// Expected keys: `truth`, `indeterminacy`, `falsity`, `feedback_count` (snake_case).
2771    pub fn from_metadata(data: &serde_json::Value) -> Option<Self> {
2772        let truth = data["truth"].as_f64()?;
2773        let indeterminacy = data["indeterminacy"].as_f64()?;
2774        let falsity = data["falsity"].as_f64()?;
2775        let feedback_count = data["feedback_count"].as_u64().unwrap_or(0);
2776        Some(Self {
2777            truth,
2778            indeterminacy,
2779            falsity,
2780            feedback_count,
2781            classification: classify_tif(truth, indeterminacy, falsity),
2782        })
2783    }
2784}
2785
2786#[cfg(test)]
2787mod tif_tests {
2788    use super::*;
2789
2790    fn make_history(signals: &[&str]) -> FeedbackHistoryResponse {
2791        FeedbackHistoryResponse {
2792            memory_id: "test-mem".to_string(),
2793            entries: signals
2794                .iter()
2795                .map(|s| {
2796                    let signal = match *s {
2797                        "upvote" => FeedbackSignal::Upvote,
2798                        "downvote" => FeedbackSignal::Downvote,
2799                        "flag" => FeedbackSignal::Flag,
2800                        "positive" => FeedbackSignal::Positive,
2801                        "negative" => FeedbackSignal::Negative,
2802                        other => panic!("unknown signal: {other}"),
2803                    };
2804                    FeedbackHistoryEntry {
2805                        signal,
2806                        timestamp: 0,
2807                        old_importance: 0.5,
2808                        new_importance: 0.5,
2809                    }
2810                })
2811                .collect(),
2812        }
2813    }
2814
2815    #[test]
2816    fn no_feedback_max_indeterminacy() {
2817        let score = TifScore::from_feedback_history(&make_history(&[]));
2818        assert_eq!(score.truth, 0.0);
2819        assert_eq!(score.indeterminacy, 1.0);
2820        assert_eq!(score.falsity, 0.0);
2821        assert_eq!(score.feedback_count, 0);
2822        assert_eq!(score.classification, TifClassification::AskClarification);
2823    }
2824
2825    #[test]
2826    fn all_upvotes() {
2827        let score = TifScore::from_feedback_history(&make_history(&["upvote", "upvote", "upvote"]));
2828        assert!((score.truth - 1.0).abs() < 1e-9);
2829        assert_eq!(score.feedback_count, 3);
2830        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2831    }
2832
2833    #[test]
2834    fn all_downvotes() {
2835        let score = TifScore::from_feedback_history(&make_history(&["downvote", "downvote"]));
2836        assert!((score.falsity - 0.8).abs() < 1e-9);
2837        assert!((score.indeterminacy - 0.2).abs() < 1e-9);
2838        assert_eq!(
2839            score.classification,
2840            TifClassification::SurfaceContradiction
2841        );
2842    }
2843
2844    #[test]
2845    fn all_flags() {
2846        let score = TifScore::from_feedback_history(&make_history(&["flag", "flag"]));
2847        assert!((score.indeterminacy - 1.0).abs() < 1e-9);
2848        assert_eq!(score.classification, TifClassification::AskClarification);
2849    }
2850
2851    #[test]
2852    fn mixed_signals() {
2853        let score = TifScore::from_feedback_history(&make_history(&[
2854            "upvote", "upvote", "upvote", "upvote", "downvote", "downvote", "flag", "flag", "flag",
2855            "flag",
2856        ]));
2857        assert!((score.truth - 0.4).abs() < 1e-9);
2858        assert!((score.falsity - 0.2).abs() < 1e-9);
2859        assert!((score.indeterminacy - 0.4).abs() < 1e-9);
2860        assert_eq!(score.feedback_count, 10);
2861    }
2862
2863    #[test]
2864    fn positive_alias() {
2865        let score =
2866            TifScore::from_feedback_history(&make_history(&["positive", "positive", "downvote"]));
2867        assert!((score.truth - 2.0 / 3.0).abs() < 1e-9);
2868        assert!((score.falsity - 1.0 / 3.0).abs() < 1e-9);
2869    }
2870
2871    #[test]
2872    fn negative_alias() {
2873        let score =
2874            TifScore::from_feedback_history(&make_history(&["upvote", "negative", "negative"]));
2875        assert!((score.falsity - 2.0 / 3.0).abs() < 1e-9);
2876    }
2877
2878    #[test]
2879    fn proportions_sum_to_one() {
2880        let score = TifScore::from_feedback_history(&make_history(&["upvote", "downvote", "flag"]));
2881        assert!((score.truth + score.indeterminacy + score.falsity - 1.0).abs() < 1e-9);
2882    }
2883
2884    #[test]
2885    fn classification_surface_contradiction() {
2886        let score = TifScore::from_feedback_history(&make_history(&[
2887            "downvote", "downvote", "downvote", "upvote", "upvote",
2888        ]));
2889        assert_eq!(
2890            score.classification,
2891            TifClassification::SurfaceContradiction
2892        );
2893    }
2894
2895    #[test]
2896    fn classification_verify_before_use() {
2897        // 2 upvotes, 2 downvotes, 3 flags → no dominant signal
2898        let score = TifScore::from_feedback_history(&make_history(&[
2899            "upvote", "upvote", "downvote", "downvote", "flag", "flag", "flag",
2900        ]));
2901        assert_eq!(score.classification, TifClassification::VerifyBeforeUse);
2902    }
2903
2904    #[test]
2905    fn falsity_priority_over_indeterminacy() {
2906        // 3 downvotes + 3 flags: both >= 0.5, falsity wins
2907        let score = TifScore::from_feedback_history(&make_history(&[
2908            "downvote", "downvote", "downvote", "flag", "flag", "flag",
2909        ]));
2910        assert_eq!(
2911            score.classification,
2912            TifClassification::SurfaceContradiction
2913        );
2914    }
2915
2916    #[test]
2917    fn from_metadata_round_trip() {
2918        use serde_json::json;
2919        let data =
2920            json!({ "truth": 0.75, "indeterminacy": 0.15, "falsity": 0.10, "feedback_count": 20 });
2921        let score = TifScore::from_metadata(&data).unwrap();
2922        assert!((score.truth - 0.75).abs() < 1e-9);
2923        assert_eq!(score.feedback_count, 20);
2924        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2925    }
2926
2927    #[test]
2928    fn from_metadata_missing_feedback_count() {
2929        use serde_json::json;
2930        let data = json!({ "truth": 0.8, "indeterminacy": 0.1, "falsity": 0.1 });
2931        let score = TifScore::from_metadata(&data).unwrap();
2932        assert_eq!(score.feedback_count, 0);
2933    }
2934
2935    #[test]
2936    fn classification_as_str() {
2937        assert_eq!(
2938            TifClassification::SurfaceContradiction.as_str(),
2939            "surface_contradiction"
2940        );
2941        assert_eq!(
2942            TifClassification::AskClarification.as_str(),
2943            "ask_clarification"
2944        );
2945        assert_eq!(
2946            TifClassification::ConfidentReuse.as_str(),
2947            "confident_reuse"
2948        );
2949        assert_eq!(
2950            TifClassification::VerifyBeforeUse.as_str(),
2951            "verify_before_use"
2952        );
2953    }
2954
2955    // ── Thin evidence ────────────────────────────────────────────────────
2956
2957    #[test]
2958    fn thin_evidence_single_upvote_not_confident() {
2959        let score = TifScore::from_feedback_history(&make_history(&["upvote"]));
2960        assert!((score.truth + score.indeterminacy + score.falsity - 1.0).abs() < 1e-9);
2961        assert!(score.indeterminacy > 0.0);
2962        assert!(score.truth < 0.70);
2963        assert_eq!(score.classification, TifClassification::VerifyBeforeUse);
2964    }
2965
2966    #[test]
2967    fn thin_evidence_two_upvotes_confident() {
2968        let score = TifScore::from_feedback_history(&make_history(&["upvote", "upvote"]));
2969        assert!((score.truth - 0.8).abs() < 1e-9);
2970        assert!((score.indeterminacy - 0.2).abs() < 1e-9);
2971        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2972    }
2973
2974    #[test]
2975    fn thin_evidence_three_upvotes_no_base() {
2976        let score = TifScore::from_feedback_history(&make_history(&["upvote", "upvote", "upvote"]));
2977        assert!((score.truth - 1.0).abs() < 1e-9);
2978        assert!((score.indeterminacy - 0.0).abs() < 1e-9);
2979        assert_eq!(score.classification, TifClassification::ConfidentReuse);
2980    }
2981
2982    // ── Golden vectors (canonical T-I-F v1 contract) ─────────────────────
2983
2984    #[test]
2985    fn golden_no_feedback() {
2986        let s = TifScore::from_feedback_history(&make_history(&[]));
2987        assert!((s.truth - 0.0).abs() < 1e-9);
2988        assert!((s.indeterminacy - 1.0).abs() < 1e-9);
2989        assert!((s.falsity - 0.0).abs() < 1e-9);
2990        assert_eq!(s.classification, TifClassification::AskClarification);
2991    }
2992
2993    #[test]
2994    fn golden_one_upvote() {
2995        let s = TifScore::from_feedback_history(&make_history(&["upvote"]));
2996        assert!((s.truth - 2.0 / 3.0).abs() < 1e-4);
2997        assert!((s.indeterminacy - 1.0 / 3.0).abs() < 1e-4);
2998        assert!((s.falsity - 0.0).abs() < 1e-9);
2999        assert_eq!(s.classification, TifClassification::VerifyBeforeUse);
3000    }
3001
3002    #[test]
3003    fn golden_two_upvotes() {
3004        let s = TifScore::from_feedback_history(&make_history(&["upvote", "upvote"]));
3005        assert!((s.truth - 0.8).abs() < 1e-9);
3006        assert!((s.indeterminacy - 0.2).abs() < 1e-9);
3007        assert!((s.falsity - 0.0).abs() < 1e-9);
3008        assert_eq!(s.classification, TifClassification::ConfidentReuse);
3009    }
3010
3011    #[test]
3012    fn golden_three_upvotes() {
3013        let s = TifScore::from_feedback_history(&make_history(&["upvote", "upvote", "upvote"]));
3014        assert!((s.truth - 1.0).abs() < 1e-9);
3015        assert!((s.indeterminacy - 0.0).abs() < 1e-9);
3016        assert!((s.falsity - 0.0).abs() < 1e-9);
3017        assert_eq!(s.classification, TifClassification::ConfidentReuse);
3018    }
3019
3020    #[test]
3021    fn golden_two_downvotes() {
3022        let s = TifScore::from_feedback_history(&make_history(&["downvote", "downvote"]));
3023        assert!((s.truth - 0.0).abs() < 1e-9);
3024        assert!((s.indeterminacy - 0.2).abs() < 1e-9);
3025        assert!((s.falsity - 0.8).abs() < 1e-9);
3026        assert_eq!(s.classification, TifClassification::SurfaceContradiction);
3027    }
3028
3029    #[test]
3030    fn golden_two_flags() {
3031        let s = TifScore::from_feedback_history(&make_history(&["flag", "flag"]));
3032        assert!((s.truth - 0.0).abs() < 1e-9);
3033        assert!((s.indeterminacy - 1.0).abs() < 1e-9);
3034        assert!((s.falsity - 0.0).abs() < 1e-9);
3035        assert_eq!(s.classification, TifClassification::AskClarification);
3036    }
3037
3038    #[test]
3039    fn golden_8up_1down_1flag() {
3040        let s = TifScore::from_feedback_history(&make_history(&[
3041            "upvote", "upvote", "upvote", "upvote", "upvote", "upvote", "upvote", "upvote",
3042            "downvote", "flag",
3043        ]));
3044        assert!((s.truth - 0.8).abs() < 1e-9);
3045        assert!((s.indeterminacy - 0.1).abs() < 1e-9);
3046        assert!((s.falsity - 0.1).abs() < 1e-9);
3047        assert_eq!(s.classification, TifClassification::ConfidentReuse);
3048    }
3049
3050    #[test]
3051    fn golden_3down_3flag() {
3052        let s = TifScore::from_feedback_history(&make_history(&[
3053            "downvote", "downvote", "downvote", "flag", "flag", "flag",
3054        ]));
3055        assert!((s.truth - 0.0).abs() < 1e-9);
3056        assert!((s.indeterminacy - 0.5).abs() < 1e-9);
3057        assert!((s.falsity - 0.5).abs() < 1e-9);
3058        assert_eq!(s.classification, TifClassification::SurfaceContradiction);
3059    }
3060}
3061
3062// ============================================================================
3063// ODE-2: GLiNER Entity Extraction (dakera-ode sidecar)
3064// ============================================================================
3065
3066/// A single entity extracted by the GLiNER model (ODE-2).
3067#[derive(Debug, Clone, Serialize, Deserialize)]
3068pub struct OdeEntity {
3069    /// Span text as it appears in the input.
3070    pub text: String,
3071    /// Entity type label (e.g. `"person"`, `"organization"`).
3072    pub label: String,
3073    /// Start character offset (inclusive) within the input text.
3074    pub start: usize,
3075    /// End character offset (exclusive) within the input text.
3076    pub end: usize,
3077    /// Confidence score in the range [0, 1].
3078    pub score: f32,
3079}
3080
3081/// Request body for `POST /ode/extract` (ODE-2).
3082#[derive(Debug, Clone, Serialize, Deserialize)]
3083pub struct ExtractEntitiesRequest {
3084    /// The text to extract entities from.
3085    pub content: String,
3086    /// Agent context for the extraction.
3087    pub agent_id: String,
3088    /// Optional memory ID to associate with the extraction.
3089    #[serde(skip_serializing_if = "Option::is_none")]
3090    pub memory_id: Option<String>,
3091    /// Optional list of entity type labels to extract.
3092    /// When omitted the ODE sidecar uses its default set.
3093    #[serde(skip_serializing_if = "Option::is_none")]
3094    pub entity_types: Option<Vec<String>>,
3095}
3096
3097/// Response from `POST /ode/extract` on the ODE sidecar (ODE-2).
3098#[derive(Debug, Clone, Serialize, Deserialize)]
3099pub struct ExtractEntitiesResponse {
3100    /// Extracted entities ordered by their start offset.
3101    pub entities: Vec<OdeEntity>,
3102    /// GLiNER model variant used for extraction.
3103    pub model: String,
3104    /// Wall-clock time taken by the ODE sidecar in milliseconds.
3105    pub processing_time_ms: u64,
3106}
3107
3108// ============================================================================
3109// KG-2: Graph Query & Export — response types
3110// ============================================================================
3111
3112/// Response from `GET /v1/knowledge/query` (KG-2).
3113#[derive(Debug, Clone, Serialize, Deserialize)]
3114pub struct KgQueryResponse {
3115    /// Agent whose graph was queried.
3116    pub agent_id: String,
3117    /// Number of unique memory node IDs referenced by the returned edges.
3118    pub node_count: usize,
3119    /// Number of edges returned.
3120    pub edge_count: usize,
3121    /// Matching edges, up to `limit`.
3122    pub edges: Vec<GraphEdge>,
3123}
3124
3125/// Response from `GET /v1/knowledge/path` (KG-2).
3126#[derive(Debug, Clone, Serialize, Deserialize)]
3127pub struct KgPathResponse {
3128    /// Agent whose graph was traversed.
3129    pub agent_id: String,
3130    /// Source memory ID.
3131    pub from_id: String,
3132    /// Target memory ID.
3133    pub to_id: String,
3134    /// Number of edges in the shortest path (0 if source == target).
3135    pub hop_count: usize,
3136    /// Ordered list of memory IDs from source to target (inclusive).
3137    pub path: Vec<String>,
3138}
3139
3140/// Response from `GET /v1/knowledge/export` with `format=json` (KG-2).
3141#[derive(Debug, Clone, Serialize, Deserialize)]
3142pub struct KgExportResponse {
3143    /// Agent whose graph was exported.
3144    pub agent_id: String,
3145    /// Export format used (`"json"` when this struct is deserialized).
3146    pub format: String,
3147    /// Total number of unique memory node IDs in the export.
3148    pub node_count: usize,
3149    /// Total number of edges in the export.
3150    pub edge_count: usize,
3151    /// All graph edges for the agent.
3152    pub edges: Vec<GraphEdge>,
3153}
3154
3155// ============================================================================
3156// COG-1: Cognitive Memory Lifecycle — per-namespace memory policy
3157// ============================================================================
3158
3159/// Per-namespace memory lifecycle policy (COG-1).
3160///
3161/// Controls type-specific TTLs, decay curves, and spaced repetition behaviour.
3162/// All fields have sensible defaults; only override what you need.
3163///
3164/// Used by [`DakeraClient::get_memory_policy`] and
3165/// [`DakeraClient::set_memory_policy`].
3166#[derive(Debug, Clone, Serialize, Deserialize)]
3167pub struct MemoryPolicy {
3168    // Differential TTLs ------------------------------------------------------
3169    /// Default TTL for `working` memories in seconds (default: 14 400 = 4 h).
3170    #[serde(skip_serializing_if = "Option::is_none")]
3171    pub working_ttl_seconds: Option<u64>,
3172    /// Default TTL for `episodic` memories in seconds (default: 2 592 000 = 30 d).
3173    #[serde(skip_serializing_if = "Option::is_none")]
3174    pub episodic_ttl_seconds: Option<u64>,
3175    /// Default TTL for `semantic` memories in seconds (default: 31 536 000 = 365 d).
3176    #[serde(skip_serializing_if = "Option::is_none")]
3177    pub semantic_ttl_seconds: Option<u64>,
3178    /// Default TTL for `procedural` memories in seconds (default: 63 072 000 = 730 d).
3179    #[serde(skip_serializing_if = "Option::is_none")]
3180    pub procedural_ttl_seconds: Option<u64>,
3181
3182    // Decay curves ------------------------------------------------------------
3183    /// Decay strategy for `working` memories (default: `"exponential"`).
3184    #[serde(skip_serializing_if = "Option::is_none")]
3185    pub working_decay: Option<String>,
3186    /// Decay strategy for `episodic` memories (default: `"power_law"`).
3187    #[serde(skip_serializing_if = "Option::is_none")]
3188    pub episodic_decay: Option<String>,
3189    /// Decay strategy for `semantic` memories (default: `"logarithmic"`).
3190    #[serde(skip_serializing_if = "Option::is_none")]
3191    pub semantic_decay: Option<String>,
3192    /// Decay strategy for `procedural` memories (default: `"flat"` — no decay).
3193    #[serde(skip_serializing_if = "Option::is_none")]
3194    pub procedural_decay: Option<String>,
3195
3196    // Spaced repetition -------------------------------------------------------
3197    /// TTL extension multiplier per recall hit (default: 1.0; set to 0.0 to disable).
3198    /// Extension = `access_count × sr_factor × sr_base_interval_seconds`.
3199    #[serde(skip_serializing_if = "Option::is_none")]
3200    pub spaced_repetition_factor: Option<f64>,
3201    /// Base interval in seconds for spaced repetition TTL extension (default: 86 400 = 1 d).
3202    #[serde(skip_serializing_if = "Option::is_none")]
3203    pub spaced_repetition_base_interval_seconds: Option<u64>,
3204
3205    // Proactive consolidation (COG-3) -----------------------------------------
3206    /// Enable background DBSCAN deduplication for this namespace (default: `false`).
3207    /// When `true` the server merges semantically near-duplicate memories every
3208    /// [`consolidation_interval_hours`](Self::consolidation_interval_hours) hours.
3209    #[serde(skip_serializing_if = "Option::is_none")]
3210    pub consolidation_enabled: Option<bool>,
3211    /// DBSCAN epsilon — cosine-similarity threshold to consider two memories
3212    /// duplicates (default: `0.92`; higher = only merge very close neighbours).
3213    #[serde(skip_serializing_if = "Option::is_none")]
3214    pub consolidation_threshold: Option<f32>,
3215    /// How often (in hours) the background consolidation job runs (default: `24`).
3216    #[serde(skip_serializing_if = "Option::is_none")]
3217    pub consolidation_interval_hours: Option<u32>,
3218    /// **Read-only.** Lifetime count of memories merged by the consolidation engine.
3219    /// The server manages this field; any value sent via [`set_memory_policy`] is ignored.
3220    ///
3221    /// [`set_memory_policy`]: crate::DakeraClient::set_memory_policy
3222    #[serde(skip_serializing_if = "Option::is_none")]
3223    pub consolidated_count: Option<u64>,
3224
3225    // Per-namespace rate limiting (SEC-5) -----------------------------------------
3226    /// Enable per-namespace store/recall rate limiting (default: `false`).
3227    #[serde(skip_serializing_if = "Option::is_none")]
3228    pub rate_limit_enabled: Option<bool>,
3229    /// Max store operations per minute for this namespace. `None` = unlimited (default).
3230    #[serde(skip_serializing_if = "Option::is_none")]
3231    pub rate_limit_stores_per_minute: Option<u32>,
3232    /// Max recall operations per minute for this namespace. `None` = unlimited (default).
3233    #[serde(skip_serializing_if = "Option::is_none")]
3234    pub rate_limit_recalls_per_minute: Option<u32>,
3235
3236    // Store-time deduplication (CE-10) -----------------------------------------
3237    /// Deduplicate against existing memories at store time (CE-10, default: `false`).
3238    ///
3239    /// When `true` the server computes a similarity check before persisting a new
3240    /// memory and drops it if a near-duplicate already exists (threshold controlled
3241    /// by [`dedup_threshold`](Self::dedup_threshold)).
3242    #[serde(skip_serializing_if = "Option::is_none")]
3243    pub dedup_on_store: Option<bool>,
3244    /// Cosine-similarity threshold for store-time deduplication (default: `0.92`).
3245    ///
3246    /// Memories with similarity ≥ this value are considered duplicates and the
3247    /// incoming memory is dropped. Only active when `dedup_on_store` is `true`.
3248    #[serde(skip_serializing_if = "Option::is_none")]
3249    pub dedup_threshold: Option<f32>,
3250}
3251
3252impl Default for MemoryPolicy {
3253    fn default() -> Self {
3254        Self {
3255            working_ttl_seconds: Some(14_400),
3256            episodic_ttl_seconds: Some(2_592_000),
3257            semantic_ttl_seconds: Some(31_536_000),
3258            procedural_ttl_seconds: Some(63_072_000),
3259            working_decay: Some("exponential".to_string()),
3260            episodic_decay: Some("power_law".to_string()),
3261            semantic_decay: Some("logarithmic".to_string()),
3262            procedural_decay: Some("flat".to_string()),
3263            spaced_repetition_factor: Some(1.0),
3264            spaced_repetition_base_interval_seconds: Some(86_400),
3265            consolidation_enabled: Some(false),
3266            consolidation_threshold: Some(0.92),
3267            consolidation_interval_hours: Some(24),
3268            consolidated_count: Some(0),
3269            rate_limit_enabled: Some(false),
3270            rate_limit_stores_per_minute: None,
3271            rate_limit_recalls_per_minute: None,
3272            dedup_on_store: Some(false),
3273            dedup_threshold: Some(0.92),
3274        }
3275    }
3276}
3277
3278// =============================================================================
3279// Engine Parity — Vector Bulk Ops, Agent Consolidation, Namespace Config
3280// =============================================================================
3281
3282/// Request for `POST /v1/namespaces/{ns}/vectors/bulk-update`.
3283#[derive(Debug, Clone, Serialize, Deserialize)]
3284pub struct BulkUpdateRequest {
3285    pub filter: serde_json::Value,
3286    pub update: serde_json::Value,
3287}
3288
3289/// Response from `POST /v1/namespaces/{ns}/vectors/bulk-update`.
3290#[derive(Debug, Clone, Serialize, Deserialize)]
3291pub struct BulkUpdateResponse {
3292    pub updated: u64,
3293    pub failed: u64,
3294    pub errors: Vec<String>,
3295}
3296
3297/// Request for `POST /v1/namespaces/{ns}/vectors/bulk-delete`.
3298#[derive(Debug, Clone, Serialize, Deserialize)]
3299pub struct BulkDeleteRequest {
3300    pub filter: serde_json::Value,
3301}
3302
3303/// Response from `POST /v1/namespaces/{ns}/vectors/bulk-delete`.
3304#[derive(Debug, Clone, Serialize, Deserialize)]
3305pub struct BulkDeleteResponse {
3306    pub deleted: u64,
3307    pub failed: u64,
3308    pub errors: Vec<String>,
3309}
3310
3311/// Request for `POST /v1/namespaces/{ns}/vectors/count`.
3312#[derive(Debug, Clone, Serialize, Deserialize)]
3313pub struct CountVectorsRequest {
3314    #[serde(skip_serializing_if = "Option::is_none")]
3315    pub filter: Option<serde_json::Value>,
3316}
3317
3318/// Response from `POST /v1/namespaces/{ns}/vectors/count`.
3319#[derive(Debug, Clone, Serialize, Deserialize)]
3320pub struct CountVectorsResponse {
3321    pub count: u64,
3322    pub namespace: String,
3323}
3324
3325/// Response from `POST /v1/agents/{agent_id}/consolidate`.
3326#[derive(Debug, Clone, Serialize, Deserialize)]
3327pub struct AgentConsolidateResponse {
3328    pub agent_id: String,
3329    pub memories_scanned: u64,
3330    pub clusters_found: u64,
3331    pub memories_deprecated: u64,
3332    pub anchor_ids: Vec<String>,
3333    pub deprecated_ids: Vec<String>,
3334    #[serde(skip_serializing_if = "Option::is_none")]
3335    pub skipped: Option<bool>,
3336    #[serde(skip_serializing_if = "Option::is_none")]
3337    pub reason: Option<String>,
3338}
3339
3340/// One entry in the agent consolidation log.
3341#[derive(Debug, Clone, Serialize, Deserialize)]
3342pub struct AgentConsolidationLogEntry {
3343    pub timestamp: u64,
3344    pub clusters_found: u64,
3345    pub memories_deprecated: u64,
3346    pub anchor_ids: Vec<String>,
3347    pub deprecated_ids: Vec<String>,
3348}
3349
3350/// Request for `PATCH /v1/agents/{agent_id}/consolidation/config`.
3351#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3352pub struct ConsolidationConfigPatch {
3353    #[serde(skip_serializing_if = "Option::is_none")]
3354    pub enabled: Option<bool>,
3355    #[serde(skip_serializing_if = "Option::is_none")]
3356    pub epsilon: Option<f64>,
3357    #[serde(skip_serializing_if = "Option::is_none")]
3358    pub min_samples: Option<u32>,
3359    #[serde(skip_serializing_if = "Option::is_none")]
3360    pub soft_deprecation_days: Option<u32>,
3361}
3362
3363/// Response from consolidation config endpoints.
3364#[derive(Debug, Clone, Serialize, Deserialize)]
3365pub struct AgentConsolidationConfig {
3366    pub enabled: bool,
3367    pub epsilon: f64,
3368    pub min_samples: u32,
3369    pub soft_deprecation_days: u32,
3370}
3371
3372/// Response from `GET /v1/namespaces/{ns}/config`.
3373#[derive(Debug, Clone, Serialize, Deserialize)]
3374pub struct NamespaceEntityConfig {
3375    pub namespace: String,
3376    pub extract_entities: bool,
3377    pub entity_types: Vec<String>,
3378}
3379
3380/// Response from `GET /v1/namespaces/{ns}/extractor`.
3381#[derive(Debug, Clone, Serialize, Deserialize)]
3382pub struct NamespaceExtractorConfig {
3383    pub provider: String,
3384    #[serde(skip_serializing_if = "Option::is_none")]
3385    pub model: Option<String>,
3386    #[serde(skip_serializing_if = "Option::is_none")]
3387    pub base_url: Option<String>,
3388}
3389
3390// ============================================================================
3391// Phase 2 Types — Cluster, Quotas, Backups, Ops
3392// ============================================================================
3393
3394/// Per-node replication lag entry.
3395#[derive(Debug, Clone, Serialize, Deserialize)]
3396pub struct NodeReplicationLag {
3397    pub node_id: String,
3398    pub lag_ms: u64,
3399    pub status: String,
3400}
3401
3402/// Response from `GET /admin/cluster/replication`.
3403#[derive(Debug, Clone, Serialize, Deserialize)]
3404pub struct ReplicationStatus {
3405    pub replication_factor: u32,
3406    pub healthy_replicas: u32,
3407    pub total_nodes: u32,
3408    #[serde(default)]
3409    pub replication_lag: Vec<NodeReplicationLag>,
3410}
3411
3412/// Shard information.
3413#[derive(Debug, Clone, Serialize, Deserialize)]
3414pub struct ShardInfo {
3415    pub shard_id: String,
3416    pub namespace: String,
3417    pub primary_node: String,
3418    #[serde(default)]
3419    pub replica_nodes: Vec<String>,
3420    pub state: String,
3421    pub vector_count: u64,
3422    pub size_bytes: u64,
3423}
3424
3425/// Response from `GET /admin/cluster/shards`.
3426#[derive(Debug, Clone, Serialize, Deserialize)]
3427pub struct ShardListResponse {
3428    pub shards: Vec<ShardInfo>,
3429    pub total: u32,
3430}
3431
3432/// Request for `POST /admin/cluster/shards/rebalance`.
3433#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3434pub struct ShardRebalanceRequest {
3435    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3436    pub shard_ids: Vec<String>,
3437    #[serde(default)]
3438    pub dry_run: bool,
3439}
3440
3441/// A planned shard move.
3442#[derive(Debug, Clone, Serialize, Deserialize)]
3443pub struct ShardMove {
3444    pub shard_id: String,
3445    pub from_node: String,
3446    pub to_node: String,
3447}
3448
3449/// Response from `POST /admin/cluster/shards/rebalance`.
3450#[derive(Debug, Clone, Serialize, Deserialize)]
3451pub struct ShardRebalanceResponse {
3452    pub initiated: bool,
3453    pub operation_id: String,
3454    pub shards_affected: u32,
3455    #[serde(skip_serializing_if = "Option::is_none")]
3456    pub estimated_seconds: Option<u64>,
3457    #[serde(default)]
3458    pub planned_moves: Vec<ShardMove>,
3459}
3460
3461/// Response from `GET /admin/cluster/maintenance`.
3462#[derive(Debug, Clone, Serialize, Deserialize)]
3463pub struct MaintenanceStatus {
3464    pub enabled: bool,
3465    #[serde(skip_serializing_if = "Option::is_none")]
3466    pub reason: Option<String>,
3467    #[serde(skip_serializing_if = "Option::is_none")]
3468    pub enabled_at: Option<u64>,
3469    #[serde(skip_serializing_if = "Option::is_none")]
3470    pub scheduled_end: Option<u64>,
3471    #[serde(default)]
3472    pub nodes_in_maintenance: Vec<String>,
3473    pub rejecting_requests: bool,
3474}
3475
3476/// Request for `POST /admin/cluster/maintenance/enable`.
3477#[derive(Debug, Clone, Serialize, Deserialize)]
3478pub struct EnableMaintenanceRequest {
3479    pub reason: String,
3480    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3481    pub node_ids: Vec<String>,
3482    #[serde(default)]
3483    pub reject_requests: bool,
3484    #[serde(skip_serializing_if = "Option::is_none")]
3485    pub duration_minutes: Option<u32>,
3486}
3487
3488/// Request for `POST /admin/cluster/maintenance/disable`.
3489#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3490pub struct DisableMaintenanceRequest {
3491    #[serde(skip_serializing_if = "Option::is_none")]
3492    pub force: Option<bool>,
3493}
3494
3495/// Quota configuration for a namespace.
3496#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3497pub struct QuotaConfig {
3498    #[serde(skip_serializing_if = "Option::is_none")]
3499    pub max_vectors: Option<u64>,
3500    #[serde(skip_serializing_if = "Option::is_none")]
3501    pub max_storage_bytes: Option<u64>,
3502    #[serde(skip_serializing_if = "Option::is_none")]
3503    pub max_dimensions: Option<usize>,
3504    #[serde(skip_serializing_if = "Option::is_none")]
3505    pub max_metadata_bytes: Option<usize>,
3506    #[serde(default)]
3507    pub enforcement: String,
3508}
3509
3510/// Quota usage for a namespace.
3511#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3512pub struct QuotaUsage {
3513    pub vector_count: u64,
3514    pub storage_bytes: u64,
3515    #[serde(skip_serializing_if = "Option::is_none")]
3516    pub avg_dimensions: Option<usize>,
3517    #[serde(skip_serializing_if = "Option::is_none")]
3518    pub avg_metadata_bytes: Option<usize>,
3519    pub last_updated: u64,
3520}
3521
3522/// Combined quota status.
3523#[derive(Debug, Clone, Serialize, Deserialize)]
3524pub struct QuotaStatus {
3525    pub namespace: String,
3526    pub config: QuotaConfig,
3527    pub usage: QuotaUsage,
3528    #[serde(skip_serializing_if = "Option::is_none")]
3529    pub vector_usage_percent: Option<f32>,
3530    #[serde(skip_serializing_if = "Option::is_none")]
3531    pub storage_usage_percent: Option<f32>,
3532    pub is_exceeded: bool,
3533    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3534    pub exceeded_quotas: Vec<String>,
3535}
3536
3537/// Response from `GET /admin/quotas`.
3538#[derive(Debug, Clone, Serialize, Deserialize)]
3539pub struct QuotaListResponse {
3540    pub quotas: Vec<QuotaStatus>,
3541    pub total: u64,
3542    #[serde(skip_serializing_if = "Option::is_none")]
3543    pub default_config: Option<QuotaConfig>,
3544}
3545
3546/// Response from `GET /admin/quotas/default`.
3547#[derive(Debug, Clone, Serialize, Deserialize)]
3548pub struct DefaultQuotaResponse {
3549    pub config: Option<QuotaConfig>,
3550}
3551
3552/// Request for `PUT /admin/quotas/default`.
3553#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3554pub struct SetDefaultQuotaRequest {
3555    pub config: Option<QuotaConfig>,
3556}
3557
3558/// Request for `PUT /admin/quotas/{namespace}`.
3559#[derive(Debug, Clone, Serialize, Deserialize)]
3560pub struct SetQuotaRequest {
3561    pub config: QuotaConfig,
3562}
3563
3564/// Response from `PUT /admin/quotas`.
3565#[derive(Debug, Clone, Serialize, Deserialize)]
3566pub struct SetQuotaResponse {
3567    pub success: bool,
3568    pub namespace: String,
3569    pub config: QuotaConfig,
3570    pub message: String,
3571}
3572
3573/// Request for `POST /admin/quotas/{namespace}/check`.
3574#[derive(Debug, Clone, Serialize, Deserialize)]
3575pub struct QuotaCheckRequest {
3576    pub vector_ids: Vec<String>,
3577    #[serde(skip_serializing_if = "Option::is_none")]
3578    pub dimensions: Option<usize>,
3579    #[serde(skip_serializing_if = "Option::is_none")]
3580    pub metadata_bytes: Option<usize>,
3581}
3582
3583/// Response from `POST /admin/quotas/{namespace}/check`.
3584#[derive(Debug, Clone, Serialize, Deserialize)]
3585pub struct QuotaCheckResult {
3586    pub allowed: bool,
3587    #[serde(skip_serializing_if = "Option::is_none")]
3588    pub reason: Option<String>,
3589    pub usage: QuotaUsage,
3590    #[serde(skip_serializing_if = "Option::is_none")]
3591    pub exceeded_quota: Option<String>,
3592}
3593
3594/// Backup information.
3595#[derive(Debug, Clone, Serialize, Deserialize)]
3596pub struct AdminBackupInfo {
3597    pub backup_id: String,
3598    pub name: String,
3599    pub backup_type: String,
3600    pub status: String,
3601    #[serde(default)]
3602    pub namespaces: Vec<String>,
3603    pub vector_count: u64,
3604    pub size_bytes: u64,
3605    pub created_at: u64,
3606    #[serde(skip_serializing_if = "Option::is_none")]
3607    pub completed_at: Option<u64>,
3608    #[serde(skip_serializing_if = "Option::is_none")]
3609    pub duration_seconds: Option<u64>,
3610    #[serde(skip_serializing_if = "Option::is_none")]
3611    pub storage_path: Option<String>,
3612    #[serde(skip_serializing_if = "Option::is_none")]
3613    pub error: Option<String>,
3614    pub encrypted: bool,
3615    #[serde(skip_serializing_if = "Option::is_none")]
3616    pub compression: Option<String>,
3617}
3618
3619/// Response from `GET /admin/backups`.
3620#[derive(Debug, Clone, Serialize, Deserialize)]
3621pub struct BackupListResponse {
3622    pub backups: Vec<AdminBackupInfo>,
3623    pub total: u64,
3624}
3625
3626/// Request for `POST /admin/backups`.
3627#[derive(Debug, Clone, Serialize, Deserialize)]
3628pub struct CreateBackupRequest {
3629    pub name: String,
3630    #[serde(skip_serializing_if = "Option::is_none")]
3631    pub backup_type: Option<String>,
3632    #[serde(skip_serializing_if = "Option::is_none")]
3633    pub namespaces: Option<Vec<String>>,
3634    #[serde(skip_serializing_if = "Option::is_none")]
3635    pub encrypt: Option<bool>,
3636    #[serde(skip_serializing_if = "Option::is_none")]
3637    pub compression: Option<String>,
3638}
3639
3640/// Response from `POST /admin/backups`.
3641#[derive(Debug, Clone, Serialize, Deserialize)]
3642pub struct CreateBackupResponse {
3643    pub backup: AdminBackupInfo,
3644    #[serde(skip_serializing_if = "Option::is_none")]
3645    pub estimated_completion: Option<u64>,
3646}
3647
3648/// Request for `POST /admin/backups/restore`.
3649#[derive(Debug, Clone, Serialize, Deserialize)]
3650pub struct RestoreBackupRequest {
3651    pub backup_id: String,
3652    #[serde(skip_serializing_if = "Option::is_none")]
3653    pub target_namespaces: Option<Vec<String>>,
3654    #[serde(skip_serializing_if = "Option::is_none")]
3655    pub overwrite: Option<bool>,
3656    #[serde(skip_serializing_if = "Option::is_none")]
3657    pub point_in_time: Option<u64>,
3658}
3659
3660/// Response from `POST /admin/backups/restore`.
3661#[derive(Debug, Clone, Serialize, Deserialize)]
3662pub struct RestoreBackupResponse {
3663    pub restore_id: String,
3664    pub status: String,
3665    pub backup_id: String,
3666    #[serde(default)]
3667    pub namespaces: Vec<String>,
3668    pub started_at: u64,
3669    #[serde(skip_serializing_if = "Option::is_none")]
3670    pub estimated_completion: Option<u64>,
3671    #[serde(skip_serializing_if = "Option::is_none")]
3672    pub progress_percent: Option<u8>,
3673    #[serde(skip_serializing_if = "Option::is_none")]
3674    pub vectors_restored: Option<u64>,
3675    #[serde(skip_serializing_if = "Option::is_none")]
3676    pub completed_at: Option<u64>,
3677    #[serde(skip_serializing_if = "Option::is_none")]
3678    pub duration_seconds: Option<u64>,
3679    #[serde(skip_serializing_if = "Option::is_none")]
3680    pub error: Option<String>,
3681}
3682
3683/// Backup schedule configuration.
3684#[derive(Debug, Clone, Serialize, Deserialize)]
3685pub struct BackupSchedule {
3686    pub enabled: bool,
3687    #[serde(skip_serializing_if = "Option::is_none")]
3688    pub cron: Option<String>,
3689    pub backup_type: String,
3690    pub retention_days: u32,
3691    pub max_backups: u32,
3692    #[serde(default)]
3693    pub namespaces: Vec<String>,
3694    pub encrypt: bool,
3695    #[serde(skip_serializing_if = "Option::is_none")]
3696    pub compression: Option<String>,
3697    #[serde(skip_serializing_if = "Option::is_none")]
3698    pub last_backup_at: Option<u64>,
3699    #[serde(skip_serializing_if = "Option::is_none")]
3700    pub next_backup_at: Option<u64>,
3701}
3702
3703/// Request for `POST /admin/backups/schedule`.
3704#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3705pub struct UpdateBackupScheduleRequest {
3706    #[serde(skip_serializing_if = "Option::is_none")]
3707    pub enabled: Option<bool>,
3708    #[serde(skip_serializing_if = "Option::is_none")]
3709    pub cron: Option<String>,
3710    #[serde(skip_serializing_if = "Option::is_none")]
3711    pub backup_type: Option<String>,
3712    #[serde(skip_serializing_if = "Option::is_none")]
3713    pub retention_days: Option<u32>,
3714    #[serde(skip_serializing_if = "Option::is_none")]
3715    pub max_backups: Option<u32>,
3716    #[serde(skip_serializing_if = "Option::is_none")]
3717    pub namespaces: Option<Vec<String>>,
3718    #[serde(skip_serializing_if = "Option::is_none")]
3719    pub encrypt: Option<bool>,
3720    #[serde(skip_serializing_if = "Option::is_none")]
3721    pub compression: Option<String>,
3722}
3723
3724// ============================================================================
3725// Route Query (POST /v1/route)
3726// ============================================================================
3727
3728fn default_route_top_k() -> usize {
3729    3
3730}
3731
3732fn default_route_min_similarity() -> f32 {
3733    0.3
3734}
3735
3736/// Request for `POST /v1/route`.
3737#[derive(Debug, Clone, Serialize, Deserialize)]
3738pub struct RouteRequest {
3739    /// The query string to route.
3740    pub query: String,
3741    /// Maximum number of matching routes to return.
3742    #[serde(default = "default_route_top_k")]
3743    pub top_k: usize,
3744    /// Minimum similarity threshold for route matches.
3745    #[serde(default = "default_route_min_similarity")]
3746    pub min_similarity: f32,
3747    /// Optional embedding model override.
3748    #[serde(skip_serializing_if = "Option::is_none")]
3749    pub model: Option<String>,
3750}
3751
3752/// A single route match returned by `POST /v1/route`.
3753#[derive(Debug, Clone, Serialize, Deserialize)]
3754pub struct RouteMatch {
3755    /// Matched namespace name.
3756    pub namespace: String,
3757    /// Cosine similarity score.
3758    pub similarity: f64,
3759    /// Optional namespace description.
3760    #[serde(skip_serializing_if = "Option::is_none")]
3761    pub description: Option<String>,
3762}
3763
3764/// Response from `POST /v1/route`.
3765#[derive(Debug, Clone, Serialize, Deserialize)]
3766pub struct RouteResponse {
3767    /// Ordered list of route matches.
3768    pub routes: Vec<RouteMatch>,
3769    /// Embedding model used.
3770    pub model: String,
3771    /// Time spent computing embeddings (milliseconds).
3772    pub embedding_time_ms: u64,
3773}
3774
3775// ============================================================================
3776// Import Job Status (GET /v1/import/{job_id}/status)
3777// ============================================================================
3778
3779/// Status of an import job returned by `GET /v1/import/{job_id}/status`.
3780#[derive(Debug, Clone, Serialize, Deserialize)]
3781pub struct ImportJobStatus {
3782    /// Unique job identifier.
3783    pub job_id: String,
3784    /// Current job status (e.g. "running", "completed", "failed").
3785    pub status: String,
3786    /// Import file format (e.g. "jsonl", "csv").
3787    pub format: String,
3788    /// Total records in the import.
3789    pub total: usize,
3790    /// Records successfully imported.
3791    pub imported: usize,
3792    /// Records skipped (duplicates, invalid).
3793    pub skipped: usize,
3794    /// Per-record error messages, if any.
3795    #[serde(default)]
3796    pub errors: Vec<String>,
3797    /// Unix timestamp when the job started.
3798    pub started_at: u64,
3799    /// Unix timestamp when the job finished, if completed.
3800    #[serde(skip_serializing_if = "Option::is_none")]
3801    pub finished_at: Option<u64>,
3802}
3803
3804// ============================================================================
3805// Storage Tier Overview (GET /admin/storage/tiers)
3806// ============================================================================
3807
3808/// Information about a single storage tier.
3809#[derive(Debug, Clone, Serialize, Deserialize)]
3810pub struct TierInfo {
3811    /// Tier name (e.g. "hot", "warm", "cold").
3812    pub name: String,
3813    /// Tier classification.
3814    pub tier_type: String,
3815    /// Underlying storage technology.
3816    pub technology: String,
3817    /// Human-readable tier description.
3818    pub description: String,
3819    /// Target access latency.
3820    pub target_latency: String,
3821    /// Optional capacity limit.
3822    #[serde(skip_serializing_if = "Option::is_none")]
3823    pub capacity: Option<String>,
3824    /// Current tier status.
3825    pub status: String,
3826    /// Number of items currently in this tier.
3827    pub current_count: u64,
3828    /// Number of cache/access hits.
3829    pub hit_count: u64,
3830    /// Hit rate as a fraction (0.0–1.0).
3831    pub hit_rate: f64,
3832}
3833
3834/// Tiered storage configuration parameters.
3835#[derive(Debug, Clone, Serialize, Deserialize)]
3836pub struct TierConfig {
3837    /// Maximum number of items in the hot tier.
3838    pub hot_tier_capacity: usize,
3839    /// Seconds of inactivity before promoting from hot to warm.
3840    pub hot_to_warm_threshold_secs: u64,
3841    /// Seconds of inactivity before demoting from warm to cold.
3842    pub warm_to_cold_threshold_secs: u64,
3843    /// Whether automatic tiering is enabled.
3844    pub auto_tier_enabled: bool,
3845    /// Interval between tier-check cycles (seconds).
3846    pub tier_check_interval_secs: u64,
3847}
3848
3849/// Tier movement activity counters.
3850#[derive(Debug, Clone, Serialize, Deserialize)]
3851pub struct TierActivity {
3852    /// Total promotions across all tiers.
3853    pub promotions: u64,
3854    /// Total demotions across all tiers.
3855    pub demotions: u64,
3856    /// Overall cache hit rate.
3857    pub cache_hit_rate: f64,
3858    /// Active storage backend name.
3859    pub storage_backend: String,
3860    /// Promotions specifically to the hot tier.
3861    pub promotions_to_hot: u64,
3862    /// Demotions specifically to warm.
3863    pub demotions_to_warm: u64,
3864    /// Demotions specifically to cold.
3865    pub demotions_to_cold: u64,
3866}
3867
3868/// Overview of the tiered storage system from `GET /admin/storage/tiers`.
3869#[derive(Debug, Clone, Serialize, Deserialize)]
3870pub struct StorageTierOverview {
3871    /// Whether tiered storage is enabled.
3872    pub tiers_enabled: bool,
3873    /// Description of each tier.
3874    pub architecture: Vec<TierInfo>,
3875    /// Current tier configuration.
3876    pub config: TierConfig,
3877    /// Tier movement activity.
3878    pub activity: TierActivity,
3879}
3880
3881// ============================================================================
3882// Memory Type Stats (GET /admin/memory-type-stats)
3883// ============================================================================
3884
3885/// Per-type memory statistics from `GET /admin/memory-type-stats`.
3886#[derive(Debug, Clone, Serialize, Deserialize)]
3887pub struct MemoryTypeStatsResponse {
3888    /// Total number of memories.
3889    pub total: u64,
3890    /// Working memory count.
3891    pub working: u64,
3892    /// Episodic memory count.
3893    pub episodic: u64,
3894    /// Semantic memory count.
3895    pub semantic: u64,
3896    /// Procedural memory count.
3897    pub procedural: u64,
3898    /// Number of distinct agent namespaces.
3899    pub agent_namespaces: u64,
3900}
3901
3902// ============================================================================
3903// Migrate Namespace Dimensions (POST /admin/namespaces/migrate-dimensions)
3904// ============================================================================
3905
3906fn default_target_dimension() -> usize {
3907    1024
3908}
3909
3910/// Request for `POST /admin/namespaces/migrate-dimensions`.
3911#[derive(Debug, Clone, Serialize, Deserialize)]
3912pub struct MigrateNamespaceDimensionsRequest {
3913    /// Namespaces to migrate (empty = all).
3914    #[serde(default)]
3915    pub namespaces: Vec<String>,
3916    /// Target embedding dimension.
3917    #[serde(default = "default_target_dimension")]
3918    pub target_dimension: usize,
3919}
3920
3921/// Per-namespace migration result.
3922#[derive(Debug, Clone, Serialize, Deserialize)]
3923pub struct NamespaceMigrationResult {
3924    /// Namespace that was migrated.
3925    pub namespace: String,
3926    /// Original embedding dimension.
3927    pub original_dimension: usize,
3928    /// Vectors successfully re-embedded.
3929    pub vectors_migrated: usize,
3930    /// Vectors skipped (already at target dimension).
3931    pub vectors_skipped: usize,
3932    /// Migration status for this namespace.
3933    pub status: String,
3934    /// Error message if migration failed for this namespace.
3935    #[serde(skip_serializing_if = "Option::is_none")]
3936    pub error: Option<String>,
3937}
3938
3939/// Response from `POST /admin/namespaces/migrate-dimensions`.
3940#[derive(Debug, Clone, Serialize, Deserialize)]
3941pub struct MigrateDimensionsResponse {
3942    /// Number of namespaces successfully migrated.
3943    pub migrated: usize,
3944    /// Number of namespaces that failed migration.
3945    pub failed: usize,
3946    /// Namespaces already at the target dimension.
3947    pub already_current: usize,
3948    /// Per-namespace results.
3949    pub results: Vec<NamespaceMigrationResult>,
3950}
3951
3952/// Request body for `POST /admin/reembed/drain` (v0.11.82+). All fields optional.
3953#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3954pub struct DrainReembedRequest {
3955    /// Hard wall-clock cap in seconds (default 600).
3956    #[serde(skip_serializing_if = "Option::is_none")]
3957    pub timeout_secs: Option<u64>,
3958    /// Candidates upgraded per cycle (default 10000).
3959    #[serde(skip_serializing_if = "Option::is_none")]
3960    pub batch_size: Option<usize>,
3961    /// Minimum importance to upgrade (default 0.0 — upgrade all statics).
3962    #[serde(skip_serializing_if = "Option::is_none")]
3963    pub min_importance: Option<f32>,
3964}
3965
3966/// Response from `POST /admin/reembed/drain` (v0.11.82+).
3967///
3968/// A [`remaining`][DrainReembedResponse::remaining] of `0` means all
3969/// `_embedding_kind=static` vectors have been upgraded to full ONNX quality.
3970#[derive(Debug, Clone, Serialize, Deserialize)]
3971pub struct DrainReembedResponse {
3972    /// Total vectors upgraded across all cycles in this drain.
3973    pub processed: usize,
3974    /// Static candidates still remaining (0 on a full drain).
3975    pub remaining: usize,
3976    /// Wall-clock duration of the drain in milliseconds.
3977    pub elapsed_ms: u128,
3978    /// Number of upgrade cycles executed.
3979    pub cycles: usize,
3980    /// `true` if the drain stopped on the timeout rather than reaching zero.
3981    pub timed_out: bool,
3982}