qdrant_client/
filters.rs

1use crate::qdrant::condition::ConditionOneOf;
2use crate::qdrant::points_selector::PointsSelectorOneOf;
3use crate::qdrant::r#match::MatchValue;
4use crate::qdrant::{
5    self, Condition, DatetimeRange, FieldCondition, Filter, GeoBoundingBox, GeoPolygon, GeoRadius,
6    HasIdCondition, HasVectorCondition, IsEmptyCondition, IsNullCondition, MinShould,
7    NestedCondition, PointId, PointsSelector, Range, ValuesCount,
8};
9
10impl From<Filter> for PointsSelector {
11    fn from(filter: Filter) -> Self {
12        PointsSelector {
13            points_selector_one_of: Some(PointsSelectorOneOf::Filter(filter)),
14        }
15    }
16}
17
18impl From<FieldCondition> for Condition {
19    fn from(field_condition: FieldCondition) -> Self {
20        Condition {
21            condition_one_of: Some(ConditionOneOf::Field(field_condition)),
22        }
23    }
24}
25
26impl From<IsEmptyCondition> for Condition {
27    fn from(is_empty_condition: IsEmptyCondition) -> Self {
28        Condition {
29            condition_one_of: Some(ConditionOneOf::IsEmpty(is_empty_condition)),
30        }
31    }
32}
33
34impl From<IsNullCondition> for Condition {
35    fn from(is_null_condition: IsNullCondition) -> Self {
36        Condition {
37            condition_one_of: Some(ConditionOneOf::IsNull(is_null_condition)),
38        }
39    }
40}
41
42impl From<HasIdCondition> for Condition {
43    fn from(has_id_condition: HasIdCondition) -> Self {
44        Condition {
45            condition_one_of: Some(ConditionOneOf::HasId(has_id_condition)),
46        }
47    }
48}
49
50impl From<HasVectorCondition> for Condition {
51    fn from(has_vector_condition: HasVectorCondition) -> Self {
52        Condition {
53            condition_one_of: Some(ConditionOneOf::HasVector(has_vector_condition)),
54        }
55    }
56}
57
58impl From<Filter> for Condition {
59    fn from(filter: Filter) -> Self {
60        Condition {
61            condition_one_of: Some(ConditionOneOf::Filter(filter)),
62        }
63    }
64}
65
66impl From<NestedCondition> for Condition {
67    fn from(nested_condition: NestedCondition) -> Self {
68        debug_assert!(
69            !&nested_condition
70                .filter
71                .as_ref()
72                .is_some_and(|f| f.check_has_id()),
73            "Filters containing a `has_id` condition are not supported for nested filtering."
74        );
75
76        Condition {
77            condition_one_of: Some(ConditionOneOf::Nested(nested_condition)),
78        }
79    }
80}
81
82impl qdrant::Filter {
83    /// Checks if the filter, or any of its nested conditions containing filters,
84    /// have a `has_id` condition, which is not allowed for nested object filters.
85    fn check_has_id(&self) -> bool {
86        self.should
87            .iter()
88            .chain(self.must.iter())
89            .chain(self.must_not.iter())
90            .any(|cond| match &cond.condition_one_of {
91                Some(ConditionOneOf::HasId(_)) => true,
92                Some(ConditionOneOf::Nested(nested)) => nested
93                    .filter
94                    .as_ref()
95                    .is_some_and(|filter| filter.check_has_id()),
96                Some(ConditionOneOf::Filter(filter)) => filter.check_has_id(),
97                _ => false,
98            })
99    }
100
101    /// Create a [`Filter`] where all the conditions must be satisfied.
102    pub fn must(conds: impl IntoIterator<Item = qdrant::Condition>) -> Self {
103        Self {
104            must: conds.into_iter().collect(),
105            ..Default::default()
106        }
107    }
108
109    /// Create a [`Filter`] where at least one of the conditions should be satisfied.
110    pub fn should(conds: impl IntoIterator<Item = qdrant::Condition>) -> Self {
111        Self {
112            should: conds.into_iter().collect(),
113            ..Default::default()
114        }
115    }
116
117    /// Create a [`Filter`] where at least a minimum amount of given conditions should be statisfied.
118    pub fn min_should(min_count: u64, conds: impl IntoIterator<Item = qdrant::Condition>) -> Self {
119        Self {
120            min_should: Some(MinShould {
121                min_count,
122                conditions: conds.into_iter().collect(),
123            }),
124            ..Default::default()
125        }
126    }
127
128    /// Create a [`Filter`] where none of the conditions must be satisfied.
129    pub fn must_not(conds: impl IntoIterator<Item = qdrant::Condition>) -> Self {
130        Self {
131            must_not: conds.into_iter().collect(),
132            ..Default::default()
133        }
134    }
135
136    /// Alias for [`should`](Self::should).
137    ///
138    /// Create a [`Filter`] that matches if any of the conditions match.
139    pub fn any(conds: impl IntoIterator<Item = qdrant::Condition>) -> Self {
140        Self::should(conds)
141    }
142
143    /// Alias for [`must`](Self::must).
144    ///
145    /// Create a [`Filter`] that matches if all of the conditions match.
146    pub fn all(conds: impl IntoIterator<Item = qdrant::Condition>) -> Self {
147        Self::must(conds)
148    }
149
150    /// Alias for [`must_not`](Self::must_not).
151    ///
152    /// Create a [`Filter`] that matches if none of the conditions match.
153    pub fn none(conds: impl IntoIterator<Item = qdrant::Condition>) -> Self {
154        Self::must_not(conds)
155    }
156}
157
158impl qdrant::Condition {
159    /// Create a [`Condition`] to check if a field is empty.
160    ///
161    /// # Examples:
162    /// ```
163    /// qdrant_client::qdrant::Condition::is_empty("field");
164    /// ```
165    pub fn is_empty(key: impl Into<String>) -> Self {
166        Self::from(qdrant::IsEmptyCondition { key: key.into() })
167    }
168
169    /// Create a [`Condition`] to check if the point has a null key.
170    ///
171    /// # Examples:
172    /// ```
173    /// qdrant_client::qdrant::Condition::is_empty("remark");
174    /// ```
175    pub fn is_null(key: impl Into<String>) -> Self {
176        Self::from(qdrant::IsNullCondition { key: key.into() })
177    }
178
179    /// Create a [`Condition`] to check if the point has one of the given ids.
180    ///
181    /// # Examples:
182    /// ```
183    /// qdrant_client::qdrant::Condition::has_id([0, 8, 15]);
184    /// ```
185    pub fn has_id(ids: impl IntoIterator<Item = impl Into<PointId>>) -> Self {
186        Self::from(qdrant::HasIdCondition {
187            has_id: ids.into_iter().map(Into::into).collect(),
188        })
189    }
190
191    /// Create a [`Condition`] to check if the point has a specific named vector.
192    ///
193    /// # Examples:
194    /// ```
195    /// qdrant_client::qdrant::Condition::has_vector("my_vector");
196    /// ```
197    pub fn has_vector(vector_name: impl Into<String>) -> Self {
198        Self::from(qdrant::HasVectorCondition {
199            has_vector: vector_name.into(),
200        })
201    }
202
203    /// Create a [`Condition`] that matches a field against a certain value.
204    ///
205    /// # Examples:
206    /// ```
207    /// qdrant_client::qdrant::Condition::matches("number", 42);
208    /// qdrant_client::qdrant::Condition::matches("tag", vec!["i".to_string(), "em".into()]);
209    /// ```
210    pub fn matches(field: impl Into<String>, r#match: impl Into<MatchValue>) -> Self {
211        Self {
212            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
213                key: field.into(),
214                r#match: Some(qdrant::Match {
215                    match_value: Some(r#match.into()),
216                }),
217                ..Default::default()
218            })),
219        }
220    }
221
222    /// Create a [`Condition`] to initiate full text match.
223    ///
224    /// # Examples:
225    /// ```
226    /// qdrant_client::qdrant::Condition::matches_text("description", "good cheap");
227    /// ```
228    pub fn matches_text(field: impl Into<String>, query: impl Into<String>) -> Self {
229        Self {
230            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
231                key: field.into(),
232                r#match: Some(qdrant::Match {
233                    match_value: Some(MatchValue::Text(query.into())),
234                }),
235                ..Default::default()
236            })),
237        }
238    }
239
240    /// Create a [`Condition`] to initiate full text phrase match.
241    ///
242    /// # Examples:
243    /// ```
244    /// qdrant_client::qdrant::Condition::matches_phrase("description", "time machine");
245    /// ```
246    pub fn matches_phrase(field: impl Into<String>, query: impl Into<String>) -> Self {
247        Self {
248            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
249                key: field.into(),
250                r#match: Some(qdrant::Match {
251                    match_value: Some(MatchValue::Phrase(query.into())),
252                }),
253                ..Default::default()
254            })),
255        }
256    }
257
258    /// Create a [`Condition`] that checks numeric fields against a range.
259    ///
260    /// # Examples:
261    ///
262    /// ```
263    /// use qdrant_client::qdrant::Range;
264    /// qdrant_client::qdrant::Condition::range("number", Range {
265    ///     gte: Some(42.),
266    ///     ..Default::default()
267    /// });
268    /// ```
269    pub fn range(field: impl Into<String>, range: Range) -> Self {
270        Self {
271            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
272                key: field.into(),
273                range: Some(range),
274                ..Default::default()
275            })),
276        }
277    }
278
279    /// Create a [`Condition`] that checks datetime fields against a range.
280    ///
281    /// # Examples:
282    ///
283    /// ```
284    /// use qdrant_client::qdrant::{DatetimeRange, Timestamp};
285    /// qdrant_client::qdrant::Condition::datetime_range("timestamp", DatetimeRange {
286    ///     gte: Some(Timestamp::date(2023, 2, 8).unwrap()),
287    ///     ..Default::default()
288    /// });
289    /// ```
290    pub fn datetime_range(field: impl Into<String>, range: DatetimeRange) -> Self {
291        Self {
292            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
293                key: field.into(),
294                datetime_range: Some(range),
295                ..Default::default()
296            })),
297        }
298    }
299
300    /// Create a [`Condition`] that checks geo fields against a radius.
301    ///
302    /// # Examples:
303    ///
304    /// ```
305    /// use qdrant_client::qdrant::{GeoPoint, GeoRadius};
306    /// qdrant_client::qdrant::Condition::geo_radius("location", GeoRadius {
307    ///   center: Some(GeoPoint { lon: 42., lat: 42. }),
308    ///   radius: 42.,
309    /// });
310    pub fn geo_radius(field: impl Into<String>, geo_radius: GeoRadius) -> Self {
311        Self {
312            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
313                key: field.into(),
314                geo_radius: Some(geo_radius),
315                ..Default::default()
316            })),
317        }
318    }
319
320    /// Create a [`Condition`] that checks geo fields against a bounding box.
321    ///
322    /// # Examples:
323    ///
324    /// ```
325    /// use qdrant_client::qdrant::{GeoPoint, GeoBoundingBox};
326    /// qdrant_client::qdrant::Condition::geo_bounding_box("location", GeoBoundingBox {
327    ///   top_left: Some(GeoPoint { lon: 42., lat: 42. }),
328    ///   bottom_right: Some(GeoPoint { lon: 42., lat: 42. }),
329    /// });
330    pub fn geo_bounding_box(field: impl Into<String>, geo_bounding_box: GeoBoundingBox) -> Self {
331        Self {
332            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
333                key: field.into(),
334                geo_bounding_box: Some(geo_bounding_box),
335                ..Default::default()
336            })),
337        }
338    }
339
340    /// Create a [`Condition`] that checks geo fields against a geo polygons.
341    ///
342    /// # Examples:
343    ///
344    /// ```
345    /// use qdrant_client::qdrant::{GeoLineString, GeoPoint, GeoPolygon};
346    /// let polygon = GeoPolygon {
347    ///  exterior: Some(GeoLineString { points: vec![GeoPoint { lon: 42., lat: 42. }]}),
348    ///  interiors: vec![],
349    /// };
350    /// qdrant_client::qdrant::Condition::geo_polygon("location", polygon);
351    pub fn geo_polygon(field: impl Into<String>, geo_polygon: GeoPolygon) -> Self {
352        Self {
353            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
354                key: field.into(),
355                geo_polygon: Some(geo_polygon),
356                ..Default::default()
357            })),
358        }
359    }
360
361    /// Create a [`Condition`] that checks count of values in a field.
362    ///
363    /// # Examples:
364    ///
365    /// ```
366    /// use qdrant_client::qdrant::ValuesCount;
367    /// qdrant_client::qdrant::Condition::values_count("tags", ValuesCount {
368    ///  gte: Some(42),
369    ///  ..Default::default()
370    /// });
371    pub fn values_count(field: impl Into<String>, values_count: ValuesCount) -> Self {
372        Self {
373            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
374                key: field.into(),
375                values_count: Some(values_count),
376                ..Default::default()
377            })),
378        }
379    }
380
381    /// Create a [`Condition`] that applies a per-element filter to a nested array
382    ///
383    /// The `field` parameter should be a key-path to a nested array of objects.
384    /// You may specify it as both `array_field` or `array_field[]`.
385    ///
386    /// For motivation and further examples,
387    /// see [API documentation](https://qdrant.tech/documentation/concepts/filtering/#nested-object-filter).
388    ///
389    /// # Panics:
390    ///
391    /// If debug assertions are enabled, this will panic if the filter, or any its subfilters,
392    /// contain a [`HasIdCondition`] (equivalently, a condition created with `Self::has_id`),
393    /// as these are unsupported for nested object filters.
394    ///
395    /// # Examples:
396    ///
397    /// ```
398    /// use qdrant_client::qdrant::Filter;
399    /// qdrant_client::qdrant::Condition::nested("array_field[]", Filter::any([
400    ///   qdrant_client::qdrant::Condition::is_null("element_field")
401    /// ]));
402    pub fn nested(field: impl Into<String>, filter: Filter) -> Self {
403        Self::from(NestedCondition {
404            key: field.into(),
405            filter: Some(filter),
406        })
407    }
408}
409
410impl From<bool> for MatchValue {
411    fn from(value: bool) -> Self {
412        Self::Boolean(value)
413    }
414}
415
416impl From<i64> for MatchValue {
417    fn from(value: i64) -> Self {
418        Self::Integer(value)
419    }
420}
421
422impl From<String> for MatchValue {
423    fn from(value: String) -> Self {
424        if value.contains(char::is_whitespace) {
425            Self::Text(value)
426        } else {
427            Self::Keyword(value)
428        }
429    }
430}
431
432impl From<Vec<i64>> for MatchValue {
433    fn from(integers: Vec<i64>) -> Self {
434        Self::Integers(qdrant::RepeatedIntegers { integers })
435    }
436}
437
438impl From<Vec<String>> for MatchValue {
439    fn from(strings: Vec<String>) -> Self {
440        Self::Keywords(qdrant::RepeatedStrings { strings })
441    }
442}
443
444impl<const N: usize> From<[&str; N]> for MatchValue {
445    fn from(strings: [&str; N]) -> Self {
446        Self::Keywords(qdrant::RepeatedStrings {
447            strings: strings.iter().map(|&s| String::from(s)).collect(),
448        })
449    }
450}
451
452impl std::ops::Not for MatchValue {
453    type Output = Self;
454
455    fn not(self) -> Self::Output {
456        match self {
457            Self::Keyword(s) => Self::ExceptKeywords(qdrant::RepeatedStrings { strings: vec![s] }),
458            Self::Integer(i) => {
459                Self::ExceptIntegers(qdrant::RepeatedIntegers { integers: vec![i] })
460            }
461            Self::Boolean(b) => Self::Boolean(!b),
462            Self::Keywords(ks) => Self::ExceptKeywords(ks),
463            Self::Integers(is) => Self::ExceptIntegers(is),
464            Self::ExceptKeywords(ks) => Self::Keywords(ks),
465            Self::ExceptIntegers(is) => Self::Integers(is),
466            Self::Text(_) => {
467                panic!("cannot negate a MatchValue::Text, use within must_not clause instead")
468            }
469            Self::Phrase(_) => {
470                panic!("cannot negate a MatchValue::Phrase, use within must_not clause instead")
471            }
472        }
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use crate::qdrant::{Condition, Filter, NestedCondition};
479
480    #[test]
481    fn test_nested_has_id() {
482        assert!(!Filter::any([]).check_has_id());
483        assert!(Filter::any([Condition::has_id([0])]).check_has_id());
484
485        // nested filter
486        assert!(Filter::any([Filter::any([Condition::has_id([0])]).into()]).check_has_id());
487
488        // nested filter where only the innermost has a `has_id`
489        assert!(
490            Filter::any([Filter::any([Filter::any([Condition::has_id([0])]).into()]).into()])
491                .check_has_id()
492        );
493
494        // `has_id` itself nested in a nested condition
495        assert!(Filter::any([Condition {
496            condition_one_of: Some(crate::qdrant::condition::ConditionOneOf::Nested(
497                NestedCondition {
498                    key: "test".to_string(),
499                    filter: Some(Filter::any([Condition::has_id([0])]))
500                }
501            ))
502        }])
503        .check_has_id());
504    }
505
506    #[test]
507    #[should_panic]
508    fn test_nested_condition_validation() {
509        let _ = Filter::any([Condition::nested(
510            "test",
511            Filter::any([Condition::has_id([0])]),
512        )]);
513    }
514}