qail_qdrant/
protocol.rs

1//! Qdrant REST/JSON protocol encoding.
2//!
3//! This module handles encoding QAIL AST to Qdrant's REST API JSON format.
4//! Using JSON instead of gRPC for simplicity and portability.
5
6use crate::error::QdrantResult;
7use crate::point::{PayloadValue, Point, PointId, ScoredPoint};
8use serde_json::{json, Value as JsonValue};
9
10/// Encode a vector search request to JSON format.
11///
12/// Generates JSON for POST /collections/{collection}/points/search
13///
14/// Example output:
15/// ```json
16/// {
17///   "vector": [0.1, 0.2, 0.3],
18///   "limit": 10,
19///   "offset": 0,
20///   "with_payload": true,
21///   "filter": { ... }
22/// }
23/// ```
24pub fn encode_search_request(
25    vector: &[f32],
26    limit: u64,
27    offset: Option<u64>,
28    score_threshold: Option<f32>,
29    with_vector: bool,
30) -> Vec<u8> {
31    let mut request = json!({
32        "vector": vector,
33        "limit": limit,
34        "with_payload": true,
35        "with_vector": with_vector,
36    });
37    
38    if let Some(off) = offset {
39        request["offset"] = json!(off);
40    }
41    
42    if let Some(threshold) = score_threshold {
43        request["score_threshold"] = json!(threshold);
44    }
45    
46    serde_json::to_vec(&request).unwrap_or_default()
47}
48
49/// Encode search request with filter conditions.
50pub fn encode_search_request_with_filter(
51    vector: &[f32],
52    limit: u64,
53    offset: Option<u64>,
54    score_threshold: Option<f32>,
55    with_vector: bool,
56    filter: JsonValue,
57) -> Vec<u8> {
58    let mut request = json!({
59        "vector": vector,
60        "limit": limit,
61        "with_payload": true,
62        "with_vector": with_vector,
63        "filter": filter,
64    });
65    
66    if let Some(off) = offset {
67        request["offset"] = json!(off);
68    }
69    
70    if let Some(threshold) = score_threshold {
71        request["score_threshold"] = json!(threshold);
72    }
73    
74    serde_json::to_vec(&request).unwrap_or_default()
75}
76
77/// Encode an upsert (insert/update) request to JSON.
78///
79/// Generates JSON for PUT /collections/{collection}/points
80///
81/// Example output:
82/// ```json
83/// {
84///   "points": [
85///     { "id": "abc123", "vector": [0.1, 0.2], "payload": {"name": "test"} }
86///   ]
87/// }
88/// ```
89pub fn encode_upsert_request(points: &[Point]) -> Vec<u8> {
90    let points_json: Vec<JsonValue> = points
91        .iter()
92        .map(|p| {
93            let id = match &p.id {
94                PointId::Uuid(s) => json!(s),
95                PointId::Num(n) => json!(n),
96            };
97            
98            let payload: JsonValue = p.payload
99                .iter()
100                .map(|(k, v)| (k.clone(), payload_value_to_json(v)))
101                .collect();
102            
103            json!({
104                "id": id,
105                "vector": p.vector,
106                "payload": payload,
107            })
108        })
109        .collect();
110    
111    let request = json!({ "points": points_json });
112    serde_json::to_vec(&request).unwrap_or_default()
113}
114
115/// Encode an upsert request for multi-vector points (named vectors).
116///
117/// For collections with multiple vector fields (e.g., "title", "content").
118pub fn encode_upsert_multi_vector_request(points: &[crate::point::MultiVectorPoint]) -> Vec<u8> {
119    use crate::point::MultiVectorPoint;
120    
121    let points_json: Vec<JsonValue> = points
122        .iter()
123        .map(|p: &MultiVectorPoint| {
124            let id = match &p.id {
125                PointId::Uuid(s) => json!(s),
126                PointId::Num(n) => json!(n),
127            };
128            
129            let payload: JsonValue = p.payload
130                .iter()
131                .map(|(k, v)| (k.clone(), payload_value_to_json(v)))
132                .collect();
133            
134            // Named vectors as object
135            let vectors: JsonValue = p.vectors
136                .iter()
137                .map(|(k, v)| (k.clone(), json!(v)))
138                .collect();
139            
140            json!({
141                "id": id,
142                "vector": vectors,
143                "payload": payload,
144            })
145        })
146        .collect();
147    
148    let request = json!({ "points": points_json });
149    serde_json::to_vec(&request).unwrap_or_default()
150}
151
152/// Encode a delete request to JSON.
153///
154/// Generates JSON for POST /collections/{collection}/points/delete
155///
156/// Example output:
157/// ```json
158/// { "points": ["id1", "id2"] }
159/// ```
160pub fn encode_delete_request(ids: &[PointId]) -> Vec<u8> {
161    let ids_json: Vec<JsonValue> = ids
162        .iter()
163        .map(|id| match id {
164            PointId::Uuid(s) => json!(s),
165            PointId::Num(n) => json!(n),
166        })
167        .collect();
168    
169    let request = json!({ "points": ids_json });
170    serde_json::to_vec(&request).unwrap_or_default()
171}
172
173/// Encode create collection request.
174///
175/// Generates JSON for PUT /collections/{collection}
176pub fn encode_create_collection_request(
177    vector_size: u64,
178    distance: &str, // "Cosine", "Euclidean", "Dot"
179) -> Vec<u8> {
180    let request = json!({
181        "vectors": {
182            "size": vector_size,
183            "distance": distance,
184        }
185    });
186    serde_json::to_vec(&request).unwrap_or_default()
187}
188
189/// Convert QAIL conditions to Qdrant filter format.
190///
191/// Qdrant uses `must`, `should`, `must_not` arrays for filtering.
192/// Each condition becomes a clause in `must` (AND logic).
193///
194/// # Example
195/// ```ignore
196/// use qail_core::ast::{Condition, Operator, Expr, Value};
197///
198/// let conditions = vec![
199///     Condition { left: Expr::Named("category".into()), op: Operator::Eq, value: Value::String("electronics".into()), is_array_unnest: false },
200///     Condition { left: Expr::Named("price".into()), op: Operator::Lt, value: Value::Int(1000), is_array_unnest: false },
201/// ];
202///
203/// let filter = encode_conditions_to_filter(&conditions, false);
204/// // Returns: {"must": [{"key": "category", "match": {"value": "electronics"}}, {"key": "price", "range": {"lt": 1000}}]}
205/// ```
206pub fn encode_conditions_to_filter(conditions: &[qail_core::ast::Condition], is_or: bool) -> JsonValue {
207    use qail_core::ast::{Expr, Operator, Value};
208    
209    let clauses: Vec<JsonValue> = conditions
210        .iter()
211        .filter_map(|cond| {
212            // Extract field name from left expression
213            let key = match &cond.left {
214                Expr::Named(name) => name.clone(),
215                Expr::Aliased { name, .. } => name.clone(),
216                _ => return None,
217            };
218            
219            // Convert operator and value to Qdrant filter clause
220            let clause = match (&cond.op, &cond.value) {
221                // Match (equality)
222                (Operator::Eq, Value::String(s)) => json!({
223                    "key": key,
224                    "match": { "value": s }
225                }),
226                (Operator::Eq, Value::Int(n)) => json!({
227                    "key": key,
228                    "match": { "value": n }
229                }),
230                (Operator::Eq, Value::Bool(b)) => json!({
231                    "key": key,
232                    "match": { "value": b }
233                }),
234                
235                // Range operators
236                (Operator::Gt, Value::Int(n)) => json!({
237                    "key": key,
238                    "range": { "gt": n }
239                }),
240                (Operator::Gt, Value::Float(f)) => json!({
241                    "key": key,
242                    "range": { "gt": f }
243                }),
244                (Operator::Gte, Value::Int(n)) => json!({
245                    "key": key,
246                    "range": { "gte": n }
247                }),
248                (Operator::Gte, Value::Float(f)) => json!({
249                    "key": key,
250                    "range": { "gte": f }
251                }),
252                (Operator::Lt, Value::Int(n)) => json!({
253                    "key": key,
254                    "range": { "lt": n }
255                }),
256                (Operator::Lt, Value::Float(f)) => json!({
257                    "key": key,
258                    "range": { "lt": f }
259                }),
260                (Operator::Lte, Value::Int(n)) => json!({
261                    "key": key,
262                    "range": { "lte": n }
263                }),
264                (Operator::Lte, Value::Float(f)) => json!({
265                    "key": key,
266                    "range": { "lte": f }
267                }),
268                
269                // In / NotIn (array membership)
270                (Operator::In, Value::Array(arr)) => {
271                    let values: Vec<JsonValue> = arr.iter().filter_map(value_to_json).collect();
272                    json!({
273                        "key": key,
274                        "match": { "any": values }
275                    })
276                },
277                
278                // IsNull / IsNotNull
279                (Operator::IsNull, _) => json!({
280                    "is_null": { "key": key }
281                }),
282                (Operator::IsNotNull, _) => json!({
283                    "is_empty": { "key": key, "is_empty": false }
284                }),
285                
286                // Text/keyword match with contains
287                (Operator::Contains | Operator::Like, Value::String(s)) => json!({
288                    "key": key,
289                    "match": { "text": s }
290                }),
291                
292                // Default: try match for other types
293                (_, Value::String(s)) => json!({
294                    "key": key,
295                    "match": { "value": s }
296                }),
297                (_, Value::Int(n)) => json!({
298                    "key": key,
299                    "match": { "value": n }
300                }),
301                
302                _ => return None,
303            };
304            
305            Some(clause)
306        })
307        .collect();
308    
309    // Use "should" for OR, "must" for AND
310    if is_or {
311        json!({ "should": clauses })
312    } else {
313        json!({ "must": clauses })
314    }
315}
316
317/// Convert Value to JsonValue for filter encoding.
318fn value_to_json(value: &qail_core::ast::Value) -> Option<JsonValue> {
319    use qail_core::ast::Value;
320    match value {
321        Value::String(s) => Some(json!(s)),
322        Value::Int(n) => Some(json!(n)),
323        Value::Float(f) => Some(json!(f)),
324        Value::Bool(b) => Some(json!(b)),
325        Value::Null => Some(JsonValue::Null),
326        _ => None,
327    }
328}
329
330/// Decode search response from JSON.
331pub fn decode_search_response(data: &[u8]) -> QdrantResult<Vec<ScoredPoint>> {
332    let response: JsonValue = serde_json::from_slice(data)
333        .map_err(|e| crate::error::QdrantError::Decode(e.to_string()))?;
334    
335    let results = response["result"]
336        .as_array()
337        .ok_or_else(|| crate::error::QdrantError::Decode("Missing 'result' array".to_string()))?;
338    
339    let scored_points: Vec<ScoredPoint> = results
340        .iter()
341        .filter_map(|item| {
342            let id = parse_point_id(&item["id"])?;
343            let score = item["score"].as_f64()? as f32;
344            let payload = parse_payload(&item["payload"]);
345            let vector = item["vector"]
346                .as_array()
347                .map(|arr| arr.iter().filter_map(|v| v.as_f64().map(|f| f as f32)).collect());
348            
349            Some(ScoredPoint { id, score, payload, vector })
350        })
351        .collect();
352    
353    Ok(scored_points)
354}
355
356/// Parse a point ID from JSON.
357pub fn parse_point_id(value: &JsonValue) -> Option<PointId> {
358    if let Some(s) = value.as_str() {
359        Some(PointId::Uuid(s.to_string()))
360    } else {
361        value.as_u64().map(PointId::Num)
362    }
363}
364
365/// Parse payload from JSON object.
366pub fn parse_payload(value: &JsonValue) -> crate::point::Payload {
367    let mut payload = crate::point::Payload::new();
368    
369    if let Some(obj) = value.as_object() {
370        for (k, v) in obj {
371            if let Some(pv) = json_to_payload_value(v) {
372                payload.insert(k.clone(), pv);
373            }
374        }
375    }
376    
377    payload
378}
379
380/// Convert PayloadValue to JSON.
381fn payload_value_to_json(value: &PayloadValue) -> JsonValue {
382    match value {
383        PayloadValue::String(s) => json!(s),
384        PayloadValue::Integer(n) => json!(n),
385        PayloadValue::Float(f) => json!(f),
386        PayloadValue::Bool(b) => json!(b),
387        PayloadValue::List(arr) => {
388            JsonValue::Array(arr.iter().map(payload_value_to_json).collect())
389        }
390        PayloadValue::Object(obj) => {
391            JsonValue::Object(obj.iter().map(|(k, v)| (k.clone(), payload_value_to_json(v))).collect())
392        }
393        PayloadValue::Null => JsonValue::Null,
394    }
395}
396
397/// Convert JSON to PayloadValue.
398fn json_to_payload_value(value: &JsonValue) -> Option<PayloadValue> {
399    match value {
400        JsonValue::Null => Some(PayloadValue::Null),
401        JsonValue::Bool(b) => Some(PayloadValue::Bool(*b)),
402        JsonValue::Number(n) => {
403            if let Some(i) = n.as_i64() {
404                Some(PayloadValue::Integer(i))
405            } else {
406                n.as_f64().map(PayloadValue::Float)
407            }
408        }
409        JsonValue::String(s) => Some(PayloadValue::String(s.clone())),
410        JsonValue::Array(arr) => {
411            let items: Vec<PayloadValue> = arr.iter().filter_map(json_to_payload_value).collect();
412            Some(PayloadValue::List(items))
413        }
414        JsonValue::Object(obj) => {
415            let map: std::collections::HashMap<String, PayloadValue> = obj
416                .iter()
417                .filter_map(|(k, v)| json_to_payload_value(v).map(|pv| (k.clone(), pv)))
418                .collect();
419            Some(PayloadValue::Object(map))
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    
428    #[test]
429    fn test_encode_search_request() {
430        let vector = vec![0.1, 0.2, 0.3];
431        let json_bytes = encode_search_request(&vector, 10, None, None, false);
432        let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
433        
434        // Check structure exists
435        assert!(json["vector"].is_array());
436        assert_eq!(json["limit"], 10);
437        assert_eq!(json["with_payload"], true);
438        
439        // Check vector length
440        assert_eq!(json["vector"].as_array().unwrap().len(), 3);
441    }
442    
443    #[test]
444    fn test_encode_upsert_request() {
445        let point = Point::new("test-id", vec![0.5, 0.5]);
446        let json_bytes = encode_upsert_request(&[point]);
447        let json_str = String::from_utf8(json_bytes).unwrap();
448        
449        assert!(json_str.contains("\"points\""));
450        assert!(json_str.contains("\"test-id\""));
451        assert!(json_str.contains("[0.5,0.5]"));
452    }
453    
454    #[test]
455    fn test_encode_delete_request() {
456        let ids = vec![PointId::Uuid("id1".to_string()), PointId::Num(42)];
457        let json_bytes = encode_delete_request(&ids);
458        let json_str = String::from_utf8(json_bytes).unwrap();
459        
460        assert!(json_str.contains("\"id1\""));
461        assert!(json_str.contains("42"));
462    }
463    
464    #[test]
465    fn test_decode_search_response() {
466        let response = r#"{
467            "result": [
468                {"id": "abc", "score": 0.95, "payload": {"name": "test"}},
469                {"id": 123, "score": 0.80, "payload": {}}
470            ]
471        }"#;
472        
473        let results = decode_search_response(response.as_bytes()).unwrap();
474        assert_eq!(results.len(), 2);
475        assert_eq!(results[0].score, 0.95);
476        assert_eq!(results[1].score, 0.80);
477    }
478
479    #[test]
480    fn test_encode_conditions_to_filter() {
481        use qail_core::ast::{Condition, Expr, Operator, Value};
482        
483        let conditions = vec![
484            Condition {
485                left: Expr::Named("category".to_string()),
486                op: Operator::Eq,
487                value: Value::String("electronics".to_string()),
488                is_array_unnest: false,
489            },
490            Condition {
491                left: Expr::Named("price".to_string()),
492                op: Operator::Lt,
493                value: Value::Int(1000),
494                is_array_unnest: false,
495            },
496        ];
497        
498        let filter = encode_conditions_to_filter(&conditions, false);
499        
500        // Should have "must" with 2 clauses
501        assert!(filter["must"].is_array());
502        let must = filter["must"].as_array().unwrap();
503        assert_eq!(must.len(), 2);
504        
505        // First clause: category match
506        assert_eq!(must[0]["key"], "category");
507        assert_eq!(must[0]["match"]["value"], "electronics");
508        
509        // Second clause: price range
510        assert_eq!(must[1]["key"], "price");
511        assert_eq!(must[1]["range"]["lt"], 1000);
512    }
513
514    #[test]
515    fn test_encode_conditions_to_filter_or() {
516        use qail_core::ast::{Condition, Expr, Operator, Value};
517        
518        let conditions = vec![
519            Condition {
520                left: Expr::Named("status".to_string()),
521                op: Operator::Eq,
522                value: Value::String("active".to_string()),
523                is_array_unnest: false,
524            },
525        ];
526        
527        let filter = encode_conditions_to_filter(&conditions, true);
528        
529        // Should have "should" instead of "must"
530        assert!(filter["should"].is_array());
531        assert!(filter["must"].is_null());
532    }
533}