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`] to match keywords containing the given substring.
306    ///
307    /// # Examples:
308    /// ```
309    /// qdrant_client::qdrant::Condition::matches_substring("city", "erl");
310    /// ```
311    pub fn matches_substring(field: impl Into<String>, substring: impl Into<String>) -> Self {
312        Self {
313            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
314                key: field.into(),
315                r#match: Some(qdrant::Match {
316                    match_value: Some(MatchValue::Substring(substring.into())),
317                }),
318                ..Default::default()
319            })),
320        }
321    }
322
323    /// Create a [`Condition`] selecting one of `total` disjoint deterministic slices of the id
324    /// space. Useful to split a collection into equally sized chunks, for example to scroll
325    /// through it in parallel.
326    ///
327    /// `index` must be less than `total`.
328    ///
329    /// # Examples:
330    /// ```
331    /// // Select the first of four slices of the collection
332    /// qdrant_client::qdrant::Condition::slice(4, 0);
333    /// ```
334    pub fn slice(total: u32, index: u32) -> Self {
335        debug_assert!(total >= 1, "`total` must be at least 1");
336        debug_assert!(index < total, "`index` must be less than `total`");
337
338        Self::from(SliceCondition { total, index })
339    }
340
341    /// Create a [`Condition`] that checks numeric fields against a range.
342    ///
343    /// # Examples:
344    ///
345    /// ```
346    /// use qdrant_client::qdrant::Range;
347    /// qdrant_client::qdrant::Condition::range("number", Range {
348    ///     gte: Some(42.),
349    ///     ..Default::default()
350    /// });
351    /// ```
352    pub fn range(field: impl Into<String>, range: Range) -> Self {
353        Self {
354            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
355                key: field.into(),
356                range: Some(range),
357                ..Default::default()
358            })),
359        }
360    }
361
362    /// Create a [`Condition`] that checks datetime fields against a range.
363    ///
364    /// # Examples:
365    ///
366    /// ```
367    /// use qdrant_client::qdrant::{DatetimeRange, Timestamp};
368    /// qdrant_client::qdrant::Condition::datetime_range("timestamp", DatetimeRange {
369    ///     gte: Some(Timestamp::date(2023, 2, 8).unwrap()),
370    ///     ..Default::default()
371    /// });
372    /// ```
373    pub fn datetime_range(field: impl Into<String>, range: DatetimeRange) -> Self {
374        Self {
375            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
376                key: field.into(),
377                datetime_range: Some(range),
378                ..Default::default()
379            })),
380        }
381    }
382
383    /// Create a [`Condition`] that checks geo fields against a radius.
384    ///
385    /// # Examples:
386    ///
387    /// ```
388    /// use qdrant_client::qdrant::{GeoPoint, GeoRadius};
389    /// qdrant_client::qdrant::Condition::geo_radius("location", GeoRadius {
390    ///   center: Some(GeoPoint { lon: 42., lat: 42. }),
391    ///   radius: 42.,
392    /// });
393    pub fn geo_radius(field: impl Into<String>, geo_radius: GeoRadius) -> Self {
394        Self {
395            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
396                key: field.into(),
397                geo_radius: Some(geo_radius),
398                ..Default::default()
399            })),
400        }
401    }
402
403    /// Create a [`Condition`] that checks geo fields against a bounding box.
404    ///
405    /// # Examples:
406    ///
407    /// ```
408    /// use qdrant_client::qdrant::{GeoPoint, GeoBoundingBox};
409    /// qdrant_client::qdrant::Condition::geo_bounding_box("location", GeoBoundingBox {
410    ///   top_left: Some(GeoPoint { lon: 42., lat: 42. }),
411    ///   bottom_right: Some(GeoPoint { lon: 42., lat: 42. }),
412    /// });
413    pub fn geo_bounding_box(field: impl Into<String>, geo_bounding_box: GeoBoundingBox) -> Self {
414        Self {
415            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
416                key: field.into(),
417                geo_bounding_box: Some(geo_bounding_box),
418                ..Default::default()
419            })),
420        }
421    }
422
423    /// Create a [`Condition`] that checks geo fields against a geo polygons.
424    ///
425    /// # Examples:
426    ///
427    /// ```
428    /// use qdrant_client::qdrant::{GeoLineString, GeoPoint, GeoPolygon};
429    /// let polygon = GeoPolygon {
430    ///  exterior: Some(GeoLineString { points: vec![GeoPoint { lon: 42., lat: 42. }]}),
431    ///  interiors: vec![],
432    /// };
433    /// qdrant_client::qdrant::Condition::geo_polygon("location", polygon);
434    pub fn geo_polygon(field: impl Into<String>, geo_polygon: GeoPolygon) -> Self {
435        Self {
436            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
437                key: field.into(),
438                geo_polygon: Some(geo_polygon),
439                ..Default::default()
440            })),
441        }
442    }
443
444    /// Create a [`Condition`] that checks count of values in a field.
445    ///
446    /// # Examples:
447    ///
448    /// ```
449    /// use qdrant_client::qdrant::ValuesCount;
450    /// qdrant_client::qdrant::Condition::values_count("tags", ValuesCount {
451    ///  gte: Some(42),
452    ///  ..Default::default()
453    /// });
454    pub fn values_count(field: impl Into<String>, values_count: ValuesCount) -> Self {
455        Self {
456            condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition {
457                key: field.into(),
458                values_count: Some(values_count),
459                ..Default::default()
460            })),
461        }
462    }
463
464    /// Create a [`Condition`] that applies a per-element filter to a nested array
465    ///
466    /// The `field` parameter should be a key-path to a nested array of objects.
467    /// You may specify it as both `array_field` or `array_field[]`.
468    ///
469    /// For motivation and further examples,
470    /// see [API documentation](https://qdrant.tech/documentation/concepts/filtering/#nested-object-filter).
471    ///
472    /// # Panics:
473    ///
474    /// If debug assertions are enabled, this will panic if the filter, or any its subfilters,
475    /// contain a [`HasIdCondition`] (equivalently, a condition created with `Self::has_id`),
476    /// as these are unsupported for nested object filters.
477    ///
478    /// # Examples:
479    ///
480    /// ```
481    /// use qdrant_client::qdrant::Filter;
482    /// qdrant_client::qdrant::Condition::nested("array_field[]", Filter::any([
483    ///   qdrant_client::qdrant::Condition::is_null("element_field")
484    /// ]));
485    pub fn nested(field: impl Into<String>, filter: Filter) -> Self {
486        Self::from(NestedCondition {
487            key: field.into(),
488            filter: Some(filter),
489        })
490    }
491}
492
493impl From<bool> for MatchValue {
494    fn from(value: bool) -> Self {
495        Self::Boolean(value)
496    }
497}
498
499impl From<i64> for MatchValue {
500    fn from(value: i64) -> Self {
501        Self::Integer(value)
502    }
503}
504
505impl From<String> for MatchValue {
506    fn from(value: String) -> Self {
507        if value.contains(char::is_whitespace) {
508            Self::Text(value)
509        } else {
510            Self::Keyword(value)
511        }
512    }
513}
514
515impl From<Vec<i64>> for MatchValue {
516    fn from(integers: Vec<i64>) -> Self {
517        Self::Integers(qdrant::RepeatedIntegers { integers })
518    }
519}
520
521impl From<Vec<String>> for MatchValue {
522    fn from(strings: Vec<String>) -> Self {
523        Self::Keywords(qdrant::RepeatedStrings { strings })
524    }
525}
526
527impl<const N: usize> From<[&str; N]> for MatchValue {
528    fn from(strings: [&str; N]) -> Self {
529        Self::Keywords(qdrant::RepeatedStrings {
530            strings: strings.iter().map(|&s| String::from(s)).collect(),
531        })
532    }
533}
534
535impl std::ops::Not for MatchValue {
536    type Output = Self;
537
538    fn not(self) -> Self::Output {
539        match self {
540            Self::Keyword(s) => Self::ExceptKeywords(qdrant::RepeatedStrings { strings: vec![s] }),
541            Self::Integer(i) => {
542                Self::ExceptIntegers(qdrant::RepeatedIntegers { integers: vec![i] })
543            }
544            Self::Boolean(b) => Self::Boolean(!b),
545            Self::Keywords(ks) => Self::ExceptKeywords(ks),
546            Self::Integers(is) => Self::ExceptIntegers(is),
547            Self::ExceptKeywords(ks) => Self::Keywords(ks),
548            Self::ExceptIntegers(is) => Self::Integers(is),
549            Self::Text(_) => {
550                panic!("cannot negate a MatchValue::Text, use within must_not clause instead")
551            }
552            Self::Phrase(_) => {
553                panic!("cannot negate a MatchValue::Phrase, use within must_not clause instead")
554            }
555            Self::TextAny(_) => {
556                panic!("cannot negate a MatchValue::TextAny, use within must_not clause instead")
557            }
558            Self::Prefix(_) => {
559                panic!("cannot negate a MatchValue::Prefix, use within must_not clause instead")
560            }
561            Self::Substring(_) => {
562                panic!("cannot negate a MatchValue::Substring, use within must_not clause instead")
563            }
564        }
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use crate::qdrant::{Condition, Filter, NestedCondition};
571
572    #[test]
573    fn test_nested_has_id() {
574        assert!(!Filter::any([]).check_has_id());
575        assert!(Filter::any([Condition::has_id([0])]).check_has_id());
576
577        // nested filter
578        assert!(Filter::any([Filter::any([Condition::has_id([0])]).into()]).check_has_id());
579
580        // nested filter where only the innermost has a `has_id`
581        assert!(
582            Filter::any([Filter::any([Filter::any([Condition::has_id([0])]).into()]).into()])
583                .check_has_id()
584        );
585
586        // `has_id` itself nested in a nested condition
587        assert!(Filter::any([Condition {
588            condition_one_of: Some(crate::qdrant::condition::ConditionOneOf::Nested(
589                NestedCondition {
590                    key: "test".to_string(),
591                    filter: Some(Filter::any([Condition::has_id([0])]))
592                }
593            ))
594        }])
595        .check_has_id());
596    }
597
598    #[test]
599    #[should_panic]
600    fn test_nested_condition_validation() {
601        let _ = Filter::any([Condition::nested(
602            "test",
603            Filter::any([Condition::has_id([0])]),
604        )]);
605    }
606}