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`] to match any of the given text tokens.
259    ///
260    /// # Examples:
261    /// ```
262    /// qdrant_client::qdrant::Condition::matches_text_any("tags", "rust python");
263    /// ```
264    pub fn matches_text_any(field: impl Into<String>, query: impl Into<String>) -> Self {
265        Self {
266            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
267                key: field.into(),
268                r#match: Some(qdrant::Match {
269                    match_value: Some(MatchValue::TextAny(query.into())),
270                }),
271                ..Default::default()
272            })),
273        }
274    }
275
276    /// Create a [`Condition`] that checks numeric fields against a range.
277    ///
278    /// # Examples:
279    ///
280    /// ```
281    /// use qdrant_client::qdrant::Range;
282    /// qdrant_client::qdrant::Condition::range("number", Range {
283    ///     gte: Some(42.),
284    ///     ..Default::default()
285    /// });
286    /// ```
287    pub fn range(field: impl Into<String>, range: Range) -> Self {
288        Self {
289            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
290                key: field.into(),
291                range: Some(range),
292                ..Default::default()
293            })),
294        }
295    }
296
297    /// Create a [`Condition`] that checks datetime fields against a range.
298    ///
299    /// # Examples:
300    ///
301    /// ```
302    /// use qdrant_client::qdrant::{DatetimeRange, Timestamp};
303    /// qdrant_client::qdrant::Condition::datetime_range("timestamp", DatetimeRange {
304    ///     gte: Some(Timestamp::date(2023, 2, 8).unwrap()),
305    ///     ..Default::default()
306    /// });
307    /// ```
308    pub fn datetime_range(field: impl Into<String>, range: DatetimeRange) -> Self {
309        Self {
310            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
311                key: field.into(),
312                datetime_range: Some(range),
313                ..Default::default()
314            })),
315        }
316    }
317
318    /// Create a [`Condition`] that checks geo fields against a radius.
319    ///
320    /// # Examples:
321    ///
322    /// ```
323    /// use qdrant_client::qdrant::{GeoPoint, GeoRadius};
324    /// qdrant_client::qdrant::Condition::geo_radius("location", GeoRadius {
325    ///   center: Some(GeoPoint { lon: 42., lat: 42. }),
326    ///   radius: 42.,
327    /// });
328    pub fn geo_radius(field: impl Into<String>, geo_radius: GeoRadius) -> Self {
329        Self {
330            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
331                key: field.into(),
332                geo_radius: Some(geo_radius),
333                ..Default::default()
334            })),
335        }
336    }
337
338    /// Create a [`Condition`] that checks geo fields against a bounding box.
339    ///
340    /// # Examples:
341    ///
342    /// ```
343    /// use qdrant_client::qdrant::{GeoPoint, GeoBoundingBox};
344    /// qdrant_client::qdrant::Condition::geo_bounding_box("location", GeoBoundingBox {
345    ///   top_left: Some(GeoPoint { lon: 42., lat: 42. }),
346    ///   bottom_right: Some(GeoPoint { lon: 42., lat: 42. }),
347    /// });
348    pub fn geo_bounding_box(field: impl Into<String>, geo_bounding_box: GeoBoundingBox) -> Self {
349        Self {
350            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
351                key: field.into(),
352                geo_bounding_box: Some(geo_bounding_box),
353                ..Default::default()
354            })),
355        }
356    }
357
358    /// Create a [`Condition`] that checks geo fields against a geo polygons.
359    ///
360    /// # Examples:
361    ///
362    /// ```
363    /// use qdrant_client::qdrant::{GeoLineString, GeoPoint, GeoPolygon};
364    /// let polygon = GeoPolygon {
365    ///  exterior: Some(GeoLineString { points: vec![GeoPoint { lon: 42., lat: 42. }]}),
366    ///  interiors: vec![],
367    /// };
368    /// qdrant_client::qdrant::Condition::geo_polygon("location", polygon);
369    pub fn geo_polygon(field: impl Into<String>, geo_polygon: GeoPolygon) -> Self {
370        Self {
371            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
372                key: field.into(),
373                geo_polygon: Some(geo_polygon),
374                ..Default::default()
375            })),
376        }
377    }
378
379    /// Create a [`Condition`] that checks count of values in a field.
380    ///
381    /// # Examples:
382    ///
383    /// ```
384    /// use qdrant_client::qdrant::ValuesCount;
385    /// qdrant_client::qdrant::Condition::values_count("tags", ValuesCount {
386    ///  gte: Some(42),
387    ///  ..Default::default()
388    /// });
389    pub fn values_count(field: impl Into<String>, values_count: ValuesCount) -> Self {
390        Self {
391            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
392                key: field.into(),
393                values_count: Some(values_count),
394                ..Default::default()
395            })),
396        }
397    }
398
399    /// Create a [`Condition`] that applies a per-element filter to a nested array
400    ///
401    /// The `field` parameter should be a key-path to a nested array of objects.
402    /// You may specify it as both `array_field` or `array_field[]`.
403    ///
404    /// For motivation and further examples,
405    /// see [API documentation](https://qdrant.tech/documentation/concepts/filtering/#nested-object-filter).
406    ///
407    /// # Panics:
408    ///
409    /// If debug assertions are enabled, this will panic if the filter, or any its subfilters,
410    /// contain a [`HasIdCondition`] (equivalently, a condition created with `Self::has_id`),
411    /// as these are unsupported for nested object filters.
412    ///
413    /// # Examples:
414    ///
415    /// ```
416    /// use qdrant_client::qdrant::Filter;
417    /// qdrant_client::qdrant::Condition::nested("array_field[]", Filter::any([
418    ///   qdrant_client::qdrant::Condition::is_null("element_field")
419    /// ]));
420    pub fn nested(field: impl Into<String>, filter: Filter) -> Self {
421        Self::from(NestedCondition {
422            key: field.into(),
423            filter: Some(filter),
424        })
425    }
426}
427
428impl From<bool> for MatchValue {
429    fn from(value: bool) -> Self {
430        Self::Boolean(value)
431    }
432}
433
434impl From<i64> for MatchValue {
435    fn from(value: i64) -> Self {
436        Self::Integer(value)
437    }
438}
439
440impl From<String> for MatchValue {
441    fn from(value: String) -> Self {
442        if value.contains(char::is_whitespace) {
443            Self::Text(value)
444        } else {
445            Self::Keyword(value)
446        }
447    }
448}
449
450impl From<Vec<i64>> for MatchValue {
451    fn from(integers: Vec<i64>) -> Self {
452        Self::Integers(qdrant::RepeatedIntegers { integers })
453    }
454}
455
456impl From<Vec<String>> for MatchValue {
457    fn from(strings: Vec<String>) -> Self {
458        Self::Keywords(qdrant::RepeatedStrings { strings })
459    }
460}
461
462impl<const N: usize> From<[&str; N]> for MatchValue {
463    fn from(strings: [&str; N]) -> Self {
464        Self::Keywords(qdrant::RepeatedStrings {
465            strings: strings.iter().map(|&s| String::from(s)).collect(),
466        })
467    }
468}
469
470impl std::ops::Not for MatchValue {
471    type Output = Self;
472
473    fn not(self) -> Self::Output {
474        match self {
475            Self::Keyword(s) => Self::ExceptKeywords(qdrant::RepeatedStrings { strings: vec![s] }),
476            Self::Integer(i) => {
477                Self::ExceptIntegers(qdrant::RepeatedIntegers { integers: vec![i] })
478            }
479            Self::Boolean(b) => Self::Boolean(!b),
480            Self::Keywords(ks) => Self::ExceptKeywords(ks),
481            Self::Integers(is) => Self::ExceptIntegers(is),
482            Self::ExceptKeywords(ks) => Self::Keywords(ks),
483            Self::ExceptIntegers(is) => Self::Integers(is),
484            Self::Text(_) => {
485                panic!("cannot negate a MatchValue::Text, use within must_not clause instead")
486            }
487            Self::Phrase(_) => {
488                panic!("cannot negate a MatchValue::Phrase, use within must_not clause instead")
489            }
490            Self::TextAny(_) => {
491                panic!("cannot negate a MatchValue::TextAny, use within must_not clause instead")
492            }
493        }
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use crate::qdrant::{Condition, Filter, NestedCondition};
500
501    #[test]
502    fn test_nested_has_id() {
503        assert!(!Filter::any([]).check_has_id());
504        assert!(Filter::any([Condition::has_id([0])]).check_has_id());
505
506        // nested filter
507        assert!(Filter::any([Filter::any([Condition::has_id([0])]).into()]).check_has_id());
508
509        // nested filter where only the innermost has a `has_id`
510        assert!(
511            Filter::any([Filter::any([Filter::any([Condition::has_id([0])]).into()]).into()])
512                .check_has_id()
513        );
514
515        // `has_id` itself nested in a nested condition
516        assert!(Filter::any([Condition {
517            condition_one_of: Some(crate::qdrant::condition::ConditionOneOf::Nested(
518                NestedCondition {
519                    key: "test".to_string(),
520                    filter: Some(Filter::any([Condition::has_id([0])]))
521                }
522            ))
523        }])
524        .check_has_id());
525    }
526
527    #[test]
528    #[should_panic]
529    fn test_nested_condition_validation() {
530        let _ = Filter::any([Condition::nested(
531            "test",
532            Filter::any([Condition::has_id([0])]),
533        )]);
534    }
535}