1use super::VectorizerClient;
10use crate::error::{Result, VectorizerError};
11use crate::models::hybrid_search::{
12 HybridScoringAlgorithm, HybridSearchRequest, HybridSearchResponse,
13};
14use crate::models::{ExplainRequest, ExplainResponse, ExplainTrace, *};
15
16impl VectorizerClient {
17 pub async fn search_vectors(
22 &self,
23 collection: &str,
24 query: &str,
25 limit: Option<usize>,
26 score_threshold: Option<f32>,
27 ) -> Result<SearchResponse> {
28 let mut payload = serde_json::Map::new();
29 payload.insert(
30 "query".to_string(),
31 serde_json::Value::String(query.to_string()),
32 );
33 payload.insert(
34 "limit".to_string(),
35 serde_json::Value::Number(limit.unwrap_or(10).into()),
36 );
37 if let Some(threshold) = score_threshold {
38 payload.insert(
39 "score_threshold".to_string(),
40 serde_json::Value::Number(serde_json::Number::from_f64(threshold as f64).unwrap()),
41 );
42 }
43 let response = self
44 .make_request(
45 "POST",
46 &format!("/collections/{collection}/search/text"),
47 Some(serde_json::Value::Object(payload)),
48 )
49 .await?;
50 let search_response: SearchResponse = serde_json::from_str(&response).map_err(|e| {
51 VectorizerError::server(format!("Failed to parse search response: {e}"))
52 })?;
53 Ok(search_response)
54 }
55
56 pub async fn intelligent_search(
59 &self,
60 request: IntelligentSearchRequest,
61 ) -> Result<IntelligentSearchResponse> {
62 let response = self
63 .make_request(
64 "POST",
65 "/intelligent_search",
66 Some(serde_json::to_value(request).unwrap()),
67 )
68 .await?;
69 serde_json::from_str(&response).map_err(|e| {
70 VectorizerError::server(format!("Failed to parse intelligent search response: {e}"))
71 })
72 }
73
74 pub async fn semantic_search(
77 &self,
78 request: SemanticSearchRequest,
79 ) -> Result<SemanticSearchResponse> {
80 let response = self
81 .make_request(
82 "POST",
83 "/semantic_search",
84 Some(serde_json::to_value(request).unwrap()),
85 )
86 .await?;
87 serde_json::from_str(&response).map_err(|e| {
88 VectorizerError::server(format!("Failed to parse semantic search response: {e}"))
89 })
90 }
91
92 pub async fn contextual_search(
95 &self,
96 request: ContextualSearchRequest,
97 ) -> Result<ContextualSearchResponse> {
98 let response = self
99 .make_request(
100 "POST",
101 "/contextual_search",
102 Some(serde_json::to_value(request).unwrap()),
103 )
104 .await?;
105 serde_json::from_str(&response).map_err(|e| {
106 VectorizerError::server(format!("Failed to parse contextual search response: {e}"))
107 })
108 }
109
110 pub async fn multi_collection_search(
113 &self,
114 request: MultiCollectionSearchRequest,
115 ) -> Result<MultiCollectionSearchResponse> {
116 let response = self
117 .make_request(
118 "POST",
119 "/multi_collection_search",
120 Some(serde_json::to_value(request).unwrap()),
121 )
122 .await?;
123 serde_json::from_str(&response).map_err(|e| {
124 VectorizerError::server(format!(
125 "Failed to parse multi-collection search response: {e}"
126 ))
127 })
128 }
129
130 pub async fn search_by_file(
135 &self,
136 collection: &str,
137 request: SearchByFileRequest,
138 ) -> Result<SearchResponse> {
139 let url = format!("/collections/{collection}/search/file");
140 let payload = serde_json::json!({
141 "file_path": request.file_path,
142 "limit": request.limit.unwrap_or(10),
143 });
144 let response = self.make_request("POST", &url, Some(payload)).await?;
145 serde_json::from_str(&response).map_err(|e| {
146 VectorizerError::server(format!("Failed to parse search_by_file response: {e}"))
147 })
148 }
149
150 pub async fn explain_search(
162 &self,
163 collection: &str,
164 request: crate::models::ExplainRequest,
165 ) -> Result<crate::models::ExplainResponse> {
166 let mut payload = serde_json::json!({ "vector": request.vector });
167 if let Some(k) = request.k {
168 payload
169 .as_object_mut()
170 .map(|o| o.insert("k".to_string(), serde_json::json!(k)));
171 }
172 let response = self
173 .make_request(
174 "POST",
175 &format!("/collections/{collection}/explain"),
176 Some(payload),
177 )
178 .await?;
179 serde_json::from_str(&response).map_err(|e| {
180 VectorizerError::server(format!("Failed to parse explain_search response: {e}"))
181 })
182 }
183
184 pub async fn hybrid_search(
187 &self,
188 request: HybridSearchRequest,
189 ) -> Result<HybridSearchResponse> {
190 let url = format!("/collections/{}/hybrid_search", request.collection);
191 let payload = serde_json::json!({
192 "query": request.query,
193 "alpha": request.alpha,
194 "algorithm": match request.algorithm {
195 HybridScoringAlgorithm::ReciprocalRankFusion => "rrf",
196 HybridScoringAlgorithm::WeightedCombination => "weighted",
197 HybridScoringAlgorithm::AlphaBlending => "alpha",
198 },
199 "dense_k": request.dense_k,
200 "sparse_k": request.sparse_k,
201 "final_k": request.final_k,
202 "query_sparse": request.query_sparse.as_ref().map(|sv| serde_json::json!({
203 "indices": sv.indices,
204 "values": sv.values,
205 })),
206 });
207 let response = self.make_request("POST", &url, Some(payload)).await?;
208 serde_json::from_str(&response).map_err(|e| {
209 VectorizerError::server(format!("Failed to parse hybrid search response: {e}"))
210 })
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use serde_json::json;
217
218 use crate::models::{ExplainRequest, ExplainResponse, ExplainTrace};
219
220 #[test]
221 fn explain_request_serialize_with_k() {
222 let req = ExplainRequest {
223 vector: vec![0.1, 0.2, 0.3],
224 k: Some(5),
225 };
226 let v = serde_json::to_value(&req).unwrap();
227 assert_eq!(v["k"], 5);
228 let first = v["vector"][0].as_f64().unwrap();
230 assert!((first - 0.1_f64).abs() < 1e-6, "unexpected value: {first}");
231 }
232
233 #[test]
234 fn explain_request_serialize_without_k() {
235 let req = ExplainRequest {
236 vector: vec![0.1],
237 k: None,
238 };
239 let v = serde_json::to_value(&req).unwrap();
240 assert!(v.get("k").is_none());
242 }
243
244 #[test]
245 fn explain_response_wire_shape() {
246 let raw = json!({
248 "collection": "docs",
249 "k": 10,
250 "results": [
251 { "id": "vec-1", "score": 0.95, "payload": null }
252 ],
253 "trace": {
254 "visited_nodes": 120,
255 "ef_search": 100,
256 "hnsw_search_ms": 1.23,
257 "payload_filter_evals": 0,
258 "quantization_score_ms": 0.45,
259 "total_ms": 2.10,
260 }
261 });
262 let resp: ExplainResponse = serde_json::from_value(raw).unwrap();
263 assert_eq!(resp.collection, "docs");
264 assert_eq!(resp.k, 10);
265 assert_eq!(resp.results.len(), 1);
266 assert_eq!(resp.trace.visited_nodes, 120);
267 assert_eq!(resp.trace.ef_search, 100);
268 assert!((resp.trace.hnsw_search_ms - 1.23).abs() < 1e-6);
269 }
270
271 #[test]
272 fn explain_trace_deserializes_all_fields() {
273 let raw = json!({
274 "visited_nodes": 200,
275 "ef_search": 64,
276 "hnsw_search_ms": 3.5,
277 "payload_filter_evals": 10,
278 "quantization_score_ms": 0.8,
279 "total_ms": 5.0,
280 });
281 let t: ExplainTrace = serde_json::from_value(raw).unwrap();
282 assert_eq!(t.payload_filter_evals, 10);
283 assert!((t.quantization_score_ms - 0.8).abs() < 1e-9);
284 }
285}