Skip to main content

vectorizer_sdk/client/
discovery.rs

1//! Discovery surface: orchestrated multi-stage retrieval.
2//!
3//! `discover` is the headline pipeline (filter → score → expand →
4//! search → bullet-summarise); the other three methods expose the
5//! individual stages so callers can swap or compose them.
6
7use super::VectorizerClient;
8use crate::error::{Result, VectorizerError};
9
10impl VectorizerClient {
11    /// End-to-end discovery pipeline with intelligent search and
12    /// LLM-style bullet generation.
13    #[allow(clippy::too_many_arguments)]
14    pub async fn discover(
15        &self,
16        query: &str,
17        include_collections: Option<Vec<String>>,
18        exclude_collections: Option<Vec<String>>,
19        max_bullets: Option<usize>,
20        broad_k: Option<usize>,
21        focus_k: Option<usize>,
22    ) -> Result<serde_json::Value> {
23        if query.trim().is_empty() {
24            return Err(VectorizerError::validation("Query cannot be empty"));
25        }
26        if let Some(max) = max_bullets
27            && max == 0
28        {
29            return Err(VectorizerError::validation(
30                "max_bullets must be greater than 0",
31            ));
32        }
33
34        let mut payload = serde_json::Map::new();
35        payload.insert(
36            "query".to_string(),
37            serde_json::Value::String(query.to_string()),
38        );
39        if let Some(inc) = include_collections {
40            payload.insert(
41                "include_collections".to_string(),
42                serde_json::to_value(inc).unwrap(),
43            );
44        }
45        if let Some(exc) = exclude_collections {
46            payload.insert(
47                "exclude_collections".to_string(),
48                serde_json::to_value(exc).unwrap(),
49            );
50        }
51        if let Some(max) = max_bullets {
52            payload.insert(
53                "max_bullets".to_string(),
54                serde_json::Value::Number(max.into()),
55            );
56        }
57        if let Some(k) = broad_k {
58            payload.insert("broad_k".to_string(), serde_json::Value::Number(k.into()));
59        }
60        if let Some(k) = focus_k {
61            payload.insert("focus_k".to_string(), serde_json::Value::Number(k.into()));
62        }
63
64        let response = self
65            .make_request(
66                "POST",
67                "/discover",
68                Some(serde_json::Value::Object(payload)),
69            )
70            .await?;
71        serde_json::from_str(&response)
72            .map_err(|e| VectorizerError::server(format!("Failed to parse discover response: {e}")))
73    }
74
75    /// Pre-filter collections by name patterns.
76    pub async fn filter_collections(
77        &self,
78        query: &str,
79        include: Option<Vec<String>>,
80        exclude: Option<Vec<String>>,
81    ) -> Result<serde_json::Value> {
82        if query.trim().is_empty() {
83            return Err(VectorizerError::validation("Query cannot be empty"));
84        }
85        let mut payload = serde_json::Map::new();
86        payload.insert(
87            "query".to_string(),
88            serde_json::Value::String(query.to_string()),
89        );
90        if let Some(inc) = include {
91            payload.insert("include".to_string(), serde_json::to_value(inc).unwrap());
92        }
93        if let Some(exc) = exclude {
94            payload.insert("exclude".to_string(), serde_json::to_value(exc).unwrap());
95        }
96        let response = self
97            .make_request(
98                "POST",
99                "/discovery/filter_collections",
100                Some(serde_json::Value::Object(payload)),
101            )
102            .await?;
103        serde_json::from_str(&response)
104            .map_err(|e| VectorizerError::server(format!("Failed to parse filter response: {e}")))
105    }
106
107    /// Rank collections by relevance to a query. The three weights
108    /// must each be in `[0.0, 1.0]` when supplied.
109    pub async fn score_collections(
110        &self,
111        query: &str,
112        name_match_weight: Option<f32>,
113        term_boost_weight: Option<f32>,
114        signal_boost_weight: Option<f32>,
115    ) -> Result<serde_json::Value> {
116        if let Some(w) = name_match_weight
117            && !(0.0..=1.0).contains(&w)
118        {
119            return Err(VectorizerError::validation(
120                "name_match_weight must be between 0.0 and 1.0",
121            ));
122        }
123        if let Some(w) = term_boost_weight
124            && !(0.0..=1.0).contains(&w)
125        {
126            return Err(VectorizerError::validation(
127                "term_boost_weight must be between 0.0 and 1.0",
128            ));
129        }
130        if let Some(w) = signal_boost_weight
131            && !(0.0..=1.0).contains(&w)
132        {
133            return Err(VectorizerError::validation(
134                "signal_boost_weight must be between 0.0 and 1.0",
135            ));
136        }
137
138        let mut payload = serde_json::Map::new();
139        payload.insert(
140            "query".to_string(),
141            serde_json::Value::String(query.to_string()),
142        );
143        if let Some(w) = name_match_weight {
144            payload.insert("name_match_weight".to_string(), serde_json::json!(w));
145        }
146        if let Some(w) = term_boost_weight {
147            payload.insert("term_boost_weight".to_string(), serde_json::json!(w));
148        }
149        if let Some(w) = signal_boost_weight {
150            payload.insert("signal_boost_weight".to_string(), serde_json::json!(w));
151        }
152        let response = self
153            .make_request(
154                "POST",
155                "/discovery/score_collections",
156                Some(serde_json::Value::Object(payload)),
157            )
158            .await?;
159        serde_json::from_str(&response)
160            .map_err(|e| VectorizerError::server(format!("Failed to parse score response: {e}")))
161    }
162
163    /// Generate query variations (definition / features /
164    /// architecture-style expansions, capped by `max_expansions`).
165    pub async fn expand_queries(
166        &self,
167        query: &str,
168        max_expansions: Option<usize>,
169        include_definition: Option<bool>,
170        include_features: Option<bool>,
171        include_architecture: Option<bool>,
172    ) -> Result<serde_json::Value> {
173        let mut payload = serde_json::Map::new();
174        payload.insert(
175            "query".to_string(),
176            serde_json::Value::String(query.to_string()),
177        );
178        if let Some(max) = max_expansions {
179            payload.insert(
180                "max_expansions".to_string(),
181                serde_json::Value::Number(max.into()),
182            );
183        }
184        if let Some(def) = include_definition {
185            payload.insert(
186                "include_definition".to_string(),
187                serde_json::Value::Bool(def),
188            );
189        }
190        if let Some(feat) = include_features {
191            payload.insert(
192                "include_features".to_string(),
193                serde_json::Value::Bool(feat),
194            );
195        }
196        if let Some(arch) = include_architecture {
197            payload.insert(
198                "include_architecture".to_string(),
199                serde_json::Value::Bool(arch),
200            );
201        }
202        let response = self
203            .make_request(
204                "POST",
205                "/discovery/expand_queries",
206                Some(serde_json::Value::Object(payload)),
207            )
208            .await?;
209        serde_json::from_str(&response)
210            .map_err(|e| VectorizerError::server(format!("Failed to parse expand response: {e}")))
211    }
212}