1use reqwest::{Client, StatusCode};
4use std::time::Duration;
5use tracing::{debug, instrument};
6
7use crate::error::{ClientError, Result};
8use crate::types::*;
9
10const DEFAULT_TIMEOUT_SECS: u64 = 30;
12
13#[derive(Debug, Clone)]
15pub struct DakeraClient {
16 pub(crate) client: Client,
18 pub(crate) base_url: String,
20}
21
22impl DakeraClient {
23 pub fn new(base_url: impl Into<String>) -> Result<Self> {
33 DakeraClientBuilder::new(base_url).build()
34 }
35
36 pub fn builder(base_url: impl Into<String>) -> DakeraClientBuilder {
38 DakeraClientBuilder::new(base_url)
39 }
40
41 #[instrument(skip(self))]
47 pub async fn health(&self) -> Result<HealthResponse> {
48 let url = format!("{}/health", self.base_url);
49 let response = self.client.get(&url).send().await?;
50
51 if response.status().is_success() {
52 Ok(response.json().await?)
53 } else {
54 Ok(HealthResponse {
56 healthy: true,
57 version: None,
58 uptime_seconds: None,
59 })
60 }
61 }
62
63 #[instrument(skip(self))]
65 pub async fn ready(&self) -> Result<ReadinessResponse> {
66 let url = format!("{}/health/ready", self.base_url);
67 let response = self.client.get(&url).send().await?;
68
69 if response.status().is_success() {
70 Ok(response.json().await?)
71 } else {
72 Ok(ReadinessResponse {
73 ready: false,
74 components: None,
75 })
76 }
77 }
78
79 #[instrument(skip(self))]
81 pub async fn live(&self) -> Result<bool> {
82 let url = format!("{}/health/live", self.base_url);
83 let response = self.client.get(&url).send().await?;
84 Ok(response.status().is_success())
85 }
86
87 #[instrument(skip(self))]
93 pub async fn list_namespaces(&self) -> Result<Vec<String>> {
94 let url = format!("{}/v1/namespaces", self.base_url);
95 let response = self.client.get(&url).send().await?;
96 self.handle_response::<ListNamespacesResponse>(response)
97 .await
98 .map(|r| r.namespaces)
99 }
100
101 #[instrument(skip(self))]
103 pub async fn get_namespace(&self, namespace: &str) -> Result<NamespaceInfo> {
104 let url = format!("{}/v1/namespaces/{}", self.base_url, namespace);
105 let response = self.client.get(&url).send().await?;
106 self.handle_response(response).await
107 }
108
109 #[instrument(skip(self, request))]
111 pub async fn create_namespace(
112 &self,
113 namespace: &str,
114 request: CreateNamespaceRequest,
115 ) -> Result<NamespaceInfo> {
116 let url = format!("{}/v1/namespaces/{}", self.base_url, namespace);
117 let response = self.client.post(&url).json(&request).send().await?;
118 self.handle_response(response).await
119 }
120
121 #[instrument(skip(self, request), fields(namespace = %namespace))]
127 pub async fn configure_namespace(
128 &self,
129 namespace: &str,
130 request: ConfigureNamespaceRequest,
131 ) -> Result<ConfigureNamespaceResponse> {
132 let url = format!("{}/v1/namespaces/{}", self.base_url, namespace);
133 let response = self.client.put(&url).json(&request).send().await?;
134 self.handle_response(response).await
135 }
136
137 #[instrument(skip(self, request), fields(vector_count = request.vectors.len()))]
143 pub async fn upsert(&self, namespace: &str, request: UpsertRequest) -> Result<UpsertResponse> {
144 let url = format!("{}/v1/namespaces/{}/vectors", self.base_url, namespace);
145 debug!(
146 "Upserting {} vectors to {}",
147 request.vectors.len(),
148 namespace
149 );
150
151 let response = self.client.post(&url).json(&request).send().await?;
152 self.handle_response(response).await
153 }
154
155 #[instrument(skip(self, vector))]
157 pub async fn upsert_one(&self, namespace: &str, vector: Vector) -> Result<UpsertResponse> {
158 self.upsert(namespace, UpsertRequest::single(vector)).await
159 }
160
161 #[instrument(skip(self, request), fields(namespace = %namespace, count = request.ids.len()))]
194 pub async fn upsert_columns(
195 &self,
196 namespace: &str,
197 request: ColumnUpsertRequest,
198 ) -> Result<UpsertResponse> {
199 let url = format!(
200 "{}/v1/namespaces/{}/upsert-columns",
201 self.base_url, namespace
202 );
203 debug!(
204 "Upserting {} vectors in column format to {}",
205 request.ids.len(),
206 namespace
207 );
208
209 let response = self.client.post(&url).json(&request).send().await?;
210 self.handle_response(response).await
211 }
212
213 #[instrument(skip(self, request), fields(top_k = request.top_k))]
215 pub async fn query(&self, namespace: &str, request: QueryRequest) -> Result<QueryResponse> {
216 let url = format!("{}/v1/namespaces/{}/query", self.base_url, namespace);
217 debug!(
218 "Querying namespace {} for top {} results",
219 namespace, request.top_k
220 );
221
222 let response = self.client.post(&url).json(&request).send().await?;
223 self.handle_response(response).await
224 }
225
226 #[instrument(skip(self, vector))]
228 pub async fn query_simple(
229 &self,
230 namespace: &str,
231 vector: Vec<f32>,
232 top_k: u32,
233 ) -> Result<QueryResponse> {
234 self.query(namespace, QueryRequest::new(vector, top_k))
235 .await
236 }
237
238 #[instrument(skip(self, request), fields(namespace = %namespace, query_count = request.queries.len()))]
262 pub async fn batch_query(
263 &self,
264 namespace: &str,
265 request: BatchQueryRequest,
266 ) -> Result<BatchQueryResponse> {
267 let url = format!("{}/v1/namespaces/{}/batch-query", self.base_url, namespace);
268 debug!(
269 "Batch querying namespace {} with {} queries",
270 namespace,
271 request.queries.len()
272 );
273
274 let response = self.client.post(&url).json(&request).send().await?;
275 self.handle_response(response).await
276 }
277
278 #[instrument(skip(self, request), fields(id_count = request.ids.len()))]
280 pub async fn delete(&self, namespace: &str, request: DeleteRequest) -> Result<DeleteResponse> {
281 let url = format!(
282 "{}/v1/namespaces/{}/vectors/delete",
283 self.base_url, namespace
284 );
285 debug!("Deleting {} vectors from {}", request.ids.len(), namespace);
286
287 let response = self.client.post(&url).json(&request).send().await?;
288 self.handle_response(response).await
289 }
290
291 #[instrument(skip(self))]
293 pub async fn delete_one(&self, namespace: &str, id: &str) -> Result<DeleteResponse> {
294 self.delete(namespace, DeleteRequest::single(id)).await
295 }
296
297 #[instrument(skip(self, request), fields(doc_count = request.documents.len()))]
303 pub async fn index_documents(
304 &self,
305 namespace: &str,
306 request: IndexDocumentsRequest,
307 ) -> Result<IndexDocumentsResponse> {
308 let url = format!(
309 "{}/v1/namespaces/{}/fulltext/index",
310 self.base_url, namespace
311 );
312 debug!(
313 "Indexing {} documents in {}",
314 request.documents.len(),
315 namespace
316 );
317
318 let response = self.client.post(&url).json(&request).send().await?;
319 self.handle_response(response).await
320 }
321
322 #[instrument(skip(self, document))]
324 pub async fn index_document(
325 &self,
326 namespace: &str,
327 document: Document,
328 ) -> Result<IndexDocumentsResponse> {
329 self.index_documents(
330 namespace,
331 IndexDocumentsRequest {
332 documents: vec![document],
333 },
334 )
335 .await
336 }
337
338 #[instrument(skip(self, request))]
340 pub async fn fulltext_search(
341 &self,
342 namespace: &str,
343 request: FullTextSearchRequest,
344 ) -> Result<FullTextSearchResponse> {
345 let url = format!(
346 "{}/v1/namespaces/{}/fulltext/search",
347 self.base_url, namespace
348 );
349 debug!("Full-text search in {} for: {}", namespace, request.query);
350
351 let response = self.client.post(&url).json(&request).send().await?;
352 self.handle_response(response).await
353 }
354
355 #[instrument(skip(self))]
357 pub async fn search_text(
358 &self,
359 namespace: &str,
360 query: &str,
361 top_k: u32,
362 ) -> Result<FullTextSearchResponse> {
363 self.fulltext_search(namespace, FullTextSearchRequest::new(query, top_k))
364 .await
365 }
366
367 #[instrument(skip(self))]
369 pub async fn fulltext_stats(&self, namespace: &str) -> Result<FullTextStats> {
370 let url = format!(
371 "{}/v1/namespaces/{}/fulltext/stats",
372 self.base_url, namespace
373 );
374 let response = self.client.get(&url).send().await?;
375 self.handle_response(response).await
376 }
377
378 #[instrument(skip(self, request))]
380 pub async fn fulltext_delete(
381 &self,
382 namespace: &str,
383 request: DeleteRequest,
384 ) -> Result<DeleteResponse> {
385 let url = format!(
386 "{}/v1/namespaces/{}/fulltext/delete",
387 self.base_url, namespace
388 );
389 let response = self.client.post(&url).json(&request).send().await?;
390 self.handle_response(response).await
391 }
392
393 #[instrument(skip(self, request), fields(top_k = request.top_k))]
399 pub async fn hybrid_search(
400 &self,
401 namespace: &str,
402 request: HybridSearchRequest,
403 ) -> Result<HybridSearchResponse> {
404 let url = format!("{}/v1/namespaces/{}/hybrid", self.base_url, namespace);
405 debug!(
406 "Hybrid search in {} with vector_weight={}",
407 namespace, request.vector_weight
408 );
409
410 let response = self.client.post(&url).json(&request).send().await?;
411 self.handle_response(response).await
412 }
413
414 #[instrument(skip(self, request), fields(namespace = %namespace))]
451 pub async fn multi_vector_search(
452 &self,
453 namespace: &str,
454 request: MultiVectorSearchRequest,
455 ) -> Result<MultiVectorSearchResponse> {
456 let url = format!("{}/v1/namespaces/{}/multi-vector", self.base_url, namespace);
457 debug!(
458 "Multi-vector search in {} with {} positive vectors",
459 namespace,
460 request.positive_vectors.len()
461 );
462
463 let response = self.client.post(&url).json(&request).send().await?;
464 self.handle_response(response).await
465 }
466
467 #[instrument(skip(self, request), fields(namespace = %namespace))]
501 pub async fn aggregate(
502 &self,
503 namespace: &str,
504 request: AggregationRequest,
505 ) -> Result<AggregationResponse> {
506 let url = format!("{}/v1/namespaces/{}/aggregate", self.base_url, namespace);
507 debug!(
508 "Aggregating in namespace {} with {} aggregations",
509 namespace,
510 request.aggregate_by.len()
511 );
512
513 let response = self.client.post(&url).json(&request).send().await?;
514 self.handle_response(response).await
515 }
516
517 #[instrument(skip(self, request), fields(namespace = %namespace))]
555 pub async fn unified_query(
556 &self,
557 namespace: &str,
558 request: UnifiedQueryRequest,
559 ) -> Result<UnifiedQueryResponse> {
560 let url = format!(
561 "{}/v1/namespaces/{}/unified-query",
562 self.base_url, namespace
563 );
564 debug!(
565 "Unified query in namespace {} with top_k={}",
566 namespace, request.top_k
567 );
568
569 let response = self.client.post(&url).json(&request).send().await?;
570 self.handle_response(response).await
571 }
572
573 #[instrument(skip(self, vector))]
577 pub async fn unified_vector_search(
578 &self,
579 namespace: &str,
580 vector: Vec<f32>,
581 top_k: usize,
582 ) -> Result<UnifiedQueryResponse> {
583 self.unified_query(namespace, UnifiedQueryRequest::vector_search(vector, top_k))
584 .await
585 }
586
587 #[instrument(skip(self))]
591 pub async fn unified_text_search(
592 &self,
593 namespace: &str,
594 field: &str,
595 query: &str,
596 top_k: usize,
597 ) -> Result<UnifiedQueryResponse> {
598 self.unified_query(
599 namespace,
600 UnifiedQueryRequest::fulltext_search(field, query, top_k),
601 )
602 .await
603 }
604
605 #[instrument(skip(self, request), fields(namespace = %namespace))]
642 pub async fn explain_query(
643 &self,
644 namespace: &str,
645 request: QueryExplainRequest,
646 ) -> Result<QueryExplainResponse> {
647 let url = format!("{}/v1/namespaces/{}/explain", self.base_url, namespace);
648 debug!(
649 "Explaining query in namespace {} (query_type={:?}, top_k={})",
650 namespace, request.query_type, request.top_k
651 );
652
653 let response = self.client.post(&url).json(&request).send().await?;
654 self.handle_response(response).await
655 }
656
657 #[instrument(skip(self, request), fields(namespace = %request.namespace, priority = ?request.priority))]
685 pub async fn warm_cache(&self, request: WarmCacheRequest) -> Result<WarmCacheResponse> {
686 let url = format!(
687 "{}/v1/namespaces/{}/cache/warm",
688 self.base_url, request.namespace
689 );
690 debug!(
691 "Warming cache for namespace {} with priority {:?}",
692 request.namespace, request.priority
693 );
694
695 let response = self.client.post(&url).json(&request).send().await?;
696 self.handle_response(response).await
697 }
698
699 #[instrument(skip(self, vector_ids))]
701 pub async fn warm_vectors(
702 &self,
703 namespace: &str,
704 vector_ids: Vec<String>,
705 ) -> Result<WarmCacheResponse> {
706 self.warm_cache(WarmCacheRequest::new(namespace).with_vector_ids(vector_ids))
707 .await
708 }
709
710 #[instrument(skip(self, request), fields(namespace = %namespace))]
743 pub async fn export(&self, namespace: &str, request: ExportRequest) -> Result<ExportResponse> {
744 let url = format!("{}/v1/namespaces/{}/export", self.base_url, namespace);
745 debug!(
746 "Exporting vectors from namespace {} (top_k={}, cursor={:?})",
747 namespace, request.top_k, request.cursor
748 );
749
750 let response = self.client.post(&url).json(&request).send().await?;
751 self.handle_response(response).await
752 }
753
754 #[instrument(skip(self))]
758 pub async fn export_all(&self, namespace: &str) -> Result<ExportResponse> {
759 self.export(namespace, ExportRequest::new()).await
760 }
761
762 #[instrument(skip(self))]
768 pub async fn diagnostics(&self) -> Result<SystemDiagnostics> {
769 let url = format!("{}/ops/diagnostics", self.base_url);
770 let response = self.client.get(&url).send().await?;
771 self.handle_response(response).await
772 }
773
774 #[instrument(skip(self))]
776 pub async fn list_jobs(&self) -> Result<Vec<JobInfo>> {
777 let url = format!("{}/ops/jobs", self.base_url);
778 let response = self.client.get(&url).send().await?;
779 self.handle_response(response).await
780 }
781
782 #[instrument(skip(self))]
784 pub async fn get_job(&self, job_id: &str) -> Result<Option<JobInfo>> {
785 let url = format!("{}/ops/jobs/{}", self.base_url, job_id);
786 let response = self.client.get(&url).send().await?;
787
788 if response.status() == StatusCode::NOT_FOUND {
789 return Ok(None);
790 }
791
792 self.handle_response(response).await.map(Some)
793 }
794
795 #[instrument(skip(self, request))]
797 pub async fn compact(&self, request: CompactionRequest) -> Result<CompactionResponse> {
798 let url = format!("{}/ops/compact", self.base_url);
799 let response = self.client.post(&url).json(&request).send().await?;
800 self.handle_response(response).await
801 }
802
803 #[instrument(skip(self))]
805 pub async fn shutdown(&self) -> Result<()> {
806 let url = format!("{}/ops/shutdown", self.base_url);
807 let response = self.client.post(&url).send().await?;
808
809 if response.status().is_success() {
810 Ok(())
811 } else {
812 let status = response.status().as_u16();
813 let text = response.text().await.unwrap_or_default();
814 Err(ClientError::Server {
815 status,
816 message: text,
817 })
818 }
819 }
820
821 #[instrument(skip(self, request), fields(id_count = request.ids.len()))]
827 pub async fn fetch(&self, namespace: &str, request: FetchRequest) -> Result<FetchResponse> {
828 let url = format!("{}/v1/namespaces/{}/fetch", self.base_url, namespace);
829 debug!("Fetching {} vectors from {}", request.ids.len(), namespace);
830 let response = self.client.post(&url).json(&request).send().await?;
831 self.handle_response(response).await
832 }
833
834 #[instrument(skip(self))]
836 pub async fn fetch_by_ids(&self, namespace: &str, ids: &[&str]) -> Result<Vec<Vector>> {
837 let request = FetchRequest::new(ids.iter().map(|s| s.to_string()).collect());
838 self.fetch(namespace, request).await.map(|r| r.vectors)
839 }
840
841 #[instrument(skip(self, request), fields(doc_count = request.documents.len()))]
847 pub async fn upsert_text(
848 &self,
849 namespace: &str,
850 request: UpsertTextRequest,
851 ) -> Result<TextUpsertResponse> {
852 let url = format!("{}/v1/namespaces/{}/upsert-text", self.base_url, namespace);
853 debug!(
854 "Upserting {} text documents to {}",
855 request.documents.len(),
856 namespace
857 );
858 let response = self.client.post(&url).json(&request).send().await?;
859 self.handle_response(response).await
860 }
861
862 #[instrument(skip(self, request), fields(top_k = request.top_k))]
864 pub async fn query_text(
865 &self,
866 namespace: &str,
867 request: QueryTextRequest,
868 ) -> Result<TextQueryResponse> {
869 let url = format!("{}/v1/namespaces/{}/query-text", self.base_url, namespace);
870 debug!("Text query in {} for: {}", namespace, request.text);
871 let response = self.client.post(&url).json(&request).send().await?;
872 self.handle_response(response).await
873 }
874
875 #[instrument(skip(self))]
877 pub async fn query_text_simple(
878 &self,
879 namespace: &str,
880 text: &str,
881 top_k: u32,
882 ) -> Result<TextQueryResponse> {
883 self.query_text(namespace, QueryTextRequest::new(text, top_k))
884 .await
885 }
886
887 #[instrument(skip(self, request), fields(query_count = request.queries.len()))]
889 pub async fn batch_query_text(
890 &self,
891 namespace: &str,
892 request: BatchQueryTextRequest,
893 ) -> Result<BatchQueryTextResponse> {
894 let url = format!(
895 "{}/v1/namespaces/{}/batch-query-text",
896 self.base_url, namespace
897 );
898 debug!(
899 "Batch text query in {} with {} queries",
900 namespace,
901 request.queries.len()
902 );
903 let response = self.client.post(&url).json(&request).send().await?;
904 self.handle_response(response).await
905 }
906
907 pub(crate) async fn handle_response<T: serde::de::DeserializeOwned>(
913 &self,
914 response: reqwest::Response,
915 ) -> Result<T> {
916 let status = response.status();
917
918 if status.is_success() {
919 Ok(response.json().await?)
920 } else {
921 let status_code = status.as_u16();
922 let text = response.text().await.unwrap_or_default();
923
924 let message = if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
926 json.get("error")
927 .and_then(|e| e.as_str())
928 .unwrap_or(&text)
929 .to_string()
930 } else {
931 text
932 };
933
934 Err(ClientError::Server {
935 status: status_code,
936 message,
937 })
938 }
939 }
940}
941
942#[derive(Debug)]
944pub struct DakeraClientBuilder {
945 base_url: String,
946 timeout: Duration,
947 user_agent: Option<String>,
948}
949
950impl DakeraClientBuilder {
951 pub fn new(base_url: impl Into<String>) -> Self {
953 Self {
954 base_url: base_url.into(),
955 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
956 user_agent: None,
957 }
958 }
959
960 pub fn timeout(mut self, timeout: Duration) -> Self {
962 self.timeout = timeout;
963 self
964 }
965
966 pub fn timeout_secs(mut self, secs: u64) -> Self {
968 self.timeout = Duration::from_secs(secs);
969 self
970 }
971
972 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
974 self.user_agent = Some(user_agent.into());
975 self
976 }
977
978 pub fn build(self) -> Result<DakeraClient> {
980 let base_url = self.base_url.trim_end_matches('/').to_string();
982
983 if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
985 return Err(ClientError::InvalidUrl(
986 "URL must start with http:// or https://".to_string(),
987 ));
988 }
989
990 let user_agent = self
991 .user_agent
992 .unwrap_or_else(|| format!("dakera-client/{}", env!("CARGO_PKG_VERSION")));
993
994 let client = Client::builder()
995 .timeout(self.timeout)
996 .user_agent(user_agent)
997 .build()
998 .map_err(|e| ClientError::Config(e.to_string()))?;
999
1000 Ok(DakeraClient { client, base_url })
1001 }
1002}
1003
1004impl DakeraClient {
1009 pub async fn stream_namespace_events(
1034 &self,
1035 namespace: &str,
1036 ) -> Result<tokio::sync::mpsc::Receiver<Result<crate::events::DakeraEvent>>> {
1037 let url = format!(
1038 "{}/v1/namespaces/{}/events",
1039 self.base_url,
1040 urlencoding::encode(namespace)
1041 );
1042 self.stream_sse(url).await
1043 }
1044
1045 pub async fn stream_global_events(
1052 &self,
1053 ) -> Result<tokio::sync::mpsc::Receiver<Result<crate::events::DakeraEvent>>> {
1054 let url = format!("{}/ops/events", self.base_url);
1055 self.stream_sse(url).await
1056 }
1057
1058 pub async fn stream_memory_events(
1067 &self,
1068 ) -> Result<tokio::sync::mpsc::Receiver<Result<crate::events::MemoryEvent>>> {
1069 let url = format!("{}/v1/events/stream", self.base_url);
1070 self.stream_sse(url).await
1071 }
1072
1073 async fn stream_sse<T>(&self, url: String) -> Result<tokio::sync::mpsc::Receiver<Result<T>>>
1075 where
1076 T: serde::de::DeserializeOwned + Send + 'static,
1077 {
1078 use futures_util::StreamExt;
1079
1080 let response = self
1081 .client
1082 .get(&url)
1083 .header("Accept", "text/event-stream")
1084 .header("Cache-Control", "no-cache")
1085 .send()
1086 .await?;
1087
1088 if !response.status().is_success() {
1089 let status = response.status().as_u16();
1090 let body = response.text().await.unwrap_or_default();
1091 return Err(ClientError::Server {
1092 status,
1093 message: body,
1094 });
1095 }
1096
1097 let (tx, rx) = tokio::sync::mpsc::channel(64);
1098
1099 tokio::spawn(async move {
1100 let mut byte_stream = response.bytes_stream();
1101 let mut remaining = String::new();
1102 let mut data_lines: Vec<String> = Vec::new();
1103
1104 while let Some(chunk) = byte_stream.next().await {
1105 match chunk {
1106 Ok(bytes) => {
1107 remaining.push_str(&String::from_utf8_lossy(&bytes));
1108 while let Some(pos) = remaining.find('\n') {
1109 let raw = &remaining[..pos];
1110 let line = raw.trim_end_matches('\r').to_string();
1111 remaining = remaining[pos + 1..].to_string();
1112
1113 if line.starts_with(':') {
1114 } else if let Some(data) = line.strip_prefix("data:") {
1116 data_lines.push(data.trim_start().to_string());
1117 } else if line.is_empty() {
1118 if !data_lines.is_empty() {
1119 let payload = data_lines.join("\n");
1120 data_lines.clear();
1121 let result = serde_json::from_str::<T>(&payload)
1122 .map_err(ClientError::Json);
1123 if tx.send(result).await.is_err() {
1124 return; }
1126 }
1127 } else {
1128 }
1130 }
1131 }
1132 Err(e) => {
1133 let _ = tx.send(Err(ClientError::Http(e))).await;
1134 return;
1135 }
1136 }
1137 }
1138 });
1139
1140 Ok(rx)
1141 }
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146 use super::*;
1147
1148 #[test]
1149 fn test_client_builder() {
1150 let client = DakeraClient::new("http://localhost:3000");
1151 assert!(client.is_ok());
1152 }
1153
1154 #[test]
1155 fn test_client_builder_with_options() {
1156 let client = DakeraClient::builder("http://localhost:3000")
1157 .timeout_secs(60)
1158 .user_agent("test-client/1.0")
1159 .build();
1160 assert!(client.is_ok());
1161 }
1162
1163 #[test]
1164 fn test_client_builder_invalid_url() {
1165 let client = DakeraClient::new("invalid-url");
1166 assert!(client.is_err());
1167 }
1168
1169 #[test]
1170 fn test_client_builder_trailing_slash() {
1171 let client = DakeraClient::new("http://localhost:3000/").unwrap();
1172 assert!(!client.base_url.ends_with('/'));
1173 }
1174
1175 #[test]
1176 fn test_vector_creation() {
1177 let v = Vector::new("test", vec![0.1, 0.2, 0.3]);
1178 assert_eq!(v.id, "test");
1179 assert_eq!(v.values.len(), 3);
1180 assert!(v.metadata.is_none());
1181 }
1182
1183 #[test]
1184 fn test_query_request_builder() {
1185 let req = QueryRequest::new(vec![0.1, 0.2], 10)
1186 .with_filter(serde_json::json!({"category": "test"}))
1187 .include_metadata(false);
1188
1189 assert_eq!(req.top_k, 10);
1190 assert!(req.filter.is_some());
1191 assert!(!req.include_metadata);
1192 }
1193
1194 #[test]
1195 fn test_hybrid_search_request() {
1196 let req = HybridSearchRequest::new(vec![0.1], "test query", 5).with_vector_weight(0.7);
1197
1198 assert_eq!(req.vector_weight, 0.7);
1199 assert_eq!(req.text, "test query");
1200 }
1201
1202 #[test]
1203 fn test_hybrid_search_weight_clamping() {
1204 let req = HybridSearchRequest::new(vec![0.1], "test", 5).with_vector_weight(1.5); assert_eq!(req.vector_weight, 1.0);
1207 }
1208
1209 #[test]
1210 fn test_text_document_builder() {
1211 let doc = TextDocument::new("doc1", "Hello world").with_ttl(3600);
1212
1213 assert_eq!(doc.id, "doc1");
1214 assert_eq!(doc.text, "Hello world");
1215 assert_eq!(doc.ttl_seconds, Some(3600));
1216 assert!(doc.metadata.is_none());
1217 }
1218
1219 #[test]
1220 fn test_upsert_text_request_builder() {
1221 let docs = vec![
1222 TextDocument::new("doc1", "Hello"),
1223 TextDocument::new("doc2", "World"),
1224 ];
1225 let req = UpsertTextRequest::new(docs).with_model(EmbeddingModel::BgeSmall);
1226
1227 assert_eq!(req.documents.len(), 2);
1228 assert_eq!(req.model, Some(EmbeddingModel::BgeSmall));
1229 }
1230
1231 #[test]
1232 fn test_query_text_request_builder() {
1233 let req = QueryTextRequest::new("semantic search query", 5)
1234 .with_filter(serde_json::json!({"category": "docs"}))
1235 .include_vectors(true)
1236 .with_model(EmbeddingModel::E5Small);
1237
1238 assert_eq!(req.text, "semantic search query");
1239 assert_eq!(req.top_k, 5);
1240 assert!(req.filter.is_some());
1241 assert!(req.include_vectors);
1242 assert_eq!(req.model, Some(EmbeddingModel::E5Small));
1243 }
1244
1245 #[test]
1246 fn test_fetch_request_builder() {
1247 let req = FetchRequest::new(vec!["id1".to_string(), "id2".to_string()]);
1248
1249 assert_eq!(req.ids.len(), 2);
1250 assert!(req.include_values);
1251 assert!(req.include_metadata);
1252 }
1253
1254 #[test]
1255 fn test_create_namespace_request_builder() {
1256 let req = CreateNamespaceRequest::new()
1257 .with_dimensions(384)
1258 .with_index_type("hnsw");
1259
1260 assert_eq!(req.dimensions, Some(384));
1261 assert_eq!(req.index_type.as_deref(), Some("hnsw"));
1262 }
1263
1264 #[test]
1265 fn test_batch_query_text_request() {
1266 let req =
1267 BatchQueryTextRequest::new(vec!["query one".to_string(), "query two".to_string()], 10);
1268
1269 assert_eq!(req.queries.len(), 2);
1270 assert_eq!(req.top_k, 10);
1271 assert!(!req.include_vectors);
1272 assert!(req.model.is_none());
1273 }
1274}