ruvector_filter/
expression.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Filter expression for querying vectors by payload
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "type", rename_all = "snake_case")]
7pub enum FilterExpression {
8    // Comparison operators
9    Eq {
10        field: String,
11        value: Value,
12    },
13    Ne {
14        field: String,
15        value: Value,
16    },
17    Gt {
18        field: String,
19        value: Value,
20    },
21    Gte {
22        field: String,
23        value: Value,
24    },
25    Lt {
26        field: String,
27        value: Value,
28    },
29    Lte {
30        field: String,
31        value: Value,
32    },
33
34    // Range
35    Range {
36        field: String,
37        gte: Option<Value>,
38        lte: Option<Value>,
39    },
40
41    // Array operations
42    In {
43        field: String,
44        values: Vec<Value>,
45    },
46
47    // Text matching
48    Match {
49        field: String,
50        text: String,
51    },
52
53    // Geo operations (basic)
54    GeoRadius {
55        field: String,
56        lat: f64,
57        lon: f64,
58        radius_m: f64,
59    },
60    GeoBoundingBox {
61        field: String,
62        top_left: (f64, f64),
63        bottom_right: (f64, f64),
64    },
65
66    // Logical operators
67    And(Vec<FilterExpression>),
68    Or(Vec<FilterExpression>),
69    Not(Box<FilterExpression>),
70
71    // Existence check
72    Exists {
73        field: String,
74    },
75    IsNull {
76        field: String,
77    },
78}
79
80impl FilterExpression {
81    /// Create an equality filter
82    pub fn eq(field: impl Into<String>, value: Value) -> Self {
83        Self::Eq {
84            field: field.into(),
85            value,
86        }
87    }
88
89    /// Create a not-equal filter
90    pub fn ne(field: impl Into<String>, value: Value) -> Self {
91        Self::Ne {
92            field: field.into(),
93            value,
94        }
95    }
96
97    /// Create a greater-than filter
98    pub fn gt(field: impl Into<String>, value: Value) -> Self {
99        Self::Gt {
100            field: field.into(),
101            value,
102        }
103    }
104
105    /// Create a greater-than-or-equal filter
106    pub fn gte(field: impl Into<String>, value: Value) -> Self {
107        Self::Gte {
108            field: field.into(),
109            value,
110        }
111    }
112
113    /// Create a less-than filter
114    pub fn lt(field: impl Into<String>, value: Value) -> Self {
115        Self::Lt {
116            field: field.into(),
117            value,
118        }
119    }
120
121    /// Create a less-than-or-equal filter
122    pub fn lte(field: impl Into<String>, value: Value) -> Self {
123        Self::Lte {
124            field: field.into(),
125            value,
126        }
127    }
128
129    /// Create a range filter
130    pub fn range(field: impl Into<String>, gte: Option<Value>, lte: Option<Value>) -> Self {
131        Self::Range {
132            field: field.into(),
133            gte,
134            lte,
135        }
136    }
137
138    /// Create an IN filter
139    pub fn in_values(field: impl Into<String>, values: Vec<Value>) -> Self {
140        Self::In {
141            field: field.into(),
142            values,
143        }
144    }
145
146    /// Create a text match filter
147    pub fn match_text(field: impl Into<String>, text: impl Into<String>) -> Self {
148        Self::Match {
149            field: field.into(),
150            text: text.into(),
151        }
152    }
153
154    /// Create a geo radius filter
155    pub fn geo_radius(field: impl Into<String>, lat: f64, lon: f64, radius_m: f64) -> Self {
156        Self::GeoRadius {
157            field: field.into(),
158            lat,
159            lon,
160            radius_m,
161        }
162    }
163
164    /// Create a geo bounding box filter
165    pub fn geo_bounding_box(
166        field: impl Into<String>,
167        top_left: (f64, f64),
168        bottom_right: (f64, f64),
169    ) -> Self {
170        Self::GeoBoundingBox {
171            field: field.into(),
172            top_left,
173            bottom_right,
174        }
175    }
176
177    /// Create an AND filter
178    pub fn and(filters: Vec<FilterExpression>) -> Self {
179        Self::And(filters)
180    }
181
182    /// Create an OR filter
183    pub fn or(filters: Vec<FilterExpression>) -> Self {
184        Self::Or(filters)
185    }
186
187    /// Create a NOT filter
188    pub fn not(filter: FilterExpression) -> Self {
189        Self::Not(Box::new(filter))
190    }
191
192    /// Create an EXISTS filter
193    pub fn exists(field: impl Into<String>) -> Self {
194        Self::Exists {
195            field: field.into(),
196        }
197    }
198
199    /// Create an IS NULL filter
200    pub fn is_null(field: impl Into<String>) -> Self {
201        Self::IsNull {
202            field: field.into(),
203        }
204    }
205
206    /// Get all field names referenced in this expression
207    pub fn get_fields(&self) -> Vec<String> {
208        let mut fields = Vec::new();
209        self.collect_fields(&mut fields);
210        fields.sort();
211        fields.dedup();
212        fields
213    }
214
215    fn collect_fields(&self, fields: &mut Vec<String>) {
216        match self {
217            Self::Eq { field, .. }
218            | Self::Ne { field, .. }
219            | Self::Gt { field, .. }
220            | Self::Gte { field, .. }
221            | Self::Lt { field, .. }
222            | Self::Lte { field, .. }
223            | Self::Range { field, .. }
224            | Self::In { field, .. }
225            | Self::Match { field, .. }
226            | Self::GeoRadius { field, .. }
227            | Self::GeoBoundingBox { field, .. }
228            | Self::Exists { field }
229            | Self::IsNull { field } => {
230                fields.push(field.clone());
231            }
232            Self::And(exprs) | Self::Or(exprs) => {
233                for expr in exprs {
234                    expr.collect_fields(fields);
235                }
236            }
237            Self::Not(expr) => {
238                expr.collect_fields(fields);
239            }
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use serde_json::json;
248
249    #[test]
250    fn test_filter_builders() {
251        let filter = FilterExpression::eq("status", json!("active"));
252        assert!(matches!(filter, FilterExpression::Eq { .. }));
253
254        let filter = FilterExpression::and(vec![
255            FilterExpression::eq("status", json!("active")),
256            FilterExpression::gte("age", json!(18)),
257        ]);
258        assert!(matches!(filter, FilterExpression::And(_)));
259    }
260
261    #[test]
262    fn test_get_fields() {
263        let filter = FilterExpression::and(vec![
264            FilterExpression::eq("status", json!("active")),
265            FilterExpression::or(vec![
266                FilterExpression::gte("age", json!(18)),
267                FilterExpression::lt("score", json!(100)),
268            ]),
269        ]);
270
271        let fields = filter.get_fields();
272        assert_eq!(fields, vec!["age", "score", "status"]);
273    }
274
275    #[test]
276    fn test_serialization() {
277        let filter = FilterExpression::eq("status", json!("active"));
278        let json = serde_json::to_string(&filter).unwrap();
279        let deserialized: FilterExpression = serde_json::from_str(&json).unwrap();
280        assert!(matches!(deserialized, FilterExpression::Eq { .. }));
281    }
282}