Skip to main content

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