Skip to main content

dakera_client/
types.rs

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