vectorizer_sdk/
client.rs

1//! Vectorizer client with transport abstraction
2
3use std::sync::Arc;
4
5use serde_json;
6
7use crate::error::{Result, VectorizerError};
8use crate::http_transport::HttpTransport;
9use crate::models::hybrid_search::{
10    HybridScoringAlgorithm, HybridSearchRequest, HybridSearchResponse,
11};
12use crate::models::*;
13use crate::transport::{Protocol, Transport};
14#[cfg(feature = "umicp")]
15use crate::umicp_transport::UmicpTransport;
16
17/// Configuration for VectorizerClient
18#[derive(Clone)]
19pub struct ClientConfig {
20    /// Base URL for HTTP transport (single-node deployments)
21    pub base_url: Option<String>,
22    /// Connection string (supports http://, https://, umicp://)
23    pub connection_string: Option<String>,
24    /// Protocol to use
25    pub protocol: Option<Protocol>,
26    /// API key for authentication
27    pub api_key: Option<String>,
28    /// Request timeout in seconds
29    pub timeout_secs: Option<u64>,
30    /// UMICP configuration
31    #[cfg(feature = "umicp")]
32    pub umicp: Option<UmicpConfig>,
33    /// Master/replica host configuration for read/write routing
34    pub hosts: Option<HostConfig>,
35    /// Default read preference for read operations
36    pub read_preference: Option<ReadPreference>,
37}
38
39#[cfg(feature = "umicp")]
40/// UMICP-specific configuration
41#[derive(Clone)]
42pub struct UmicpConfig {
43    pub host: String,
44    pub port: u16,
45}
46
47impl Default for ClientConfig {
48    fn default() -> Self {
49        Self {
50            base_url: Some("http://localhost:15002".to_string()),
51            connection_string: None,
52            protocol: None,
53            api_key: None,
54            timeout_secs: Some(30),
55            #[cfg(feature = "umicp")]
56            umicp: None,
57            hosts: None,
58            read_preference: None,
59        }
60    }
61}
62
63/// Vectorizer client with optional master/replica topology support
64pub struct VectorizerClient {
65    transport: Arc<dyn Transport>,
66    protocol: Protocol,
67    base_url: String,
68    /// Master transport for write operations (if replica mode is enabled)
69    #[allow(dead_code)]
70    master_transport: Option<Arc<dyn Transport>>,
71    /// Replica transports for read operations (if replica mode is enabled)
72    #[allow(dead_code)]
73    replica_transports: Vec<Arc<dyn Transport>>,
74    /// Current replica index for round-robin selection
75    #[allow(dead_code)]
76    replica_index: std::sync::atomic::AtomicUsize,
77    /// Default read preference
78    #[allow(dead_code)]
79    read_preference: ReadPreference,
80    /// Whether replica mode is enabled
81    #[allow(dead_code)]
82    is_replica_mode: bool,
83    /// Original config for creating child clients
84    config: ClientConfig,
85}
86
87impl VectorizerClient {
88    /// Get the base URL (for HTTP transport)
89    pub fn base_url(&self) -> &str {
90        &self.base_url
91    }
92
93    /// Create a new client with configuration
94    pub fn new(config: ClientConfig) -> Result<Self> {
95        let timeout_secs = config.timeout_secs.unwrap_or(30);
96
97        // Determine protocol and create transport
98        let (transport, protocol, base_url): (Arc<dyn Transport>, Protocol, String) =
99            if let Some(ref conn_str) = config.connection_string {
100                // Use connection string
101                #[allow(unused_variables)] // port is only used when umicp feature is enabled
102                let (proto, host, port) = crate::transport::parse_connection_string(conn_str)?;
103
104                match proto {
105                    Protocol::Http => {
106                        let transport =
107                            HttpTransport::new(&host, config.api_key.as_deref(), timeout_secs)?;
108                        (Arc::new(transport), Protocol::Http, host.clone())
109                    }
110                    #[cfg(feature = "umicp")]
111                    Protocol::Umicp => {
112                        let umicp_port = port.unwrap_or(15003);
113                        let transport = UmicpTransport::new(
114                            &host,
115                            umicp_port,
116                            config.api_key.as_deref(),
117                            timeout_secs,
118                        )?;
119                        let base_url = format!("umicp://{host}:{umicp_port}");
120                        (Arc::new(transport), Protocol::Umicp, base_url)
121                    }
122                }
123            } else {
124                // Use explicit configuration
125                let proto = config.protocol.unwrap_or(Protocol::Http);
126
127                match proto {
128                    Protocol::Http => {
129                        let base_url = config
130                            .base_url
131                            .clone()
132                            .unwrap_or_else(|| "http://localhost:15002".to_string());
133                        let transport =
134                            HttpTransport::new(&base_url, config.api_key.as_deref(), timeout_secs)?;
135                        (Arc::new(transport), Protocol::Http, base_url)
136                    }
137                    #[cfg(feature = "umicp")]
138                    Protocol::Umicp => {
139                        #[cfg(feature = "umicp")]
140                        {
141                            let umicp_config = config.umicp.clone().ok_or_else(|| {
142                                VectorizerError::configuration(
143                                    "UMICP configuration is required when using UMICP protocol",
144                                )
145                            })?;
146
147                            let transport = UmicpTransport::new(
148                                &umicp_config.host,
149                                umicp_config.port,
150                                config.api_key.as_deref(),
151                                timeout_secs,
152                            )?;
153                            let base_url =
154                                format!("umicp://{}:{}", umicp_config.host, umicp_config.port);
155                            (Arc::new(transport), Protocol::Umicp, base_url)
156                        }
157                        #[cfg(not(feature = "umicp"))]
158                        {
159                            return Err(VectorizerError::configuration(
160                                "UMICP feature is not enabled. Enable it with --features umicp",
161                            ));
162                        }
163                    }
164                }
165            };
166
167        // Initialize replica mode if hosts are configured
168        let (master_transport, replica_transports, is_replica_mode) =
169            if let Some(ref hosts) = config.hosts {
170                let master =
171                    HttpTransport::new(&hosts.master, config.api_key.as_deref(), timeout_secs)?;
172                let replicas: Result<Vec<Arc<dyn Transport>>> = hosts
173                    .replicas
174                    .iter()
175                    .map(|url| {
176                        let t = HttpTransport::new(url, config.api_key.as_deref(), timeout_secs)?;
177                        Ok(Arc::new(t) as Arc<dyn Transport>)
178                    })
179                    .collect();
180                (
181                    Some(Arc::new(master) as Arc<dyn Transport>),
182                    replicas?,
183                    true,
184                )
185            } else {
186                (None, vec![], false)
187            };
188
189        let read_preference = config.read_preference.unwrap_or(ReadPreference::Replica);
190
191        Ok(Self {
192            transport,
193            protocol,
194            base_url,
195            master_transport,
196            replica_transports,
197            replica_index: std::sync::atomic::AtomicUsize::new(0),
198            read_preference,
199            is_replica_mode,
200            config,
201        })
202    }
203
204    /// Create a new client with default configuration
205    pub fn new_default() -> Result<Self> {
206        Self::new(ClientConfig::default())
207    }
208
209    /// Create client with custom URL
210    pub fn new_with_url(base_url: &str) -> Result<Self> {
211        Self::new(ClientConfig {
212            base_url: Some(base_url.to_string()),
213            ..Default::default()
214        })
215    }
216
217    /// Create client with API key
218    pub fn new_with_api_key(base_url: &str, api_key: &str) -> Result<Self> {
219        Self::new(ClientConfig {
220            base_url: Some(base_url.to_string()),
221            api_key: Some(api_key.to_string()),
222            ..Default::default()
223        })
224    }
225
226    /// Create client from connection string
227    pub fn from_connection_string(connection_string: &str, api_key: Option<&str>) -> Result<Self> {
228        Self::new(ClientConfig {
229            connection_string: Some(connection_string.to_string()),
230            api_key: api_key.map(|s| s.to_string()),
231            ..Default::default()
232        })
233    }
234
235    /// Get the current protocol being used
236    pub fn protocol(&self) -> Protocol {
237        self.protocol
238    }
239
240    /// Get transport for write operations (always master)
241    #[allow(dead_code)]
242    fn get_write_transport(&self) -> &Arc<dyn Transport> {
243        if self.is_replica_mode {
244            self.master_transport.as_ref().unwrap_or(&self.transport)
245        } else {
246            &self.transport
247        }
248    }
249
250    /// Get transport for read operations based on read preference
251    #[allow(dead_code)]
252    fn get_read_transport(&self, options: Option<&ReadOptions>) -> &Arc<dyn Transport> {
253        if !self.is_replica_mode {
254            return &self.transport;
255        }
256
257        let preference = options
258            .and_then(|o| o.read_preference)
259            .unwrap_or(self.read_preference);
260
261        match preference {
262            ReadPreference::Master => self.master_transport.as_ref().unwrap_or(&self.transport),
263            ReadPreference::Replica | ReadPreference::Nearest => {
264                if self.replica_transports.is_empty() {
265                    return self.master_transport.as_ref().unwrap_or(&self.transport);
266                }
267                // Round-robin selection
268                let idx = self
269                    .replica_index
270                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
271                    % self.replica_transports.len();
272                &self.replica_transports[idx]
273            }
274        }
275    }
276
277    /// Execute a callback with master transport for read-your-writes scenarios.
278    /// All operations within the callback will be routed to master.
279    pub async fn with_master<F, Fut, T>(&self, callback: F) -> Result<T>
280    where
281        F: FnOnce(VectorizerClient) -> Fut,
282        Fut: std::future::Future<Output = Result<T>>,
283    {
284        let mut master_config = self.config.clone();
285        master_config.read_preference = Some(ReadPreference::Master);
286        let master_client = VectorizerClient::new(master_config)?;
287        callback(master_client).await
288    }
289
290    /// Health check
291    pub async fn health_check(&self) -> Result<HealthStatus> {
292        let response = self.make_request("GET", "/health", None).await?;
293        let health: HealthStatus = serde_json::from_str(&response).map_err(|e| {
294            VectorizerError::server(format!("Failed to parse health check response: {e}"))
295        })?;
296        Ok(health)
297    }
298
299    /// List collections
300    pub async fn list_collections(&self) -> Result<Vec<Collection>> {
301        let response = self.make_request("GET", "/collections", None).await?;
302        // Handle both array and {collections: [...]} response formats
303        let collections: Vec<Collection> = if let Ok(wrapper) =
304            serde_json::from_str::<serde_json::Value>(&response)
305        {
306            if let Some(arr) = wrapper.get("collections").and_then(|v| v.as_array()) {
307                serde_json::from_value(serde_json::Value::Array(arr.clone())).map_err(|e| {
308                    VectorizerError::server(format!("Failed to parse collections array: {e}"))
309                })?
310            } else if wrapper.is_array() {
311                serde_json::from_value(wrapper).map_err(|e| {
312                    VectorizerError::server(format!("Failed to parse collections response: {e}"))
313                })?
314            } else {
315                return Err(VectorizerError::server(
316                    "Unexpected collections response format".to_string(),
317                ));
318            }
319        } else {
320            return Err(VectorizerError::server(
321                "Failed to parse collections response".to_string(),
322            ));
323        };
324        Ok(collections)
325    }
326
327    /// Search vectors
328    pub async fn search_vectors(
329        &self,
330        collection: &str,
331        query: &str,
332        limit: Option<usize>,
333        score_threshold: Option<f32>,
334    ) -> Result<SearchResponse> {
335        let mut payload = serde_json::Map::new();
336        payload.insert(
337            "query".to_string(),
338            serde_json::Value::String(query.to_string()),
339        );
340        payload.insert(
341            "limit".to_string(),
342            serde_json::Value::Number(limit.unwrap_or(10).into()),
343        );
344
345        if let Some(threshold) = score_threshold {
346            payload.insert(
347                "score_threshold".to_string(),
348                serde_json::Value::Number(serde_json::Number::from_f64(threshold as f64).unwrap()),
349            );
350        }
351
352        let response = self
353            .make_request(
354                "POST",
355                &format!("/collections/{}/search/text", collection),
356                Some(serde_json::Value::Object(payload)),
357            )
358            .await?;
359        let search_response: SearchResponse = serde_json::from_str(&response).map_err(|e| {
360            VectorizerError::server(format!("Failed to parse search response: {e}"))
361        })?;
362        Ok(search_response)
363    }
364
365    // ===== INTELLIGENT SEARCH OPERATIONS =====
366
367    /// Intelligent search with multi-query expansion and semantic reranking
368    pub async fn intelligent_search(
369        &self,
370        request: IntelligentSearchRequest,
371    ) -> Result<IntelligentSearchResponse> {
372        let response = self
373            .make_request(
374                "POST",
375                "/intelligent_search",
376                Some(serde_json::to_value(request).unwrap()),
377            )
378            .await?;
379        let search_response: IntelligentSearchResponse =
380            serde_json::from_str(&response).map_err(|e| {
381                VectorizerError::server(format!(
382                    "Failed to parse intelligent search response: {}",
383                    e
384                ))
385            })?;
386        Ok(search_response)
387    }
388
389    /// Semantic search with advanced reranking and similarity thresholds
390    pub async fn semantic_search(
391        &self,
392        request: SemanticSearchRequest,
393    ) -> Result<SemanticSearchResponse> {
394        let response = self
395            .make_request(
396                "POST",
397                "/semantic_search",
398                Some(serde_json::to_value(request).unwrap()),
399            )
400            .await?;
401        let search_response: SemanticSearchResponse =
402            serde_json::from_str(&response).map_err(|e| {
403                VectorizerError::server(format!("Failed to parse semantic search response: {e}"))
404            })?;
405        Ok(search_response)
406    }
407
408    /// Context-aware search with metadata filtering and contextual reranking
409    pub async fn contextual_search(
410        &self,
411        request: ContextualSearchRequest,
412    ) -> Result<ContextualSearchResponse> {
413        let response = self
414            .make_request(
415                "POST",
416                "/contextual_search",
417                Some(serde_json::to_value(request).unwrap()),
418            )
419            .await?;
420        let search_response: ContextualSearchResponse =
421            serde_json::from_str(&response).map_err(|e| {
422                VectorizerError::server(format!(
423                    "Failed to parse contextual search response: {}",
424                    e
425                ))
426            })?;
427        Ok(search_response)
428    }
429
430    /// Multi-collection search with cross-collection reranking and aggregation
431    pub async fn multi_collection_search(
432        &self,
433        request: MultiCollectionSearchRequest,
434    ) -> Result<MultiCollectionSearchResponse> {
435        let response = self
436            .make_request(
437                "POST",
438                "/multi_collection_search",
439                Some(serde_json::to_value(request).unwrap()),
440            )
441            .await?;
442        let search_response: MultiCollectionSearchResponse = serde_json::from_str(&response)
443            .map_err(|e| {
444                VectorizerError::server(format!(
445                    "Failed to parse multi-collection search response: {}",
446                    e
447                ))
448            })?;
449        Ok(search_response)
450    }
451
452    /// Perform hybrid search combining dense and sparse vectors
453    pub async fn hybrid_search(
454        &self,
455        request: HybridSearchRequest,
456    ) -> Result<HybridSearchResponse> {
457        let url = format!("/collections/{}/hybrid_search", request.collection);
458        let payload = serde_json::json!({
459            "query": request.query,
460            "alpha": request.alpha,
461            "algorithm": match request.algorithm {
462                HybridScoringAlgorithm::ReciprocalRankFusion => "rrf",
463                HybridScoringAlgorithm::WeightedCombination => "weighted",
464                HybridScoringAlgorithm::AlphaBlending => "alpha",
465            },
466            "dense_k": request.dense_k,
467            "sparse_k": request.sparse_k,
468            "final_k": request.final_k,
469            "query_sparse": request.query_sparse.as_ref().map(|sv| serde_json::json!({
470                "indices": sv.indices,
471                "values": sv.values,
472            })),
473        });
474        let response = self.make_request("POST", &url, Some(payload)).await?;
475        let search_response: HybridSearchResponse =
476            serde_json::from_str(&response).map_err(|e| {
477                VectorizerError::server(format!("Failed to parse hybrid search response: {e}"))
478            })?;
479        Ok(search_response)
480    }
481
482    // ===== QDRANT COMPATIBILITY METHODS =====
483
484    /// List all collections (Qdrant-compatible API)
485    pub async fn qdrant_list_collections(&self) -> Result<serde_json::Value> {
486        let response = self
487            .make_request("GET", "/qdrant/collections", None)
488            .await?;
489        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
490            VectorizerError::server(format!(
491                "Failed to parse Qdrant collections response: {}",
492                e
493            ))
494        })?;
495        Ok(result)
496    }
497
498    /// Get collection information (Qdrant-compatible API)
499    pub async fn qdrant_get_collection(&self, name: &str) -> Result<serde_json::Value> {
500        let url = format!("/qdrant/collections/{name}");
501        let response = self.make_request("GET", &url, None).await?;
502        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
503            VectorizerError::server(format!("Failed to parse Qdrant collection response: {e}"))
504        })?;
505        Ok(result)
506    }
507
508    /// Create collection (Qdrant-compatible API)
509    pub async fn qdrant_create_collection(
510        &self,
511        name: &str,
512        config: &serde_json::Value,
513    ) -> Result<serde_json::Value> {
514        let url = format!("/qdrant/collections/{name}");
515        let payload = serde_json::json!({ "config": config });
516        let response = self.make_request("PUT", &url, Some(payload)).await?;
517        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
518            VectorizerError::server(format!(
519                "Failed to parse Qdrant create collection response: {}",
520                e
521            ))
522        })?;
523        Ok(result)
524    }
525
526    /// Upsert points to collection (Qdrant-compatible API)
527    pub async fn qdrant_upsert_points(
528        &self,
529        collection: &str,
530        points: &serde_json::Value,
531        wait: bool,
532    ) -> Result<serde_json::Value> {
533        let url = format!("/qdrant/collections/{collection}/points");
534        let payload = serde_json::json!({
535            "points": points,
536            "wait": wait,
537        });
538        let response = self.make_request("PUT", &url, Some(payload)).await?;
539        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
540            VectorizerError::server(format!(
541                "Failed to parse Qdrant upsert points response: {}",
542                e
543            ))
544        })?;
545        Ok(result)
546    }
547
548    /// Search points in collection (Qdrant-compatible API)
549    pub async fn qdrant_search_points(
550        &self,
551        collection: &str,
552        vector: &[f32],
553        limit: Option<usize>,
554        filter: Option<&serde_json::Value>,
555        with_payload: bool,
556        with_vector: bool,
557    ) -> Result<serde_json::Value> {
558        let url = format!("/qdrant/collections/{collection}/points/search");
559        let mut payload = serde_json::json!({
560            "vector": vector,
561            "limit": limit.unwrap_or(10),
562            "with_payload": with_payload,
563            "with_vector": with_vector,
564        });
565        if let Some(filter) = filter {
566            payload["filter"] = filter.clone();
567        }
568        let response = self.make_request("POST", &url, Some(payload)).await?;
569        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
570            VectorizerError::server(format!("Failed to parse Qdrant search response: {e}"))
571        })?;
572        Ok(result)
573    }
574
575    /// Delete points from collection (Qdrant-compatible API)
576    pub async fn qdrant_delete_points(
577        &self,
578        collection: &str,
579        point_ids: &[serde_json::Value],
580        wait: bool,
581    ) -> Result<serde_json::Value> {
582        let url = format!("/qdrant/collections/{collection}/points/delete");
583        let payload = serde_json::json!({
584            "points": point_ids,
585            "wait": wait,
586        });
587        let response = self.make_request("POST", &url, Some(payload)).await?;
588        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
589            VectorizerError::server(format!(
590                "Failed to parse Qdrant delete points response: {}",
591                e
592            ))
593        })?;
594        Ok(result)
595    }
596
597    /// Retrieve points by IDs (Qdrant-compatible API)
598    pub async fn qdrant_retrieve_points(
599        &self,
600        collection: &str,
601        point_ids: &[serde_json::Value],
602        with_payload: bool,
603        with_vector: bool,
604    ) -> Result<serde_json::Value> {
605        let ids_str = point_ids
606            .iter()
607            .map(|id| match id {
608                serde_json::Value::String(s) => s.clone(),
609                serde_json::Value::Number(n) => n.to_string(),
610                _ => serde_json::to_string(id).unwrap_or_default(),
611            })
612            .collect::<Vec<_>>()
613            .join(",");
614        let url = format!(
615            "/qdrant/collections/{}/points?ids={}&with_payload={}&with_vector={}",
616            collection, ids_str, with_payload, with_vector
617        );
618        let response = self.make_request("GET", &url, None).await?;
619        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
620            VectorizerError::server(format!(
621                "Failed to parse Qdrant retrieve points response: {}",
622                e
623            ))
624        })?;
625        Ok(result)
626    }
627
628    /// Count points in collection (Qdrant-compatible API)
629    pub async fn qdrant_count_points(
630        &self,
631        collection: &str,
632        filter: Option<&serde_json::Value>,
633    ) -> Result<serde_json::Value> {
634        let url = format!("/qdrant/collections/{collection}/points/count");
635        let payload = if let Some(filter) = filter {
636            serde_json::json!({ "filter": filter })
637        } else {
638            serde_json::json!({})
639        };
640        let response = self.make_request("POST", &url, Some(payload)).await?;
641        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
642            VectorizerError::server(format!(
643                "Failed to parse Qdrant count points response: {}",
644                e
645            ))
646        })?;
647        Ok(result)
648    }
649
650    // ===== QDRANT ADVANCED FEATURES (1.14.x) =====
651
652    /// List snapshots for a collection (Qdrant-compatible API)
653    pub async fn qdrant_list_collection_snapshots(
654        &self,
655        collection: &str,
656    ) -> Result<serde_json::Value> {
657        let url = format!("/qdrant/collections/{collection}/snapshots");
658        let response = self.make_request("GET", &url, None).await?;
659        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
660            VectorizerError::server(format!(
661                "Failed to parse Qdrant list snapshots response: {}",
662                e
663            ))
664        })?;
665        Ok(result)
666    }
667
668    /// Create snapshot for a collection (Qdrant-compatible API)
669    pub async fn qdrant_create_collection_snapshot(
670        &self,
671        collection: &str,
672    ) -> Result<serde_json::Value> {
673        let url = format!("/qdrant/collections/{collection}/snapshots");
674        let response = self.make_request("POST", &url, None).await?;
675        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
676            VectorizerError::server(format!(
677                "Failed to parse Qdrant create snapshot response: {}",
678                e
679            ))
680        })?;
681        Ok(result)
682    }
683
684    /// Delete snapshot (Qdrant-compatible API)
685    pub async fn qdrant_delete_collection_snapshot(
686        &self,
687        collection: &str,
688        snapshot_name: &str,
689    ) -> Result<serde_json::Value> {
690        let url = format!("/qdrant/collections/{collection}/snapshots/{snapshot_name}");
691        let response = self.make_request("DELETE", &url, None).await?;
692        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
693            VectorizerError::server(format!(
694                "Failed to parse Qdrant delete snapshot response: {}",
695                e
696            ))
697        })?;
698        Ok(result)
699    }
700
701    /// Recover collection from snapshot (Qdrant-compatible API)
702    pub async fn qdrant_recover_collection_snapshot(
703        &self,
704        collection: &str,
705        location: &str,
706    ) -> Result<serde_json::Value> {
707        let url = format!("/qdrant/collections/{collection}/snapshots/recover");
708        let payload = serde_json::json!({ "location": location });
709        let response = self.make_request("POST", &url, Some(payload)).await?;
710        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
711            VectorizerError::server(format!(
712                "Failed to parse Qdrant recover snapshot response: {}",
713                e
714            ))
715        })?;
716        Ok(result)
717    }
718
719    /// List all snapshots (Qdrant-compatible API)
720    pub async fn qdrant_list_all_snapshots(&self) -> Result<serde_json::Value> {
721        let url = "/qdrant/snapshots";
722        let response = self.make_request("GET", url, None).await?;
723        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
724            VectorizerError::server(format!(
725                "Failed to parse Qdrant list all snapshots response: {}",
726                e
727            ))
728        })?;
729        Ok(result)
730    }
731
732    /// Create full snapshot (Qdrant-compatible API)
733    pub async fn qdrant_create_full_snapshot(&self) -> Result<serde_json::Value> {
734        let url = "/qdrant/snapshots";
735        let response = self.make_request("POST", url, None).await?;
736        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
737            VectorizerError::server(format!(
738                "Failed to parse Qdrant create full snapshot response: {}",
739                e
740            ))
741        })?;
742        Ok(result)
743    }
744
745    /// List shard keys for a collection (Qdrant-compatible API)
746    pub async fn qdrant_list_shard_keys(&self, collection: &str) -> Result<serde_json::Value> {
747        let url = format!("/qdrant/collections/{collection}/shards");
748        let response = self.make_request("GET", &url, None).await?;
749        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
750            VectorizerError::server(format!(
751                "Failed to parse Qdrant list shard keys response: {}",
752                e
753            ))
754        })?;
755        Ok(result)
756    }
757
758    /// Create shard key (Qdrant-compatible API)
759    pub async fn qdrant_create_shard_key(
760        &self,
761        collection: &str,
762        shard_key: &serde_json::Value,
763    ) -> Result<serde_json::Value> {
764        let url = format!("/qdrant/collections/{collection}/shards");
765        let payload = serde_json::json!({ "shard_key": shard_key });
766        let response = self.make_request("PUT", &url, Some(payload)).await?;
767        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
768            VectorizerError::server(format!(
769                "Failed to parse Qdrant create shard key response: {}",
770                e
771            ))
772        })?;
773        Ok(result)
774    }
775
776    /// Delete shard key (Qdrant-compatible API)
777    pub async fn qdrant_delete_shard_key(
778        &self,
779        collection: &str,
780        shard_key: &serde_json::Value,
781    ) -> Result<serde_json::Value> {
782        let url = format!("/qdrant/collections/{collection}/shards/delete");
783        let payload = serde_json::json!({ "shard_key": shard_key });
784        let response = self.make_request("POST", &url, Some(payload)).await?;
785        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
786            VectorizerError::server(format!(
787                "Failed to parse Qdrant delete shard key response: {}",
788                e
789            ))
790        })?;
791        Ok(result)
792    }
793
794    /// Get cluster status (Qdrant-compatible API)
795    pub async fn qdrant_get_cluster_status(&self) -> Result<serde_json::Value> {
796        let url = "/qdrant/cluster";
797        let response = self.make_request("GET", url, None).await?;
798        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
799            VectorizerError::server(format!(
800                "Failed to parse Qdrant cluster status response: {}",
801                e
802            ))
803        })?;
804        Ok(result)
805    }
806
807    /// Recover current peer (Qdrant-compatible API)
808    pub async fn qdrant_cluster_recover(&self) -> Result<serde_json::Value> {
809        let url = "/qdrant/cluster/recover";
810        let response = self.make_request("POST", url, None).await?;
811        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
812            VectorizerError::server(format!(
813                "Failed to parse Qdrant cluster recover response: {}",
814                e
815            ))
816        })?;
817        Ok(result)
818    }
819
820    /// Remove peer from cluster (Qdrant-compatible API)
821    pub async fn qdrant_remove_peer(&self, peer_id: &str) -> Result<serde_json::Value> {
822        let url = format!("/qdrant/cluster/peer/{peer_id}");
823        let response = self.make_request("DELETE", &url, None).await?;
824        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
825            VectorizerError::server(format!(
826                "Failed to parse Qdrant remove peer response: {}",
827                e
828            ))
829        })?;
830        Ok(result)
831    }
832
833    /// List metadata keys (Qdrant-compatible API)
834    pub async fn qdrant_list_metadata_keys(&self) -> Result<serde_json::Value> {
835        let url = "/qdrant/cluster/metadata/keys";
836        let response = self.make_request("GET", url, None).await?;
837        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
838            VectorizerError::server(format!(
839                "Failed to parse Qdrant list metadata keys response: {}",
840                e
841            ))
842        })?;
843        Ok(result)
844    }
845
846    /// Get metadata key (Qdrant-compatible API)
847    pub async fn qdrant_get_metadata_key(&self, key: &str) -> Result<serde_json::Value> {
848        let url = format!("/qdrant/cluster/metadata/keys/{key}");
849        let response = self.make_request("GET", &url, None).await?;
850        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
851            VectorizerError::server(format!(
852                "Failed to parse Qdrant get metadata key response: {}",
853                e
854            ))
855        })?;
856        Ok(result)
857    }
858
859    /// Update metadata key (Qdrant-compatible API)
860    pub async fn qdrant_update_metadata_key(
861        &self,
862        key: &str,
863        value: &serde_json::Value,
864    ) -> Result<serde_json::Value> {
865        let url = format!("/qdrant/cluster/metadata/keys/{key}");
866        let payload = serde_json::json!({ "value": value });
867        let response = self.make_request("PUT", &url, Some(payload)).await?;
868        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
869            VectorizerError::server(format!(
870                "Failed to parse Qdrant update metadata key response: {}",
871                e
872            ))
873        })?;
874        Ok(result)
875    }
876
877    /// Query points (Qdrant 1.7+ Query API)
878    pub async fn qdrant_query_points(
879        &self,
880        collection: &str,
881        request: &serde_json::Value,
882    ) -> Result<serde_json::Value> {
883        let url = format!("/qdrant/collections/{collection}/points/query");
884        let response = self
885            .make_request("POST", &url, Some(request.clone()))
886            .await?;
887        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
888            VectorizerError::server(format!(
889                "Failed to parse Qdrant query points response: {}",
890                e
891            ))
892        })?;
893        Ok(result)
894    }
895
896    /// Batch query points (Qdrant 1.7+ Query API)
897    pub async fn qdrant_batch_query_points(
898        &self,
899        collection: &str,
900        request: &serde_json::Value,
901    ) -> Result<serde_json::Value> {
902        let url = format!("/qdrant/collections/{collection}/points/query/batch");
903        let response = self
904            .make_request("POST", &url, Some(request.clone()))
905            .await?;
906        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
907            VectorizerError::server(format!(
908                "Failed to parse Qdrant batch query points response: {}",
909                e
910            ))
911        })?;
912        Ok(result)
913    }
914
915    /// Query points with groups (Qdrant 1.7+ Query API)
916    pub async fn qdrant_query_points_groups(
917        &self,
918        collection: &str,
919        request: &serde_json::Value,
920    ) -> Result<serde_json::Value> {
921        let url = format!("/qdrant/collections/{collection}/points/query/groups");
922        let response = self
923            .make_request("POST", &url, Some(request.clone()))
924            .await?;
925        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
926            VectorizerError::server(format!(
927                "Failed to parse Qdrant query points groups response: {}",
928                e
929            ))
930        })?;
931        Ok(result)
932    }
933
934    /// Search points with groups (Qdrant Search Groups API)
935    pub async fn qdrant_search_points_groups(
936        &self,
937        collection: &str,
938        request: &serde_json::Value,
939    ) -> Result<serde_json::Value> {
940        let url = format!("/qdrant/collections/{collection}/points/search/groups");
941        let response = self
942            .make_request("POST", &url, Some(request.clone()))
943            .await?;
944        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
945            VectorizerError::server(format!(
946                "Failed to parse Qdrant search points groups response: {}",
947                e
948            ))
949        })?;
950        Ok(result)
951    }
952
953    /// Search matrix pairs (Qdrant Search Matrix API)
954    pub async fn qdrant_search_matrix_pairs(
955        &self,
956        collection: &str,
957        request: &serde_json::Value,
958    ) -> Result<serde_json::Value> {
959        let url = format!("/qdrant/collections/{collection}/points/search/matrix/pairs");
960        let response = self
961            .make_request("POST", &url, Some(request.clone()))
962            .await?;
963        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
964            VectorizerError::server(format!(
965                "Failed to parse Qdrant search matrix pairs response: {}",
966                e
967            ))
968        })?;
969        Ok(result)
970    }
971
972    /// Search matrix offsets (Qdrant Search Matrix API)
973    pub async fn qdrant_search_matrix_offsets(
974        &self,
975        collection: &str,
976        request: &serde_json::Value,
977    ) -> Result<serde_json::Value> {
978        let url = format!("/qdrant/collections/{collection}/points/search/matrix/offsets");
979        let response = self
980            .make_request("POST", &url, Some(request.clone()))
981            .await?;
982        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
983            VectorizerError::server(format!(
984                "Failed to parse Qdrant search matrix offsets response: {}",
985                e
986            ))
987        })?;
988        Ok(result)
989    }
990
991    /// Create collection
992    pub async fn create_collection(
993        &self,
994        name: &str,
995        dimension: usize,
996        metric: Option<SimilarityMetric>,
997    ) -> Result<CollectionInfo> {
998        let mut payload = serde_json::Map::new();
999        payload.insert(
1000            "name".to_string(),
1001            serde_json::Value::String(name.to_string()),
1002        );
1003        payload.insert(
1004            "dimension".to_string(),
1005            serde_json::Value::Number(dimension.into()),
1006        );
1007        payload.insert(
1008            "metric".to_string(),
1009            serde_json::Value::String(format!("{:?}", metric.unwrap_or_default()).to_lowercase()),
1010        );
1011
1012        let response = self
1013            .make_request(
1014                "POST",
1015                "/collections",
1016                Some(serde_json::Value::Object(payload)),
1017            )
1018            .await?;
1019        let create_response: CreateCollectionResponse =
1020            serde_json::from_str(&response).map_err(|e| {
1021                VectorizerError::server(format!(
1022                    "Failed to parse create collection response: {}",
1023                    e
1024                ))
1025            })?;
1026
1027        // Create a basic CollectionInfo from the response
1028        let info = CollectionInfo {
1029            name: create_response.collection,
1030            dimension,
1031            metric: format!("{:?}", metric.unwrap_or_default()).to_lowercase(),
1032            vector_count: 0,
1033            document_count: 0,
1034            created_at: "".to_string(),
1035            updated_at: "".to_string(),
1036            indexing_status: Some(crate::models::IndexingStatus {
1037                status: "created".to_string(),
1038                progress: 0.0,
1039                total_documents: 0,
1040                processed_documents: 0,
1041                vector_count: 0,
1042                estimated_time_remaining: None,
1043                last_updated: "".to_string(),
1044            }),
1045        };
1046        Ok(info)
1047    }
1048
1049    /// Insert texts
1050    pub async fn insert_texts(
1051        &self,
1052        collection: &str,
1053        texts: Vec<BatchTextRequest>,
1054    ) -> Result<BatchResponse> {
1055        let payload = serde_json::json!({
1056            "texts": texts
1057        });
1058
1059        let response = self
1060            .make_request(
1061                "POST",
1062                &format!("/collections/{collection}/documents"),
1063                Some(serde_json::to_value(payload)?),
1064            )
1065            .await?;
1066        let batch_response: BatchResponse = serde_json::from_str(&response).map_err(|e| {
1067            VectorizerError::server(format!("Failed to parse insert texts response: {e}"))
1068        })?;
1069        Ok(batch_response)
1070    }
1071
1072    /// Delete collection
1073    pub async fn delete_collection(&self, name: &str) -> Result<()> {
1074        self.make_request("DELETE", &format!("/collections/{}", name), None)
1075            .await?;
1076        Ok(())
1077    }
1078
1079    /// Get vector
1080    pub async fn get_vector(&self, collection: &str, vector_id: &str) -> Result<Vector> {
1081        let response = self
1082            .make_request(
1083                "GET",
1084                &format!("/collections/{collection}/vectors/{vector_id}"),
1085                None,
1086            )
1087            .await?;
1088        let vector: Vector = serde_json::from_str(&response).map_err(|e| {
1089            VectorizerError::server(format!("Failed to parse get vector response: {e}"))
1090        })?;
1091        Ok(vector)
1092    }
1093
1094    /// Get collection info
1095    pub async fn get_collection_info(&self, collection: &str) -> Result<CollectionInfo> {
1096        let response = self
1097            .make_request("GET", &format!("/collections/{}", collection), None)
1098            .await?;
1099        let info: CollectionInfo = serde_json::from_str(&response).map_err(|e| {
1100            VectorizerError::server(format!("Failed to parse collection info: {e}"))
1101        })?;
1102        Ok(info)
1103    }
1104
1105    /// Generate embeddings
1106    pub async fn embed_text(&self, text: &str, model: Option<&str>) -> Result<EmbeddingResponse> {
1107        let mut payload = serde_json::Map::new();
1108        payload.insert(
1109            "text".to_string(),
1110            serde_json::Value::String(text.to_string()),
1111        );
1112
1113        if let Some(model) = model {
1114            payload.insert(
1115                "model".to_string(),
1116                serde_json::Value::String(model.to_string()),
1117            );
1118        }
1119
1120        let response = self
1121            .make_request("POST", "/embed", Some(serde_json::Value::Object(payload)))
1122            .await?;
1123        let embedding_response: EmbeddingResponse =
1124            serde_json::from_str(&response).map_err(|e| {
1125                VectorizerError::server(format!("Failed to parse embedding response: {e}"))
1126            })?;
1127        Ok(embedding_response)
1128    }
1129
1130    // =============================================================================
1131    // DISCOVERY OPERATIONS
1132    // =============================================================================
1133
1134    /// Complete discovery pipeline with intelligent search and prompt generation
1135    pub async fn discover(
1136        &self,
1137        query: &str,
1138        include_collections: Option<Vec<String>>,
1139        exclude_collections: Option<Vec<String>>,
1140        max_bullets: Option<usize>,
1141        broad_k: Option<usize>,
1142        focus_k: Option<usize>,
1143    ) -> Result<serde_json::Value> {
1144        // Validate query
1145        if query.trim().is_empty() {
1146            return Err(VectorizerError::validation("Query cannot be empty"));
1147        }
1148
1149        // Validate max_bullets
1150        if let Some(max) = max_bullets
1151            && max == 0
1152        {
1153            return Err(VectorizerError::validation(
1154                "max_bullets must be greater than 0",
1155            ));
1156        }
1157
1158        let mut payload = serde_json::Map::new();
1159        payload.insert(
1160            "query".to_string(),
1161            serde_json::Value::String(query.to_string()),
1162        );
1163
1164        if let Some(inc) = include_collections {
1165            payload.insert(
1166                "include_collections".to_string(),
1167                serde_json::to_value(inc).unwrap(),
1168            );
1169        }
1170        if let Some(exc) = exclude_collections {
1171            payload.insert(
1172                "exclude_collections".to_string(),
1173                serde_json::to_value(exc).unwrap(),
1174            );
1175        }
1176        if let Some(max) = max_bullets {
1177            payload.insert(
1178                "max_bullets".to_string(),
1179                serde_json::Value::Number(max.into()),
1180            );
1181        }
1182        if let Some(k) = broad_k {
1183            payload.insert("broad_k".to_string(), serde_json::Value::Number(k.into()));
1184        }
1185        if let Some(k) = focus_k {
1186            payload.insert("focus_k".to_string(), serde_json::Value::Number(k.into()));
1187        }
1188
1189        let response = self
1190            .make_request(
1191                "POST",
1192                "/discover",
1193                Some(serde_json::Value::Object(payload)),
1194            )
1195            .await?;
1196        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1197            VectorizerError::server(format!("Failed to parse discover response: {e}"))
1198        })?;
1199        Ok(result)
1200    }
1201
1202    /// Pre-filter collections by name patterns
1203    pub async fn filter_collections(
1204        &self,
1205        query: &str,
1206        include: Option<Vec<String>>,
1207        exclude: Option<Vec<String>>,
1208    ) -> Result<serde_json::Value> {
1209        // Validate query
1210        if query.trim().is_empty() {
1211            return Err(VectorizerError::validation("Query cannot be empty"));
1212        }
1213
1214        let mut payload = serde_json::Map::new();
1215        payload.insert(
1216            "query".to_string(),
1217            serde_json::Value::String(query.to_string()),
1218        );
1219
1220        if let Some(inc) = include {
1221            payload.insert("include".to_string(), serde_json::to_value(inc).unwrap());
1222        }
1223        if let Some(exc) = exclude {
1224            payload.insert("exclude".to_string(), serde_json::to_value(exc).unwrap());
1225        }
1226
1227        let response = self
1228            .make_request(
1229                "POST",
1230                "/discovery/filter_collections",
1231                Some(serde_json::Value::Object(payload)),
1232            )
1233            .await?;
1234        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1235            VectorizerError::server(format!("Failed to parse filter response: {e}"))
1236        })?;
1237        Ok(result)
1238    }
1239
1240    /// Rank collections by relevance
1241    pub async fn score_collections(
1242        &self,
1243        query: &str,
1244        name_match_weight: Option<f32>,
1245        term_boost_weight: Option<f32>,
1246        signal_boost_weight: Option<f32>,
1247    ) -> Result<serde_json::Value> {
1248        // Validate weights (must be between 0.0 and 1.0)
1249        if let Some(w) = name_match_weight
1250            && !(0.0..=1.0).contains(&w)
1251        {
1252            return Err(VectorizerError::validation(
1253                "name_match_weight must be between 0.0 and 1.0",
1254            ));
1255        }
1256        if let Some(w) = term_boost_weight
1257            && !(0.0..=1.0).contains(&w)
1258        {
1259            return Err(VectorizerError::validation(
1260                "term_boost_weight must be between 0.0 and 1.0",
1261            ));
1262        }
1263        if let Some(w) = signal_boost_weight
1264            && !(0.0..=1.0).contains(&w)
1265        {
1266            return Err(VectorizerError::validation(
1267                "signal_boost_weight must be between 0.0 and 1.0",
1268            ));
1269        }
1270
1271        let mut payload = serde_json::Map::new();
1272        payload.insert(
1273            "query".to_string(),
1274            serde_json::Value::String(query.to_string()),
1275        );
1276
1277        if let Some(w) = name_match_weight {
1278            payload.insert("name_match_weight".to_string(), serde_json::json!(w));
1279        }
1280        if let Some(w) = term_boost_weight {
1281            payload.insert("term_boost_weight".to_string(), serde_json::json!(w));
1282        }
1283        if let Some(w) = signal_boost_weight {
1284            payload.insert("signal_boost_weight".to_string(), serde_json::json!(w));
1285        }
1286
1287        let response = self
1288            .make_request(
1289                "POST",
1290                "/discovery/score_collections",
1291                Some(serde_json::Value::Object(payload)),
1292            )
1293            .await?;
1294        let result: serde_json::Value = serde_json::from_str(&response)
1295            .map_err(|e| VectorizerError::server(format!("Failed to parse score response: {e}")))?;
1296        Ok(result)
1297    }
1298
1299    /// Generate query variations
1300    pub async fn expand_queries(
1301        &self,
1302        query: &str,
1303        max_expansions: Option<usize>,
1304        include_definition: Option<bool>,
1305        include_features: Option<bool>,
1306        include_architecture: Option<bool>,
1307    ) -> Result<serde_json::Value> {
1308        let mut payload = serde_json::Map::new();
1309        payload.insert(
1310            "query".to_string(),
1311            serde_json::Value::String(query.to_string()),
1312        );
1313
1314        if let Some(max) = max_expansions {
1315            payload.insert(
1316                "max_expansions".to_string(),
1317                serde_json::Value::Number(max.into()),
1318            );
1319        }
1320        if let Some(def) = include_definition {
1321            payload.insert(
1322                "include_definition".to_string(),
1323                serde_json::Value::Bool(def),
1324            );
1325        }
1326        if let Some(feat) = include_features {
1327            payload.insert(
1328                "include_features".to_string(),
1329                serde_json::Value::Bool(feat),
1330            );
1331        }
1332        if let Some(arch) = include_architecture {
1333            payload.insert(
1334                "include_architecture".to_string(),
1335                serde_json::Value::Bool(arch),
1336            );
1337        }
1338
1339        let response = self
1340            .make_request(
1341                "POST",
1342                "/discovery/expand_queries",
1343                Some(serde_json::Value::Object(payload)),
1344            )
1345            .await?;
1346        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1347            VectorizerError::server(format!("Failed to parse expand response: {e}"))
1348        })?;
1349        Ok(result)
1350    }
1351
1352    // =============================================================================
1353    // FILE OPERATIONS
1354    // =============================================================================
1355
1356    /// Retrieve complete file content from a collection
1357    pub async fn get_file_content(
1358        &self,
1359        collection: &str,
1360        file_path: &str,
1361        max_size_kb: Option<usize>,
1362    ) -> Result<serde_json::Value> {
1363        let mut payload = serde_json::Map::new();
1364        payload.insert(
1365            "collection".to_string(),
1366            serde_json::Value::String(collection.to_string()),
1367        );
1368        payload.insert(
1369            "file_path".to_string(),
1370            serde_json::Value::String(file_path.to_string()),
1371        );
1372
1373        if let Some(max) = max_size_kb {
1374            payload.insert(
1375                "max_size_kb".to_string(),
1376                serde_json::Value::Number(max.into()),
1377            );
1378        }
1379
1380        let response = self
1381            .make_request(
1382                "POST",
1383                "/file/content",
1384                Some(serde_json::Value::Object(payload)),
1385            )
1386            .await?;
1387        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1388            VectorizerError::server(format!("Failed to parse file content response: {e}"))
1389        })?;
1390        Ok(result)
1391    }
1392
1393    /// List all indexed files in a collection
1394    pub async fn list_files_in_collection(
1395        &self,
1396        collection: &str,
1397        filter_by_type: Option<Vec<String>>,
1398        min_chunks: Option<usize>,
1399        max_results: Option<usize>,
1400        sort_by: Option<&str>,
1401    ) -> Result<serde_json::Value> {
1402        let mut payload = serde_json::Map::new();
1403        payload.insert(
1404            "collection".to_string(),
1405            serde_json::Value::String(collection.to_string()),
1406        );
1407
1408        if let Some(types) = filter_by_type {
1409            payload.insert(
1410                "filter_by_type".to_string(),
1411                serde_json::to_value(types).unwrap(),
1412            );
1413        }
1414        if let Some(min) = min_chunks {
1415            payload.insert(
1416                "min_chunks".to_string(),
1417                serde_json::Value::Number(min.into()),
1418            );
1419        }
1420        if let Some(max) = max_results {
1421            payload.insert(
1422                "max_results".to_string(),
1423                serde_json::Value::Number(max.into()),
1424            );
1425        }
1426        if let Some(sort) = sort_by {
1427            payload.insert(
1428                "sort_by".to_string(),
1429                serde_json::Value::String(sort.to_string()),
1430            );
1431        }
1432
1433        let response = self
1434            .make_request(
1435                "POST",
1436                "/file/list",
1437                Some(serde_json::Value::Object(payload)),
1438            )
1439            .await?;
1440        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1441            VectorizerError::server(format!("Failed to parse list files response: {e}"))
1442        })?;
1443        Ok(result)
1444    }
1445
1446    /// Get extractive or structural summary of an indexed file
1447    pub async fn get_file_summary(
1448        &self,
1449        collection: &str,
1450        file_path: &str,
1451        summary_type: Option<&str>,
1452        max_sentences: Option<usize>,
1453    ) -> Result<serde_json::Value> {
1454        let mut payload = serde_json::Map::new();
1455        payload.insert(
1456            "collection".to_string(),
1457            serde_json::Value::String(collection.to_string()),
1458        );
1459        payload.insert(
1460            "file_path".to_string(),
1461            serde_json::Value::String(file_path.to_string()),
1462        );
1463
1464        if let Some(stype) = summary_type {
1465            payload.insert(
1466                "summary_type".to_string(),
1467                serde_json::Value::String(stype.to_string()),
1468            );
1469        }
1470        if let Some(max) = max_sentences {
1471            payload.insert(
1472                "max_sentences".to_string(),
1473                serde_json::Value::Number(max.into()),
1474            );
1475        }
1476
1477        let response = self
1478            .make_request(
1479                "POST",
1480                "/file/summary",
1481                Some(serde_json::Value::Object(payload)),
1482            )
1483            .await?;
1484        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1485            VectorizerError::server(format!("Failed to parse file summary response: {e}"))
1486        })?;
1487        Ok(result)
1488    }
1489
1490    /// Retrieve chunks in original file order for progressive reading
1491    pub async fn get_file_chunks_ordered(
1492        &self,
1493        collection: &str,
1494        file_path: &str,
1495        start_chunk: Option<usize>,
1496        limit: Option<usize>,
1497        include_context: Option<bool>,
1498    ) -> Result<serde_json::Value> {
1499        let mut payload = serde_json::Map::new();
1500        payload.insert(
1501            "collection".to_string(),
1502            serde_json::Value::String(collection.to_string()),
1503        );
1504        payload.insert(
1505            "file_path".to_string(),
1506            serde_json::Value::String(file_path.to_string()),
1507        );
1508
1509        if let Some(start) = start_chunk {
1510            payload.insert(
1511                "start_chunk".to_string(),
1512                serde_json::Value::Number(start.into()),
1513            );
1514        }
1515        if let Some(lim) = limit {
1516            payload.insert("limit".to_string(), serde_json::Value::Number(lim.into()));
1517        }
1518        if let Some(ctx) = include_context {
1519            payload.insert("include_context".to_string(), serde_json::Value::Bool(ctx));
1520        }
1521
1522        let response = self
1523            .make_request(
1524                "POST",
1525                "/file/chunks",
1526                Some(serde_json::Value::Object(payload)),
1527            )
1528            .await?;
1529        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1530            VectorizerError::server(format!("Failed to parse chunks response: {e}"))
1531        })?;
1532        Ok(result)
1533    }
1534
1535    /// Generate hierarchical project structure overview
1536    pub async fn get_project_outline(
1537        &self,
1538        collection: &str,
1539        max_depth: Option<usize>,
1540        include_summaries: Option<bool>,
1541        highlight_key_files: Option<bool>,
1542    ) -> Result<serde_json::Value> {
1543        let mut payload = serde_json::Map::new();
1544        payload.insert(
1545            "collection".to_string(),
1546            serde_json::Value::String(collection.to_string()),
1547        );
1548
1549        if let Some(depth) = max_depth {
1550            payload.insert(
1551                "max_depth".to_string(),
1552                serde_json::Value::Number(depth.into()),
1553            );
1554        }
1555        if let Some(summ) = include_summaries {
1556            payload.insert(
1557                "include_summaries".to_string(),
1558                serde_json::Value::Bool(summ),
1559            );
1560        }
1561        if let Some(highlight) = highlight_key_files {
1562            payload.insert(
1563                "highlight_key_files".to_string(),
1564                serde_json::Value::Bool(highlight),
1565            );
1566        }
1567
1568        let response = self
1569            .make_request(
1570                "POST",
1571                "/file/outline",
1572                Some(serde_json::Value::Object(payload)),
1573            )
1574            .await?;
1575        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1576            VectorizerError::server(format!("Failed to parse outline response: {e}"))
1577        })?;
1578        Ok(result)
1579    }
1580
1581    /// Find semantically related files using vector similarity
1582    pub async fn get_related_files(
1583        &self,
1584        collection: &str,
1585        file_path: &str,
1586        limit: Option<usize>,
1587        similarity_threshold: Option<f32>,
1588        include_reason: Option<bool>,
1589    ) -> Result<serde_json::Value> {
1590        let mut payload = serde_json::Map::new();
1591        payload.insert(
1592            "collection".to_string(),
1593            serde_json::Value::String(collection.to_string()),
1594        );
1595        payload.insert(
1596            "file_path".to_string(),
1597            serde_json::Value::String(file_path.to_string()),
1598        );
1599
1600        if let Some(lim) = limit {
1601            payload.insert("limit".to_string(), serde_json::Value::Number(lim.into()));
1602        }
1603        if let Some(thresh) = similarity_threshold {
1604            payload.insert(
1605                "similarity_threshold".to_string(),
1606                serde_json::json!(thresh),
1607            );
1608        }
1609        if let Some(reason) = include_reason {
1610            payload.insert(
1611                "include_reason".to_string(),
1612                serde_json::Value::Bool(reason),
1613            );
1614        }
1615
1616        let response = self
1617            .make_request(
1618                "POST",
1619                "/file/related",
1620                Some(serde_json::Value::Object(payload)),
1621            )
1622            .await?;
1623        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1624            VectorizerError::server(format!("Failed to parse related files response: {e}"))
1625        })?;
1626        Ok(result)
1627    }
1628
1629    /// Semantic search filtered by file type
1630    pub async fn search_by_file_type(
1631        &self,
1632        collection: &str,
1633        query: &str,
1634        file_types: Vec<String>,
1635        limit: Option<usize>,
1636        return_full_files: Option<bool>,
1637    ) -> Result<serde_json::Value> {
1638        // Validate file_types is not empty
1639        if file_types.is_empty() {
1640            return Err(VectorizerError::validation("file_types cannot be empty"));
1641        }
1642
1643        let mut payload = serde_json::Map::new();
1644        payload.insert(
1645            "collection".to_string(),
1646            serde_json::Value::String(collection.to_string()),
1647        );
1648        payload.insert(
1649            "query".to_string(),
1650            serde_json::Value::String(query.to_string()),
1651        );
1652        payload.insert(
1653            "file_types".to_string(),
1654            serde_json::to_value(file_types).unwrap(),
1655        );
1656
1657        if let Some(lim) = limit {
1658            payload.insert("limit".to_string(), serde_json::Value::Number(lim.into()));
1659        }
1660        if let Some(full) = return_full_files {
1661            payload.insert(
1662                "return_full_files".to_string(),
1663                serde_json::Value::Bool(full),
1664            );
1665        }
1666
1667        let response = self
1668            .make_request(
1669                "POST",
1670                "/file/search_by_type",
1671                Some(serde_json::Value::Object(payload)),
1672            )
1673            .await?;
1674        let result: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
1675            VectorizerError::server(format!("Failed to parse search by type response: {e}"))
1676        })?;
1677        Ok(result)
1678    }
1679
1680    /// Make HTTP request
1681    async fn make_request(
1682        &self,
1683        method: &str,
1684        endpoint: &str,
1685        payload: Option<serde_json::Value>,
1686    ) -> Result<String> {
1687        match method {
1688            "GET" => self.transport.get(endpoint).await,
1689            "POST" => self.transport.post(endpoint, payload.as_ref()).await,
1690            "PUT" => self.transport.put(endpoint, payload.as_ref()).await,
1691            "DELETE" => self.transport.delete(endpoint).await,
1692            _ => Err(VectorizerError::configuration(format!(
1693                "Unsupported method: {}",
1694                method
1695            ))),
1696        }
1697    }
1698
1699    // ========== Graph Operations ==========
1700
1701    /// List all nodes in a collection's graph
1702    pub async fn list_graph_nodes(&self, collection: &str) -> Result<ListNodesResponse> {
1703        let url = format!("/graph/nodes/{}", collection);
1704        let response = self.make_request("GET", &url, None).await?;
1705        let result: ListNodesResponse = serde_json::from_str(&response).map_err(|e| {
1706            VectorizerError::server(format!("Failed to parse list nodes response: {e}"))
1707        })?;
1708        Ok(result)
1709    }
1710
1711    /// Get neighbors of a specific node
1712    pub async fn get_graph_neighbors(
1713        &self,
1714        collection: &str,
1715        node_id: &str,
1716    ) -> Result<GetNeighborsResponse> {
1717        let url = format!("/graph/nodes/{}/{}/neighbors", collection, node_id);
1718        let response = self.make_request("GET", &url, None).await?;
1719        let result: GetNeighborsResponse = serde_json::from_str(&response).map_err(|e| {
1720            VectorizerError::server(format!("Failed to parse neighbors response: {e}"))
1721        })?;
1722        Ok(result)
1723    }
1724
1725    /// Find related nodes within N hops
1726    pub async fn find_related_nodes(
1727        &self,
1728        collection: &str,
1729        node_id: &str,
1730        request: FindRelatedRequest,
1731    ) -> Result<FindRelatedResponse> {
1732        let url = format!("/graph/nodes/{}/{}/related", collection, node_id);
1733        let payload = serde_json::to_value(&request).map_err(|e| {
1734            VectorizerError::validation(format!("Failed to serialize request: {e}"))
1735        })?;
1736        let response = self.make_request("POST", &url, Some(payload)).await?;
1737        let result: FindRelatedResponse = serde_json::from_str(&response).map_err(|e| {
1738            VectorizerError::server(format!("Failed to parse related nodes response: {e}"))
1739        })?;
1740        Ok(result)
1741    }
1742
1743    /// Find shortest path between two nodes
1744    pub async fn find_graph_path(&self, request: FindPathRequest) -> Result<FindPathResponse> {
1745        let payload = serde_json::to_value(&request).map_err(|e| {
1746            VectorizerError::validation(format!("Failed to serialize request: {e}"))
1747        })?;
1748        let response = self
1749            .make_request("POST", "/graph/path", Some(payload))
1750            .await?;
1751        let result: FindPathResponse = serde_json::from_str(&response)
1752            .map_err(|e| VectorizerError::server(format!("Failed to parse path response: {e}")))?;
1753        Ok(result)
1754    }
1755
1756    /// Create an explicit edge between two nodes
1757    pub async fn create_graph_edge(
1758        &self,
1759        request: CreateEdgeRequest,
1760    ) -> Result<CreateEdgeResponse> {
1761        let payload = serde_json::to_value(&request).map_err(|e| {
1762            VectorizerError::validation(format!("Failed to serialize request: {e}"))
1763        })?;
1764        let response = self
1765            .make_request("POST", "/graph/edges", Some(payload))
1766            .await?;
1767        let result: CreateEdgeResponse = serde_json::from_str(&response).map_err(|e| {
1768            VectorizerError::server(format!("Failed to parse create edge response: {e}"))
1769        })?;
1770        Ok(result)
1771    }
1772
1773    /// Delete an edge by ID
1774    pub async fn delete_graph_edge(&self, edge_id: &str) -> Result<()> {
1775        let url = format!("/graph/edges/{}", edge_id);
1776        self.make_request("DELETE", &url, None).await?;
1777        Ok(())
1778    }
1779
1780    /// List all edges in a collection
1781    pub async fn list_graph_edges(&self, collection: &str) -> Result<ListEdgesResponse> {
1782        let url = format!("/graph/collections/{}/edges", collection);
1783        let response = self.make_request("GET", &url, None).await?;
1784        let result: ListEdgesResponse = serde_json::from_str(&response).map_err(|e| {
1785            VectorizerError::server(format!("Failed to parse list edges response: {}", e))
1786        })?;
1787        Ok(result)
1788    }
1789
1790    /// Discover SIMILAR_TO edges for entire collection
1791    pub async fn discover_graph_edges(
1792        &self,
1793        collection: &str,
1794        request: DiscoverEdgesRequest,
1795    ) -> Result<DiscoverEdgesResponse> {
1796        let url = format!("/graph/discover/{}", collection);
1797        let payload = serde_json::to_value(&request).map_err(|e| {
1798            VectorizerError::validation(format!("Failed to serialize request: {e}"))
1799        })?;
1800        let response = self.make_request("POST", &url, Some(payload)).await?;
1801        let result: DiscoverEdgesResponse = serde_json::from_str(&response).map_err(|e| {
1802            VectorizerError::server(format!("Failed to parse discover edges response: {}", e))
1803        })?;
1804        Ok(result)
1805    }
1806
1807    /// Discover SIMILAR_TO edges for a specific node
1808    pub async fn discover_graph_edges_for_node(
1809        &self,
1810        collection: &str,
1811        node_id: &str,
1812        request: DiscoverEdgesRequest,
1813    ) -> Result<DiscoverEdgesResponse> {
1814        let url = format!("/graph/discover/{collection}/{node_id}");
1815        let payload = serde_json::to_value(&request).map_err(|e| {
1816            VectorizerError::validation(format!("Failed to serialize request: {e}"))
1817        })?;
1818        let response = self.make_request("POST", &url, Some(payload)).await?;
1819        let result: DiscoverEdgesResponse = serde_json::from_str(&response).map_err(|e| {
1820            VectorizerError::server(format!("Failed to parse discover edges response: {}", e))
1821        })?;
1822        Ok(result)
1823    }
1824
1825    /// Get discovery status for a collection
1826    pub async fn get_graph_discovery_status(
1827        &self,
1828        collection: &str,
1829    ) -> Result<DiscoveryStatusResponse> {
1830        let url = format!("/graph/discover/{}/status", collection);
1831        let response = self.make_request("GET", &url, None).await?;
1832        let result: DiscoveryStatusResponse = serde_json::from_str(&response).map_err(|e| {
1833            VectorizerError::server(format!("Failed to parse discovery status response: {}", e))
1834        })?;
1835        Ok(result)
1836    }
1837
1838    // ============== FILE UPLOAD METHODS ==============
1839
1840    /// Upload a file for automatic text extraction, chunking, and indexing.
1841    ///
1842    /// # Arguments
1843    /// * `file_bytes` - File content as bytes
1844    /// * `filename` - Name of the file (used for extension detection)
1845    /// * `collection_name` - Target collection name
1846    /// * `options` - Upload options (chunk size, overlap, metadata)
1847    ///
1848    /// # Example
1849    /// ```no_run
1850    /// use vectorizer_sdk::{VectorizerClient, ClientConfig, UploadFileOptions};
1851    /// use std::collections::HashMap;
1852    ///
1853    /// #[tokio::main]
1854    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
1855    ///     let config = ClientConfig::default();
1856    ///     let client = VectorizerClient::new(config)?;
1857    ///
1858    ///     let file_bytes = std::fs::read("document.pdf")?;
1859    ///     let options = UploadFileOptions::default();
1860    ///
1861    ///     let response = client.upload_file(
1862    ///         file_bytes,
1863    ///         "document.pdf",
1864    ///         "my-docs",
1865    ///         options
1866    ///     ).await?;
1867    ///
1868    ///     println!("Uploaded: {} chunks created", response.chunks_created);
1869    ///     Ok(())
1870    /// }
1871    /// ```
1872    pub async fn upload_file(
1873        &self,
1874        file_bytes: Vec<u8>,
1875        filename: &str,
1876        collection_name: &str,
1877        options: UploadFileOptions,
1878    ) -> Result<FileUploadResponse> {
1879        let mut form_fields = std::collections::HashMap::new();
1880        form_fields.insert("collection_name".to_string(), collection_name.to_string());
1881
1882        if let Some(chunk_size) = options.chunk_size {
1883            form_fields.insert("chunk_size".to_string(), chunk_size.to_string());
1884        }
1885
1886        if let Some(chunk_overlap) = options.chunk_overlap {
1887            form_fields.insert("chunk_overlap".to_string(), chunk_overlap.to_string());
1888        }
1889
1890        if let Some(metadata) = options.metadata {
1891            let metadata_json = serde_json::to_string(&metadata).map_err(|e| {
1892                VectorizerError::validation(format!("Failed to serialize metadata: {e}"))
1893            })?;
1894            form_fields.insert("metadata".to_string(), metadata_json);
1895        }
1896
1897        if let Some(public_key) = options.public_key {
1898            form_fields.insert("public_key".to_string(), public_key);
1899        }
1900
1901        // Use HttpTransport's multipart method
1902        let http_transport = crate::http_transport::HttpTransport::new(
1903            &self.base_url,
1904            self.config.api_key.as_deref(),
1905            self.config.timeout_secs.unwrap_or(30),
1906        )?;
1907
1908        let response = http_transport
1909            .post_multipart("/files/upload", file_bytes, filename, form_fields)
1910            .await?;
1911
1912        let result: FileUploadResponse = serde_json::from_str(&response).map_err(|e| {
1913            VectorizerError::server(format!("Failed to parse upload response: {e}"))
1914        })?;
1915
1916        Ok(result)
1917    }
1918
1919    /// Upload file content directly as a string.
1920    ///
1921    /// This is a convenience method that accepts text content directly instead of file bytes.
1922    ///
1923    /// # Arguments
1924    /// * `content` - File content as string
1925    /// * `filename` - Name of the file (used for extension detection)
1926    /// * `collection_name` - Target collection name
1927    /// * `options` - Upload options (chunk size, overlap, metadata)
1928    ///
1929    /// # Example
1930    /// ```no_run
1931    /// use vectorizer_sdk::{VectorizerClient, ClientConfig, UploadFileOptions};
1932    ///
1933    /// #[tokio::main]
1934    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
1935    ///     let config = ClientConfig::default();
1936    ///     let client = VectorizerClient::new(config)?;
1937    ///
1938    ///     let code = r#"fn main() { println!("Hello!"); }"#;
1939    ///     let options = UploadFileOptions::default();
1940    ///
1941    ///     let response = client.upload_file_content(
1942    ///         code,
1943    ///         "main.rs",
1944    ///         "rust-code",
1945    ///         options
1946    ///     ).await?;
1947    ///
1948    ///     println!("Uploaded: {} vectors created", response.vectors_created);
1949    ///     Ok(())
1950    /// }
1951    /// ```
1952    pub async fn upload_file_content(
1953        &self,
1954        content: &str,
1955        filename: &str,
1956        collection_name: &str,
1957        options: UploadFileOptions,
1958    ) -> Result<FileUploadResponse> {
1959        let file_bytes = content.as_bytes().to_vec();
1960        self.upload_file(file_bytes, filename, collection_name, options)
1961            .await
1962    }
1963
1964    /// Get file upload configuration from the server.
1965    ///
1966    /// Returns the maximum file size, allowed extensions, and default chunk settings.
1967    ///
1968    /// # Example
1969    /// ```no_run
1970    /// use vectorizer_sdk::{VectorizerClient, ClientConfig};
1971    ///
1972    /// #[tokio::main]
1973    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
1974    ///     let config = ClientConfig::default();
1975    ///     let client = VectorizerClient::new(config)?;
1976    ///
1977    ///     let upload_config = client.get_upload_config().await?;
1978    ///     println!("Max file size: {}MB", upload_config.max_file_size_mb);
1979    ///     println!("Allowed extensions: {:?}", upload_config.allowed_extensions);
1980    ///     Ok(())
1981    /// }
1982    /// ```
1983    pub async fn get_upload_config(&self) -> Result<FileUploadConfig> {
1984        let response = self.make_request("GET", "/files/config", None).await?;
1985        let result: FileUploadConfig = serde_json::from_str(&response)
1986            .map_err(|e| VectorizerError::server(format!("Failed to parse upload config: {e}")))?;
1987        Ok(result)
1988    }
1989}