qail_core/transpiler/nosql/
qdrant.rs1use crate::ast::*;
2
3fn json_string(value: &str) -> String {
4 serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
5}
6
7pub trait ToQdrant {
9 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 let mut point_id = "0".to_string(); 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 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 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 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 let mut parts = Vec::new();
119
120 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 match &cond.value {
133 Value::String(s) => {
134 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 parts.push("\"vector\": [0.0]".to_string()); }
159
160 let filter = build_filter(cmd)?;
162 if !filter.is_empty() {
163 parts.push(format!("\"filter\": {}", filter));
164 }
165
166 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 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 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 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 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 ), _ => 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}