Skip to main content

qail_core/transpiler/nosql/
qdrant.rs

1use crate::ast::*;
2
3fn json_string(value: &str) -> String {
4    serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
5}
6
7/// Trait for converting QAIL AST to Qdrant vector-search JSON.
8pub trait ToQdrant {
9    /// Convert a QAIL query into a Qdrant search/upsert/delete JSON body.
10    fn to_qdrant_search(&self) -> String;
11}
12
13impl ToQdrant for Qail {
14    fn to_qdrant_search(&self) -> String {
15        let result = match self.action {
16            Action::Get => build_qdrant_search(self),
17            Action::Put | Action::Add => build_qdrant_upsert(self),
18            Action::Del => build_qdrant_delete(self),
19            _ => {
20                return format!(
21                    "{{ \"error\": \"Action {:?} not supported for Qdrant\" }}",
22                    self.action
23                );
24            }
25        };
26
27        result.unwrap_or_else(|err| qdrant_error(&err))
28    }
29}
30
31fn qdrant_error(message: &str) -> String {
32    format!("{{ \"error\": {} }}", json_string(message))
33}
34
35fn build_qdrant_upsert(cmd: &Qail) -> Result<String, String> {
36    // POST /collections/{name}/points?wait=true
37    // Body: { "points": [ { "id": 1, "vector": [...], "payload": {...} } ] }
38    // let mut points = Vec::new(); // Unused
39
40    // Single point upsert from payload/filter cages.
41    let mut point_id = "0".to_string(); // Default ID?
42    let mut vector = "[0.0]".to_string();
43    let mut payload_parts = Vec::new();
44
45    for cage in &cmd.cages {
46        match cage.kind {
47            CageKind::Payload | CageKind::Filter => {
48                for cond in &cage.conditions {
49                    if let Expr::Named(name) = &cond.left {
50                        if name == "id" {
51                            point_id = value_to_json(&cond.value)?;
52                        } else if name == "vector" {
53                            vector = vector_to_json(&cond.value)?;
54                        } else {
55                            payload_parts.push(format!(
56                                "{}: {}",
57                                json_string(name),
58                                value_to_json(&cond.value)?
59                            ));
60                        }
61                    }
62                }
63            }
64            _ => {}
65        }
66    }
67
68    let payload_json = if payload_parts.is_empty() {
69        "{}".to_string()
70    } else {
71        format!("{{ {} }}", payload_parts.join(", "))
72    };
73
74    // Construct single point
75    let point = format!(
76        "{{ \"id\": {}, \"vector\": {}, \"payload\": {} }}",
77        point_id, vector, payload_json
78    );
79
80    Ok(format!("{{ \"points\": [{}] }}", point))
81}
82
83fn build_qdrant_delete(cmd: &Qail) -> Result<String, String> {
84    // POST /collections/{name}/points/delete
85    // Body: { "points": [1, 2, 3] } OR { "filter": ... }
86
87    // If ID specified, delete by ID. Else delete by filter.
88    let mut ids = Vec::new();
89
90    for cage in &cmd.cages {
91        if let CageKind::Filter = cage.kind {
92            for cond in &cage.conditions {
93                if let Expr::Named(name) = &cond.left
94                    && name == "id"
95                {
96                    ids.push(value_to_json(&cond.value)?);
97                }
98            }
99        }
100    }
101
102    if !ids.is_empty() {
103        Ok(format!("{{ \"points\": [{}] }}", ids.join(", ")))
104    } else {
105        // Delete by filter
106        let filter = build_filter(cmd)?;
107        if filter.is_empty() {
108            return Err("Qdrant delete requires an id or filter condition".to_string());
109        }
110        Ok(format!("{{ \"filter\": {} }}", filter))
111    }
112}
113
114fn build_qdrant_search(cmd: &Qail) -> Result<String, String> {
115    // Target endpoint: POST /collections/{collection_name}/points/search
116    // Output: JSON Body
117
118    let mut parts = Vec::new();
119
120    // 1. Vector handling
121    // We look for a condition with the key "vector" or similar, usage: [vector~[0.1, 0.2]]
122    // Any array value with a Fuzzy match (~) is treated as the query vector.
123    let mut vector_found = false;
124
125    for cage in &cmd.cages {
126        if let CageKind::Filter = cage.kind {
127            for cond in &cage.conditions {
128                if cond.op == Operator::Fuzzy {
129                    // Vector Query found.
130                    // Case 1: [vector~[0.1, 0.2]] -> Explicit Vector (Already handled by Value::Array)
131                    // Case 2: [vector~"cute cat"] -> Semantic Search Intent
132                    match &cond.value {
133                        Value::String(s) => {
134                            // Output Placeholder for Runtime Resolution
135                            // e.g. {{EMBED:cute cat}}
136                            parts.push(format!(
137                                "\"vector\": {}",
138                                json_string(&format!("{{{{EMBED:{}}}}}", s))
139                            ));
140                        }
141                        _ => {
142                            parts.push(format!("\"vector\": {}", vector_to_json(&cond.value)?));
143                        }
144                    }
145                    vector_found = true;
146                    break;
147                }
148            }
149        }
150        if vector_found {
151            break;
152        }
153    }
154
155    if !vector_found {
156        // Actually, Qdrant supports Scroll API separate from Search.
157        parts.push("\"vector\": [0.0]".to_string()); // Dummy vector or error? Let's use dummy to show intent.
158    }
159
160    // 2. Filters (Hybrid Search)
161    let filter = build_filter(cmd)?;
162    if !filter.is_empty() {
163        parts.push(format!("\"filter\": {}", filter));
164    }
165
166    // 3. Limit
167    let mut limit = 10;
168    if let Some(l) = get_cage_val(cmd, CageKind::Limit(0)) {
169        limit = l;
170    }
171    parts.push(format!("\"limit\": {}", limit));
172
173    // 4. With Payload (Projections)
174    if !cmd.columns.is_empty() {
175        let mut incl = Vec::new();
176        for c in &cmd.columns {
177            if let Expr::Named(n) = c {
178                incl.push(json_string(n));
179            }
180        }
181        parts.push(format!(
182            "\"with_payload\": {{ \"include\": [{}] }}",
183            incl.join(", ")
184        ));
185    } else {
186        parts.push("\"with_payload\": true".to_string());
187    }
188
189    Ok(format!("{{ {} }}", parts.join(", ")))
190}
191
192fn build_filter(cmd: &Qail) -> Result<String, String> {
193    // Qdrant Filter structure: { "must": [ { "key": "city", "match": { "value": "London" } } ] }
194    let mut musts = Vec::new();
195    let mut should_groups: Vec<Vec<String>> = Vec::new();
196
197    for cage in &cmd.cages {
198        if let CageKind::Filter = cage.kind {
199            let mut cage_clauses = Vec::new();
200            for cond in &cage.conditions {
201                // Skip the vector query itself
202                if cond.op == Operator::Fuzzy {
203                    continue;
204                }
205
206                let val = value_to_json(&cond.value)?;
207                let col_str = match &cond.left {
208                    Expr::Named(name) => name.clone(),
209                    expr => {
210                        return Err(format!(
211                            "Qdrant filters require named fields, got expression `{expr}`"
212                        ));
213                    }
214                };
215
216                let clause = match cond.op {
217                    Operator::Eq => format!(
218                        "{{ \"key\": {}, \"match\": {{ \"value\": {} }} }}",
219                        json_string(&col_str),
220                        val
221                    ),
222                    // Qdrant range: { "key": "price", "range": { "gt": 10.0 } }
223                    Operator::Gt => format!(
224                        "{{ \"key\": {}, \"range\": {{ \"gt\": {} }} }}",
225                        json_string(&col_str),
226                        val
227                    ),
228                    Operator::Gte => format!(
229                        "{{ \"key\": {}, \"range\": {{ \"gte\": {} }} }}",
230                        json_string(&col_str),
231                        val
232                    ),
233                    Operator::Lt => format!(
234                        "{{ \"key\": {}, \"range\": {{ \"lt\": {} }} }}",
235                        json_string(&col_str),
236                        val
237                    ),
238                    Operator::Lte => format!(
239                        "{{ \"key\": {}, \"range\": {{ \"lte\": {} }} }}",
240                        json_string(&col_str),
241                        val
242                    ),
243                    Operator::Ne => format!(
244                        "{{ \"must_not\": [{{ \"key\": {}, \"match\": {{ \"value\": {} }} }}] }}",
245                        json_string(&col_str),
246                        val
247                    ), // This needs wrapping?
248                    _ => return Err(format!("unsupported Qdrant filter operator {:?}", cond.op)),
249                };
250                cage_clauses.push(clause);
251            }
252
253            if cage_clauses.is_empty() {
254                continue;
255            }
256
257            match cage.logical_op {
258                LogicalOp::And => musts.extend(cage_clauses),
259                LogicalOp::Or => should_groups.push(cage_clauses),
260            }
261        }
262    }
263
264    for group in should_groups {
265        if group.len() == 1 {
266            musts.push(group[0].clone());
267        } else {
268            musts.push(format!("{{ \"should\": [{}] }}", group.join(", ")));
269        }
270    }
271
272    if musts.is_empty() {
273        return Ok(String::new());
274    }
275
276    let mut parts = Vec::new();
277    if !musts.is_empty() {
278        parts.push(format!("\"must\": [{}]", musts.join(", ")));
279    }
280    Ok(format!("{{ {} }}", parts.join(", ")))
281}
282
283fn get_cage_val(cmd: &Qail, kind_example: CageKind) -> Option<usize> {
284    for cage in &cmd.cages {
285        if let (CageKind::Limit(n), CageKind::Limit(_)) = (&cage.kind, &kind_example) {
286            return Some(*n);
287        }
288    }
289    None
290}
291
292fn value_to_json(v: &Value) -> Result<String, String> {
293    match v {
294        Value::Null | Value::NullUuid => Ok("null".to_string()),
295        Value::String(s) => Ok(json_string(s)),
296        Value::Int(n) => Ok(n.to_string()),
297        Value::Float(n) if n.is_finite() => Ok(n.to_string()),
298        Value::Float(_) => Err("non-finite floats cannot be encoded as Qdrant JSON".to_string()),
299        Value::Bool(b) => Ok(b.to_string()),
300        Value::Uuid(u) => Ok(json_string(&u.to_string())),
301        Value::Timestamp(ts) => Ok(json_string(ts)),
302        Value::Array(arr) => {
303            let elems: Result<Vec<String>, String> = arr.iter().map(value_to_json).collect();
304            Ok(format!("[{}]", elems?.join(", ")))
305        }
306        Value::Vector(values) => {
307            let elems: Result<Vec<String>, String> = values
308                .iter()
309                .map(|value| {
310                    if value.is_finite() {
311                        Ok(value.to_string())
312                    } else {
313                        Err("non-finite vector values cannot be encoded as Qdrant JSON".to_string())
314                    }
315                })
316                .collect();
317            Ok(format!("[{}]", elems?.join(", ")))
318        }
319        Value::Json(json) => serde_json::from_str::<serde_json::Value>(json)
320            .map(|value| value.to_string())
321            .map_err(|err| format!("invalid JSON value for Qdrant payload: {err}")),
322        other => Err(format!("unsupported Qdrant JSON value: {other}")),
323    }
324}
325
326fn vector_to_json(v: &Value) -> Result<String, String> {
327    let elems: Result<Vec<String>, String> = match v {
328        Value::Vector(values) => values
329            .iter()
330            .map(|value| {
331                if value.is_finite() {
332                    Ok(value.to_string())
333                } else {
334                    Err("Qdrant vector values must be finite numbers".to_string())
335                }
336            })
337            .collect(),
338        Value::Array(values) => values
339            .iter()
340            .map(|value| match value {
341                Value::Int(n) => Ok(n.to_string()),
342                Value::Float(n) if n.is_finite() => Ok(n.to_string()),
343                Value::Float(_) => Err("Qdrant vector values must be finite numbers".to_string()),
344                other => Err(format!("Qdrant vector values must be numeric, got {other}")),
345            })
346            .collect(),
347        other => return Err(format!("Qdrant vector must be an array, got {other}")),
348    };
349
350    let elems = elems?;
351    if elems.is_empty() {
352        return Err("Qdrant vector cannot be empty".to_string());
353    }
354    Ok(format!("[{}]", elems.join(", ")))
355}