use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::types::{WarmCacheRequest, WarmCacheResponse};
use crate::DakeraClient;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpsStats {
pub version: String,
pub total_vectors: u64,
pub namespace_count: u64,
pub uptime_seconds: u64,
pub timestamp: u64,
pub state: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterStatus {
pub cluster_id: String,
pub state: String,
pub node_count: u32,
pub total_vectors: u64,
pub namespace_count: u64,
pub version: String,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redis_healthy: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfo {
pub node_id: String,
pub address: String,
pub role: String,
pub status: String,
pub version: String,
pub uptime_seconds: u64,
pub vector_count: u64,
pub memory_bytes: u64,
#[serde(default)]
pub cpu_percent: f32,
#[serde(default)]
pub memory_percent: f32,
pub last_heartbeat: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeListResponse {
pub nodes: Vec<NodeInfo>,
pub total: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexStats {
pub index_type: String,
pub is_built: bool,
pub size_bytes: u64,
pub indexed_vectors: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_rebuild: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NamespaceAdminInfo {
pub name: String,
pub vector_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub dimension: Option<usize>,
pub index_type: String,
pub storage_bytes: u64,
pub document_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<u64>,
pub index_stats: IndexStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NamespaceListResponse {
pub namespaces: Vec<NamespaceAdminInfo>,
pub total: u64,
pub total_vectors: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizeRequest {
#[serde(default)]
pub force: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_index_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizeResponse {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexStatsResponse {
pub namespaces: HashMap<String, IndexStats>,
pub total_indexed_vectors: u64,
pub total_size_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RebuildIndexRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_type: Option<String>,
#[serde(default)]
pub force: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RebuildIndexResponse {
pub success: bool,
pub job_id: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
pub enabled: bool,
pub cache_type: String,
pub entries: u64,
pub size_bytes: u64,
pub hits: u64,
pub misses: u64,
pub hit_rate: f64,
pub evictions: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClearCacheRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClearCacheResponse {
pub success: bool,
pub entries_cleared: u64,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_vectors_per_namespace: Option<u64>,
pub default_index_type: String,
pub cache_enabled: bool,
pub cache_max_size_bytes: u64,
pub rate_limit_enabled: bool,
pub rate_limit_rps: u32,
pub query_timeout_ms: u64,
#[serde(default = "default_true")]
pub autopilot_enabled: bool,
#[serde(default = "default_dedup_threshold")]
pub autopilot_dedup_threshold: f32,
#[serde(default = "default_dedup_interval")]
pub autopilot_dedup_interval_hours: u64,
#[serde(default = "default_consolidation_interval")]
pub autopilot_consolidation_interval_hours: u64,
}
fn default_true() -> bool {
true
}
fn default_dedup_threshold() -> f32 {
0.93
}
fn default_dedup_interval() -> u64 {
6
}
fn default_consolidation_interval() -> u64 {
12
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateConfigResponse {
pub success: bool,
pub config: RuntimeConfig,
pub message: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuotaConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_vectors: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_storage_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_queries_per_minute: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_writes_per_minute: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuotaUsage {
#[serde(default)]
pub current_vectors: u64,
#[serde(default)]
pub current_storage_bytes: u64,
#[serde(default)]
pub queries_this_minute: u64,
#[serde(default)]
pub writes_this_minute: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuotaStatus {
pub namespace: String,
pub config: QuotaConfig,
pub usage: QuotaUsage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuotaListResponse {
pub quotas: Vec<QuotaStatus>,
pub total: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_config: Option<QuotaConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlowQueryEntry {
pub id: String,
pub timestamp: u64,
pub namespace: String,
pub query_type: String,
pub duration_ms: f64,
#[serde(default)]
pub parameters: Option<serde_json::Value>,
#[serde(default)]
pub results_count: u64,
#[serde(default)]
pub vectors_scanned: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlowQueryListResponse {
pub queries: Vec<SlowQueryEntry>,
pub total: u64,
pub threshold_ms: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupInfo {
pub backup_id: String,
pub name: String,
pub backup_type: String,
pub status: String,
pub namespaces: Vec<String>,
pub vector_count: u64,
pub size_bytes: u64,
pub created_at: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub encrypted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub compression: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupListResponse {
pub backups: Vec<BackupInfo>,
pub total: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateBackupRequest {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub backup_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespaces: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypt: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compression: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateBackupResponse {
pub backup: BackupInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub estimated_completion: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreBackupRequest {
pub backup_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_namespaces: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub overwrite: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub point_in_time: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreBackupResponse {
pub restore_id: String,
pub status: String,
pub backup_id: String,
pub namespaces: Vec<String>,
pub started_at: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub estimated_completion: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress_percent: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vectors_restored: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoPilotConfig {
pub enabled: bool,
pub dedup_threshold: f32,
pub dedup_interval_hours: u64,
pub consolidation_interval_hours: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DedupResultSnapshot {
pub namespaces_processed: usize,
pub memories_scanned: usize,
pub duplicates_removed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsolidationResultSnapshot {
pub namespaces_processed: usize,
pub memories_scanned: usize,
pub clusters_merged: usize,
pub memories_consolidated: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoPilotStatusResponse {
pub config: AutoPilotConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_dedup_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_consolidation_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_dedup: Option<DedupResultSnapshot>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_consolidation: Option<ConsolidationResultSnapshot>,
pub total_dedup_removed: u64,
pub total_consolidated: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AutoPilotConfigRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dedup_threshold: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dedup_interval_hours: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consolidation_interval_hours: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoPilotConfigResponse {
pub success: bool,
pub config: AutoPilotConfig,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AutoPilotTriggerAction {
Dedup,
Consolidate,
All,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoPilotTriggerRequest {
pub action: AutoPilotTriggerAction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoPilotDedupResult {
pub namespaces_processed: usize,
pub memories_scanned: usize,
pub duplicates_removed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoPilotConsolidationResult {
pub namespaces_processed: usize,
pub memories_scanned: usize,
pub clusters_merged: usize,
pub memories_consolidated: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoPilotTriggerResponse {
pub success: bool,
pub action: AutoPilotTriggerAction,
#[serde(skip_serializing_if = "Option::is_none")]
pub dedup: Option<AutoPilotDedupResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consolidation: Option<AutoPilotConsolidationResult>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayConfigResponse {
pub strategy: String,
pub half_life_hours: f64,
pub min_importance: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DecayConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub strategy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub half_life_hours: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_importance: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayConfigUpdateResponse {
pub success: bool,
pub config: DecayConfigResponse,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LastDecayCycleStats {
pub namespaces_processed: usize,
pub memories_processed: usize,
pub memories_decayed: usize,
pub memories_deleted: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayStatsResponse {
pub total_decayed: u64,
pub total_deleted: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_run_at: Option<u64>,
pub cycles_run: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_cycle: Option<LastDecayCycleStats>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtlCleanupRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtlCleanupResponse {
pub success: bool,
pub vectors_removed: u64,
pub namespaces_cleaned: Vec<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtlStats {
pub namespace: String,
pub vectors_with_ttl: u64,
pub expiring_within_hour: u64,
pub expiring_within_day: u64,
pub expired_pending_cleanup: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtlStatsResponse {
pub namespaces: Vec<TtlStats>,
pub total_with_ttl: u64,
pub total_expired: u64,
}
impl DakeraClient {
pub async fn ops_stats(&self) -> Result<OpsStats> {
let url = format!("{}/v1/ops/stats", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn ops_metrics(&self) -> Result<String> {
let url = format!("{}/v1/ops/metrics", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_text_response(response).await
}
pub async fn debug_config(&self) -> Result<serde_json::Value> {
let url = format!("{}/debug/config", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn cluster_status(&self) -> Result<ClusterStatus> {
let url = format!("{}/v1/admin/cluster/status", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn cluster_nodes(&self) -> Result<NodeListResponse> {
let url = format!("{}/v1/admin/cluster/nodes", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn list_namespaces_admin(&self) -> Result<NamespaceListResponse> {
let url = format!("{}/v1/admin/namespaces", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn delete_namespace_admin(&self, namespace: &str) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/namespaces/{}", self.base_url, namespace);
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
pub async fn optimize_namespace(
&self,
namespace: &str,
request: OptimizeRequest,
) -> Result<OptimizeResponse> {
let url = format!(
"{}/v1/admin/namespaces/{}/optimize",
self.base_url, namespace
);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn index_stats(&self) -> Result<IndexStatsResponse> {
let url = format!("{}/v1/admin/indexes/stats", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn rebuild_indexes(
&self,
request: RebuildIndexRequest,
) -> Result<RebuildIndexResponse> {
let url = format!("{}/v1/admin/indexes/rebuild", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn cache_stats(&self) -> Result<CacheStats> {
let url = format!("{}/v1/admin/cache/stats", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn cache_clear(&self, namespace: Option<&str>) -> Result<ClearCacheResponse> {
let url = format!("{}/v1/admin/cache/clear", self.base_url);
let request = ClearCacheRequest {
namespace: namespace.map(|s| s.to_string()),
};
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn cache_warm(&self, request: WarmCacheRequest) -> Result<WarmCacheResponse> {
let url = format!("{}/v1/admin/cache/warm", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn get_config(&self) -> Result<RuntimeConfig> {
let url = format!("{}/v1/admin/config", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn update_config(
&self,
updates: HashMap<String, serde_json::Value>,
) -> Result<UpdateConfigResponse> {
let url = format!("{}/v1/admin/config", self.base_url);
let response = self.client.put(&url).json(&updates).send().await?;
self.handle_response(response).await
}
pub async fn get_quotas(&self) -> Result<QuotaListResponse> {
let url = format!("{}/v1/admin/quotas", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn get_quota(&self, namespace: &str) -> Result<QuotaStatus> {
let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn set_quota(
&self,
namespace: &str,
config: QuotaConfig,
) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
let request = serde_json::json!({ "config": config });
let response = self.client.put(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
pub async fn update_quotas(&self, config: Option<QuotaConfig>) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/quotas/default", self.base_url);
let request = serde_json::json!({ "config": config });
let response = self.client.put(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn slow_queries(
&self,
limit: Option<usize>,
namespace: Option<&str>,
query_type: Option<&str>,
) -> Result<SlowQueryListResponse> {
let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
let mut params = Vec::new();
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if let Some(ns) = namespace {
params.push(format!("namespace={}", ns));
}
if let Some(qt) = query_type {
params.push(format!("query_type={}", qt));
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn slow_query_summary(&self) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn clear_slow_queries(&self) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/slow-queries", self.base_url);
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
pub async fn create_backup(
&self,
request: CreateBackupRequest,
) -> Result<CreateBackupResponse> {
let url = format!("{}/v1/admin/backups", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn list_backups(&self) -> Result<BackupListResponse> {
let url = format!("{}/v1/admin/backups", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn get_backup(&self, backup_id: &str) -> Result<BackupInfo> {
let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn restore_backup(
&self,
request: RestoreBackupRequest,
) -> Result<RestoreBackupResponse> {
let url = format!("{}/v1/admin/backups/restore", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
pub async fn configure_ttl(
&self,
namespace: &str,
ttl_seconds: u64,
strategy: Option<&str>,
) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/namespaces/{}/ttl", self.base_url, namespace);
let mut body = serde_json::json!({ "ttl_seconds": ttl_seconds });
if let Some(s) = strategy {
body["strategy"] = serde_json::Value::String(s.to_string());
}
let response = self.client.post(&url).json(&body).send().await?;
self.handle_response(response).await
}
pub async fn ttl_cleanup(&self, namespace: Option<&str>) -> Result<TtlCleanupResponse> {
let url = format!("{}/v1/admin/ttl/cleanup", self.base_url);
let request = TtlCleanupRequest {
namespace: namespace.map(|s| s.to_string()),
};
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn ttl_stats(&self) -> Result<TtlStatsResponse> {
let url = format!("{}/v1/admin/ttl/stats", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn autopilot_status(&self) -> Result<AutoPilotStatusResponse> {
let url = format!("{}/v1/admin/autopilot/status", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn autopilot_update_config(
&self,
request: AutoPilotConfigRequest,
) -> Result<AutoPilotConfigResponse> {
let url = format!("{}/v1/admin/autopilot/config", self.base_url);
let response = self.client.put(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn autopilot_trigger(
&self,
action: AutoPilotTriggerAction,
) -> Result<AutoPilotTriggerResponse> {
let url = format!("{}/v1/admin/autopilot/trigger", self.base_url);
let request = AutoPilotTriggerRequest { action };
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn decay_config(&self) -> Result<DecayConfigResponse> {
let url = format!("{}/v1/admin/decay/config", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn decay_update_config(
&self,
request: DecayConfigUpdateRequest,
) -> Result<DecayConfigUpdateResponse> {
let url = format!("{}/v1/admin/decay/config", self.base_url);
let response = self.client.put(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn decay_stats(&self) -> Result<DecayStatsResponse> {
let url = format!("{}/v1/admin/decay/stats", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn get_kpis(&self) -> Result<KpiSnapshot> {
let url = format!("{}/v1/kpis", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_fulltext_reindex(
&self,
namespace: Option<&str>,
) -> Result<FulltextReindexResponse> {
let url = format!("{}/v1/admin/fulltext/reindex", self.base_url);
let body = serde_json::json!({ "namespace": namespace });
let response = self.client.post(&url).json(&body).send().await?;
self.handle_response(response).await
}
pub async fn admin_cluster_replication(&self) -> Result<crate::types::ReplicationStatus> {
let url = format!("{}/v1/admin/cluster/replication", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_list_shards(&self) -> Result<crate::types::ShardListResponse> {
let url = format!("{}/v1/admin/cluster/shards", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_rebalance_shards(
&self,
request: crate::types::ShardRebalanceRequest,
) -> Result<crate::types::ShardRebalanceResponse> {
let url = format!("{}/v1/admin/cluster/shards/rebalance", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_maintenance_status(&self) -> Result<crate::types::MaintenanceStatus> {
let url = format!("{}/v1/admin/cluster/maintenance", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_enable_maintenance(
&self,
request: crate::types::EnableMaintenanceRequest,
) -> Result<crate::types::MaintenanceStatus> {
let url = format!("{}/v1/admin/cluster/maintenance/enable", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_disable_maintenance(
&self,
request: crate::types::DisableMaintenanceRequest,
) -> Result<crate::types::MaintenanceStatus> {
let url = format!("{}/v1/admin/cluster/maintenance/disable", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_list_quotas(&self) -> Result<crate::types::QuotaListResponse> {
let url = format!("{}/v1/admin/quotas", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_get_default_quota(&self) -> Result<crate::types::DefaultQuotaResponse> {
let url = format!("{}/v1/admin/quotas/default", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_set_default_quota(
&self,
request: crate::types::SetDefaultQuotaRequest,
) -> Result<crate::types::SetQuotaResponse> {
let url = format!("{}/v1/admin/quotas/default", self.base_url);
let response = self.client.put(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_get_quota(&self, namespace: &str) -> Result<crate::types::QuotaStatus> {
let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_set_quota(
&self,
namespace: &str,
request: crate::types::SetQuotaRequest,
) -> Result<crate::types::SetQuotaResponse> {
let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
let response = self.client.put(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_check_quota(
&self,
namespace: &str,
request: crate::types::QuotaCheckRequest,
) -> Result<crate::types::QuotaCheckResult> {
let url = format!("{}/v1/admin/quotas/{}/check", self.base_url, namespace);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_list_slow_queries(
&self,
namespace: Option<&str>,
query_type: Option<&str>,
limit: Option<u32>,
) -> Result<Vec<serde_json::Value>> {
let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
let mut params = Vec::new();
if let Some(ns) = namespace {
params.push(format!("namespace={}", ns));
}
if let Some(qt) = query_type {
params.push(format!("query_type={}", qt));
}
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_slow_query_summary(&self) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_clear_slow_queries(
&self,
namespace: Option<&str>,
) -> Result<serde_json::Value> {
let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
if let Some(ns) = namespace {
url.push_str(&format!("?namespace={}", ns));
}
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_update_slow_query_config(
&self,
config: serde_json::Value,
) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/slow-queries/config", self.base_url);
let response = self.client.patch(&url).json(&config).send().await?;
self.handle_response(response).await
}
pub async fn admin_list_backups(&self) -> Result<crate::types::BackupListResponse> {
let url = format!("{}/v1/admin/backups", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_create_backup(
&self,
request: crate::types::CreateBackupRequest,
) -> Result<crate::types::CreateBackupResponse> {
let url = format!("{}/v1/admin/backups", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_get_backup(&self, backup_id: &str) -> Result<crate::types::AdminBackupInfo> {
let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_get_backup_schedule(&self) -> Result<crate::types::BackupSchedule> {
let url = format!("{}/v1/admin/backups/schedule", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn admin_update_backup_schedule(
&self,
request: crate::types::UpdateBackupScheduleRequest,
) -> Result<crate::types::BackupSchedule> {
let url = format!("{}/v1/admin/backups/schedule", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_restore_backup(
&self,
request: crate::types::RestoreBackupRequest,
) -> Result<crate::types::RestoreBackupResponse> {
let url = format!("{}/v1/admin/backups/restore", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_get_restore_status(
&self,
restore_id: &str,
) -> Result<crate::types::RestoreBackupResponse> {
let url = format!("{}/v1/admin/backups/restore/{}", self.base_url, restore_id);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn ops_diagnostics(&self) -> Result<serde_json::Value> {
let url = format!("{}/ops/diagnostics", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn ops_list_jobs(&self) -> Result<Vec<crate::types::JobInfo>> {
let url = format!("{}/ops/jobs", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn ops_get_job(&self, job_id: &str) -> Result<crate::types::JobInfo> {
let url = format!("{}/ops/jobs/{}", self.base_url, job_id);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn ops_compact(
&self,
request: crate::types::CompactionRequest,
) -> Result<crate::types::CompactionResponse> {
let url = format!("{}/ops/compact", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn ops_shutdown(&self) -> Result<serde_json::Value> {
let url = format!("{}/ops/shutdown", self.base_url);
let response = self.client.post(&url).send().await?;
self.handle_response(response).await
}
pub async fn download_backup(&self, backup_id: &str) -> Result<Vec<u8>> {
let url = format!("{}/v1/admin/backups/{}/download", self.base_url, backup_id);
let response = self.client.get(&url).send().await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(crate::error::ClientError::Server {
status: status.as_u16(),
message: body,
code: None,
});
}
Ok(response.bytes().await?.to_vec())
}
pub async fn upload_backup(&self, data: Vec<u8>) -> Result<crate::types::CreateBackupResponse> {
let url = format!("{}/v1/admin/backups/upload", self.base_url);
let response = self
.client
.post(&url)
.header("Content-Type", "application/gzip")
.body(data)
.send()
.await?;
self.handle_response(response).await
}
pub async fn storage_tier_overview(&self) -> Result<crate::types::StorageTierOverview> {
let url = format!("{}/v1/admin/storage/tiers", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn background_activity(&self) -> Result<serde_json::Value> {
let url = format!("{}/v1/admin/background-activity", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn memory_type_stats(&self) -> Result<crate::types::MemoryTypeStatsResponse> {
let url = format!("{}/v1/admin/memory-type-stats", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn migrate_namespace_dimensions(
&self,
request: crate::types::MigrateNamespaceDimensionsRequest,
) -> Result<crate::types::MigrateDimensionsResponse> {
let url = format!("{}/v1/admin/namespaces/migrate-dimensions", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn drain_reembed(
&self,
request: crate::types::DrainReembedRequest,
) -> Result<crate::types::DrainReembedResponse> {
let url = format!("{}/v1/admin/reembed/drain", self.base_url);
let response = self.client.post(&url).json(&request).send().await?;
self.handle_response(response).await
}
pub async fn admin_reembed_static_count(&self) -> Result<crate::types::StaticCountResponse> {
let url = format!("{}/v1/admin/reembed/static-count", self.base_url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KpiSnapshot {
pub recall_latency_p50_ms: f64,
pub recall_latency_p99_ms: f64,
pub store_latency_p50_ms: f64,
pub api_error_rate_5xx_pct: f64,
pub active_agents_count: u64,
pub session_count_week: u64,
pub cross_agent_network_node_count: u64,
pub memory_retention_7d_pct: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FulltextReindexNamespaceResult {
pub namespace: String,
pub vectors_scanned: usize,
pub newly_indexed: usize,
pub already_indexed: usize,
pub parse_failures: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FulltextReindexResponse {
pub namespaces_processed: usize,
pub total_indexed: usize,
pub total_skipped: usize,
pub details: Vec<FulltextReindexNamespaceResult>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ops_stats_deserializes() {
let json = r#"{
"version": "0.11.104",
"total_vectors": 100000,
"namespace_count": 5,
"uptime_seconds": 86400,
"timestamp": 1785000000,
"state": "healthy"
}"#;
let stats: OpsStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.version, "0.11.104");
assert_eq!(stats.total_vectors, 100000);
assert_eq!(stats.state, "healthy");
}
#[test]
fn test_cluster_status_deserializes_without_redis_healthy() {
let json = r#"{
"cluster_id": "cl-1",
"state": "active",
"node_count": 3,
"total_vectors": 500000,
"namespace_count": 10,
"version": "0.11.104",
"timestamp": 1785000000
}"#;
let cs: ClusterStatus = serde_json::from_str(json).unwrap();
assert_eq!(cs.cluster_id, "cl-1");
assert_eq!(cs.node_count, 3);
assert!(cs.redis_healthy.is_none());
}
#[test]
fn test_cluster_status_deserializes_with_redis_healthy() {
let json = r#"{
"cluster_id": "cl-2",
"state": "active",
"node_count": 1,
"total_vectors": 0,
"namespace_count": 0,
"version": "0.11.104",
"timestamp": 1785000000,
"redis_healthy": true
}"#;
let cs: ClusterStatus = serde_json::from_str(json).unwrap();
assert_eq!(cs.redis_healthy, Some(true));
}
#[test]
fn test_node_info_deserializes_with_defaults() {
let json = r#"{
"node_id": "n1",
"address": "10.0.0.1:8080",
"role": "primary",
"status": "online",
"version": "0.11.104",
"uptime_seconds": 3600,
"vector_count": 10000,
"memory_bytes": 1073741824,
"last_heartbeat": 1785000000
}"#;
let node: NodeInfo = serde_json::from_str(json).unwrap();
assert_eq!(node.node_id, "n1");
assert!((node.cpu_percent - 0.0).abs() < 1e-6);
assert!((node.memory_percent - 0.0).abs() < 1e-6);
}
#[test]
fn test_node_list_response_deserializes_empty() {
let json = r#"{"nodes": [], "total": 0}"#;
let resp: NodeListResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.total, 0);
assert!(resp.nodes.is_empty());
}
#[test]
fn test_index_stats_optional_last_rebuild_absent() {
let json = r#"{
"index_type": "hnsw",
"is_built": true,
"size_bytes": 2048,
"indexed_vectors": 100
}"#;
let stats: IndexStats = serde_json::from_str(json).unwrap();
assert!(stats.last_rebuild.is_none());
assert!(stats.is_built);
}
#[test]
fn test_index_stats_with_last_rebuild() {
let json = r#"{
"index_type": "flat",
"is_built": false,
"size_bytes": 0,
"indexed_vectors": 0,
"last_rebuild": 1785000000
}"#;
let stats: IndexStats = serde_json::from_str(json).unwrap();
assert_eq!(stats.last_rebuild, Some(1785000000));
}
#[test]
fn test_optimize_request_default_force_false() {
let req = OptimizeRequest {
force: false,
target_index_type: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"force\":false"));
assert!(!json.contains("target_index_type"));
}
#[test]
fn test_optimize_request_with_target_index_type() {
let req = OptimizeRequest {
force: true,
target_index_type: Some("hnsw".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"force\":true"));
assert!(json.contains("\"target_index_type\":\"hnsw\""));
}
#[test]
fn test_optimize_response_deserializes_with_job_id() {
let json = r#"{"success": true, "job_id": "job-abc", "message": "started"}"#;
let resp: OptimizeResponse = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert_eq!(resp.job_id.as_deref(), Some("job-abc"));
}
#[test]
fn test_optimize_response_deserializes_without_job_id() {
let json = r#"{"success": true, "message": "done"}"#;
let resp: OptimizeResponse = serde_json::from_str(json).unwrap();
assert!(resp.job_id.is_none());
}
#[test]
fn test_rebuild_index_request_all_optional_omitted() {
let req = RebuildIndexRequest {
namespace: None,
index_type: None,
force: false,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("namespace"));
assert!(!json.contains("index_type"));
assert!(json.contains("\"force\":false"));
}
#[test]
fn test_rebuild_index_request_with_namespace() {
let req = RebuildIndexRequest {
namespace: Some("prod".to_string()),
index_type: Some("hnsw".to_string()),
force: true,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"namespace\":\"prod\""));
assert!(json.contains("\"index_type\":\"hnsw\""));
assert!(json.contains("\"force\":true"));
}
#[test]
fn test_cache_stats_deserializes() {
let json = r#"{
"enabled": true,
"cache_type": "redis",
"entries": 1000,
"size_bytes": 524288,
"hits": 8000,
"misses": 2000,
"hit_rate": 0.8,
"evictions": 50
}"#;
let stats: CacheStats = serde_json::from_str(json).unwrap();
assert!(stats.enabled);
assert_eq!(stats.cache_type, "redis");
assert!((stats.hit_rate - 0.8).abs() < 1e-9);
}
#[test]
fn test_clear_cache_request_without_namespace_omits_field() {
let req = ClearCacheRequest { namespace: None };
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("namespace"));
}
#[test]
fn test_clear_cache_request_with_namespace() {
let req = ClearCacheRequest {
namespace: Some("prod-ns".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"namespace\":\"prod-ns\""));
}
#[test]
fn test_clear_cache_response_deserializes() {
let json = r#"{"success": true, "entries_cleared": 42, "message": "ok"}"#;
let resp: ClearCacheResponse = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert_eq!(resp.entries_cleared, 42);
}
#[test]
fn test_runtime_config_autopilot_defaults() {
let json = r#"{
"default_index_type": "hnsw",
"cache_enabled": true,
"cache_max_size_bytes": 1073741824,
"rate_limit_enabled": false,
"rate_limit_rps": 1000,
"query_timeout_ms": 5000
}"#;
let cfg: RuntimeConfig = serde_json::from_str(json).unwrap();
assert!(cfg.autopilot_enabled);
assert!((cfg.autopilot_dedup_threshold - 0.93).abs() < 1e-4);
assert_eq!(cfg.autopilot_dedup_interval_hours, 6);
assert_eq!(cfg.autopilot_consolidation_interval_hours, 12);
}
#[test]
fn test_update_config_response_warnings_default_empty() {
let json = r#"{
"success": true,
"config": {
"default_index_type": "hnsw",
"cache_enabled": false,
"cache_max_size_bytes": 0,
"rate_limit_enabled": false,
"rate_limit_rps": 0,
"query_timeout_ms": 5000
},
"message": "updated"
}"#;
let resp: UpdateConfigResponse = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert!(resp.warnings.is_empty());
}
#[test]
fn test_quota_config_all_none_serializes_empty() {
let cfg = QuotaConfig {
max_vectors: None,
max_storage_bytes: None,
max_queries_per_minute: None,
max_writes_per_minute: None,
};
let json = serde_json::to_string(&cfg).unwrap();
assert_eq!(json, "{}");
}
#[test]
fn test_quota_config_with_max_vectors() {
let cfg = QuotaConfig {
max_vectors: Some(1_000_000),
max_storage_bytes: None,
max_queries_per_minute: None,
max_writes_per_minute: None,
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("\"max_vectors\":1000000"));
assert!(!json.contains("max_storage"));
}
#[test]
fn test_quota_usage_all_fields_default_zero() {
let json = r#"{}"#;
let usage: QuotaUsage = serde_json::from_str(json).unwrap();
assert_eq!(usage.current_vectors, 0);
assert_eq!(usage.current_storage_bytes, 0);
assert_eq!(usage.queries_this_minute, 0);
assert_eq!(usage.writes_this_minute, 0);
}
#[test]
fn test_quota_list_response_optional_default_config() {
let json = r#"{"quotas": [], "total": 0}"#;
let resp: QuotaListResponse = serde_json::from_str(json).unwrap();
assert!(resp.default_config.is_none());
assert_eq!(resp.total, 0);
}
#[test]
fn test_slow_query_list_response_deserializes() {
let json = r#"{"queries": [], "total": 0, "threshold_ms": 100.0}"#;
let resp: SlowQueryListResponse = serde_json::from_str(json).unwrap();
assert!((resp.threshold_ms - 100.0).abs() < 1e-9);
assert!(resp.queries.is_empty());
}
#[test]
fn test_backup_info_deserializes_minimal() {
let json = r#"{
"backup_id": "bkp-1",
"name": "daily",
"backup_type": "full",
"status": "completed",
"namespaces": ["ns-1"],
"vector_count": 5000,
"size_bytes": 1048576,
"created_at": 1785000000,
"encrypted": true
}"#;
let bkp: BackupInfo = serde_json::from_str(json).unwrap();
assert_eq!(bkp.backup_id, "bkp-1");
assert!(bkp.encrypted);
assert!(bkp.completed_at.is_none());
assert!(bkp.error.is_none());
assert!(bkp.compression.is_none());
}
#[test]
fn test_create_backup_request_minimal_omits_optional() {
let req = CreateBackupRequest {
name: "snapshot".to_string(),
backup_type: None,
namespaces: None,
encrypt: None,
compression: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"name\":\"snapshot\""));
assert!(!json.contains("backup_type"));
assert!(!json.contains("namespaces"));
assert!(!json.contains("encrypt"));
assert!(!json.contains("compression"));
}
#[test]
fn test_create_backup_request_with_namespaces() {
let req = CreateBackupRequest {
name: "ns-backup".to_string(),
backup_type: Some("incremental".to_string()),
namespaces: Some(vec!["ns-a".to_string(), "ns-b".to_string()]),
encrypt: Some(true),
compression: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"backup_type\":\"incremental\""));
assert!(json.contains("\"ns-a\""));
assert!(json.contains("\"encrypt\":true"));
}
#[test]
fn test_namespace_list_response_deserializes() {
let json = r#"{"namespaces": [], "total": 0, "total_vectors": 0}"#;
let resp: NamespaceListResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.total, 0);
assert_eq!(resp.total_vectors, 0);
}
#[test]
fn test_index_stats_response_deserializes_with_hashmap() {
let json = r#"{
"namespaces": {
"default": {
"index_type": "hnsw",
"is_built": true,
"size_bytes": 4096,
"indexed_vectors": 200
}
},
"total_indexed_vectors": 200,
"total_size_bytes": 4096
}"#;
let resp: IndexStatsResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.total_indexed_vectors, 200);
assert!(resp.namespaces.contains_key("default"));
}
}