use crate::qdrant::condition::ConditionOneOf; use crate::qdrant::points_selector::PointsSelectorOneOf; use crate::qdrant::r#match::MatchValue; use crate::qdrant::{ self, Condition, DatetimeRange, FieldCondition, Filter, GeoBoundingBox, GeoPolygon, GeoRadius, HasIdCondition, IsEmptyCondition, IsNullCondition, MinShould, NestedCondition, PointId, PointsSelector, Range, ValuesCount, }; impl From for PointsSelector { fn from(filter: Filter) -> Self { PointsSelector { points_selector_one_of: Some(PointsSelectorOneOf::Filter(filter)), } } } impl From for Condition { fn from(field_condition: FieldCondition) -> Self { Condition { condition_one_of: Some(ConditionOneOf::Field(field_condition)), } } } impl From for Condition { fn from(is_empty_condition: IsEmptyCondition) -> Self { Condition { condition_one_of: Some(ConditionOneOf::IsEmpty(is_empty_condition)), } } } impl From for Condition { fn from(is_null_condition: IsNullCondition) -> Self { Condition { condition_one_of: Some(ConditionOneOf::IsNull(is_null_condition)), } } } impl From for Condition { fn from(has_id_condition: HasIdCondition) -> Self { Condition { condition_one_of: Some(ConditionOneOf::HasId(has_id_condition)), } } } impl From for Condition { fn from(filter: Filter) -> Self { Condition { condition_one_of: Some(ConditionOneOf::Filter(filter)), } } } impl From for Condition { fn from(nested_condition: NestedCondition) -> Self { debug_assert!( !&nested_condition .filter .as_ref() .map_or(false, |f| f.check_has_id()), "Filters containing a `has_id` condition are not supported for nested filtering." ); Condition { condition_one_of: Some(ConditionOneOf::Nested(nested_condition)), } } } impl qdrant::Filter { /// Checks if the filter, or any of its nested conditions containing filters, /// have a `has_id` condition, which is not allowed for nested object filters. fn check_has_id(&self) -> bool { self.should .iter() .chain(self.must.iter()) .chain(self.must_not.iter()) .any(|cond| match &cond.condition_one_of { Some(ConditionOneOf::HasId(_)) => true, Some(ConditionOneOf::Nested(nested)) => nested .filter .as_ref() .map_or(false, |filter| filter.check_has_id()), Some(ConditionOneOf::Filter(filter)) => filter.check_has_id(), _ => false, }) } /// Create a [`Filter`] where all the conditions must be satisfied. pub fn must(conds: impl IntoIterator) -> Self { Self { must: conds.into_iter().collect(), ..Default::default() } } /// Create a [`Filter`] where at least one of the conditions should be satisfied. pub fn should(conds: impl IntoIterator) -> Self { Self { should: conds.into_iter().collect(), ..Default::default() } } /// Create a [`Filter`] where at least a minimum amount of given conditions should be statisfied. pub fn min_should(min_count: u64, conds: impl IntoIterator) -> Self { Self { min_should: Some(MinShould { min_count, conditions: conds.into_iter().collect(), }), ..Default::default() } } /// Create a [`Filter`] where none of the conditions must be satisfied. pub fn must_not(conds: impl IntoIterator) -> Self { Self { must_not: conds.into_iter().collect(), ..Default::default() } } /// Alias for [`should`](Self::should). /// /// Create a [`Filter`] that matches if any of the conditions match. pub fn any(conds: impl IntoIterator) -> Self { Self::should(conds) } /// Alias for [`must`](Self::must). /// /// Create a [`Filter`] that matches if all of the conditions match. pub fn all(conds: impl IntoIterator) -> Self { Self::must(conds) } /// Alias for [`must_not`](Self::must_not). /// /// Create a [`Filter`] that matches if none of the conditions match. pub fn none(conds: impl IntoIterator) -> Self { Self::must_not(conds) } } impl qdrant::Condition { /// Create a [`Condition`] to check if a field is empty. /// /// # Examples: /// ``` /// qdrant_client::qdrant::Condition::is_empty("field"); /// ``` pub fn is_empty(key: impl Into) -> Self { Self::from(qdrant::IsEmptyCondition { key: key.into() }) } /// Create a [`Condition`] to check if the point has a null key. /// /// # Examples: /// ``` /// qdrant_client::qdrant::Condition::is_empty("remark"); /// ``` pub fn is_null(key: impl Into) -> Self { Self::from(qdrant::IsNullCondition { key: key.into() }) } /// Create a [`Condition`] to check if the point has one of the given ids. /// /// # Examples: /// ``` /// qdrant_client::qdrant::Condition::has_id([0, 8, 15]); /// ``` pub fn has_id(ids: impl IntoIterator>) -> Self { Self::from(qdrant::HasIdCondition { has_id: ids.into_iter().map(Into::into).collect(), }) } /// Create a [`Condition`] that matches a field against a certain value. /// /// # Examples: /// ``` /// qdrant_client::qdrant::Condition::matches("number", 42); /// qdrant_client::qdrant::Condition::matches("tag", vec!["i".to_string(), "em".into()]); /// ``` pub fn matches(field: impl Into, r#match: impl Into) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), r#match: Some(qdrant::Match { match_value: Some(r#match.into()), }), ..Default::default() })), } } /// Create a [`Condition`] to initiate full text match. /// /// # Examples: /// ``` /// qdrant_client::qdrant::Condition::matches_text("description", "good cheap"); /// ``` pub fn matches_text(field: impl Into, query: impl Into) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), r#match: Some(qdrant::Match { match_value: Some(MatchValue::Text(query.into())), }), ..Default::default() })), } } /// Create a [`Condition`] that checks numeric fields against a range. /// /// # Examples: /// /// ``` /// use qdrant_client::qdrant::Range; /// qdrant_client::qdrant::Condition::range("number", Range { /// gte: Some(42.), /// ..Default::default() /// }); /// ``` pub fn range(field: impl Into, range: Range) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), range: Some(range), ..Default::default() })), } } /// Create a [`Condition`] that checks datetime fields against a range. /// /// # Examples: /// /// ``` /// use qdrant_client::qdrant::{DatetimeRange, Timestamp}; /// qdrant_client::qdrant::Condition::datetime_range("timestamp", DatetimeRange { /// gte: Some(Timestamp::date(2023, 2, 8).unwrap()), /// ..Default::default() /// }); /// ``` pub fn datetime_range(field: impl Into, range: DatetimeRange) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), datetime_range: Some(range), ..Default::default() })), } } /// Create a [`Condition`] that checks geo fields against a radius. /// /// # Examples: /// /// ``` /// use qdrant_client::qdrant::{GeoPoint, GeoRadius}; /// qdrant_client::qdrant::Condition::geo_radius("location", GeoRadius { /// center: Some(GeoPoint { lon: 42., lat: 42. }), /// radius: 42., /// }); pub fn geo_radius(field: impl Into, geo_radius: GeoRadius) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), geo_radius: Some(geo_radius), ..Default::default() })), } } /// Create a [`Condition`] that checks geo fields against a bounding box. /// /// # Examples: /// /// ``` /// use qdrant_client::qdrant::{GeoPoint, GeoBoundingBox}; /// qdrant_client::qdrant::Condition::geo_bounding_box("location", GeoBoundingBox { /// top_left: Some(GeoPoint { lon: 42., lat: 42. }), /// bottom_right: Some(GeoPoint { lon: 42., lat: 42. }), /// }); pub fn geo_bounding_box(field: impl Into, geo_bounding_box: GeoBoundingBox) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), geo_bounding_box: Some(geo_bounding_box), ..Default::default() })), } } /// Create a [`Condition`] that checks geo fields against a geo polygons. /// /// # Examples: /// /// ``` /// use qdrant_client::qdrant::{GeoLineString, GeoPoint, GeoPolygon}; /// let polygon = GeoPolygon { /// exterior: Some(GeoLineString { points: vec![GeoPoint { lon: 42., lat: 42. }]}), /// interiors: vec![], /// }; /// qdrant_client::qdrant::Condition::geo_polygon("location", polygon); pub fn geo_polygon(field: impl Into, geo_polygon: GeoPolygon) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), geo_polygon: Some(geo_polygon), ..Default::default() })), } } /// Create a [`Condition`] that checks count of values in a field. /// /// # Examples: /// /// ``` /// use qdrant_client::qdrant::ValuesCount; /// qdrant_client::qdrant::Condition::values_count("tags", ValuesCount { /// gte: Some(42), /// ..Default::default() /// }); pub fn values_count(field: impl Into, values_count: ValuesCount) -> Self { Self { condition_one_of: Some(ConditionOneOf::Field(qdrant::FieldCondition { key: field.into(), values_count: Some(values_count), ..Default::default() })), } } /// Create a [`Condition`] that applies a per-element filter to a nested array /// /// The `field` parameter should be a key-path to a nested array of objects. /// You may specify it as both `array_field` or `array_field[]`. /// /// For motivation and further examples, /// see [API documentation](https://qdrant.tech/documentation/concepts/filtering/#nested-object-filter). /// /// # Panics: /// /// If debug assertions are enabled, this will panic if the filter, or any its subfilters, /// contain a [`HasIdCondition`] (equivalently, a condition created with `Self::has_id`), /// as these are unsupported for nested object filters. /// /// # Examples: /// /// ``` /// use qdrant_client::qdrant::Filter; /// qdrant_client::qdrant::Condition::nested("array_field[]", Filter::any([ /// qdrant_client::qdrant::Condition::is_null("element_field") /// ])); pub fn nested(field: impl Into, filter: Filter) -> Self { Self::from(NestedCondition { key: field.into(), filter: Some(filter), }) } } impl From for MatchValue { fn from(value: bool) -> Self { Self::Boolean(value) } } impl From for MatchValue { fn from(value: i64) -> Self { Self::Integer(value) } } impl From for MatchValue { fn from(value: String) -> Self { if value.contains(char::is_whitespace) { Self::Text(value) } else { Self::Keyword(value) } } } impl From> for MatchValue { fn from(integers: Vec) -> Self { Self::Integers(qdrant::RepeatedIntegers { integers }) } } impl From> for MatchValue { fn from(strings: Vec) -> Self { Self::Keywords(qdrant::RepeatedStrings { strings }) } } impl std::ops::Not for MatchValue { type Output = Self; fn not(self) -> Self::Output { match self { Self::Keyword(s) => Self::ExceptKeywords(qdrant::RepeatedStrings { strings: vec![s] }), Self::Integer(i) => { Self::ExceptIntegers(qdrant::RepeatedIntegers { integers: vec![i] }) } Self::Boolean(b) => Self::Boolean(!b), Self::Keywords(ks) => Self::ExceptKeywords(ks), Self::Integers(is) => Self::ExceptIntegers(is), Self::ExceptKeywords(ks) => Self::Keywords(ks), Self::ExceptIntegers(is) => Self::Integers(is), Self::Text(_) => panic!("cannot negate a MatchValue::Text"), } } } #[cfg(test)] mod tests { use crate::qdrant::{Condition, Filter, NestedCondition}; #[test] fn test_nested_has_id() { assert!(!Filter::any([]).check_has_id()); assert!(Filter::any([Condition::has_id([0])]).check_has_id()); // nested filter assert!(Filter::any([Filter::any([Condition::has_id([0])]).into()]).check_has_id()); // nested filter where only the innermost has a `has_id` assert!( Filter::any([Filter::any([Filter::any([Condition::has_id([0])]).into()]).into()]) .check_has_id() ); // `has_id` itself nested in a nested condition assert!(Filter::any([Condition { condition_one_of: Some(crate::qdrant::condition::ConditionOneOf::Nested( NestedCondition { key: "test".to_string(), filter: Some(Filter::any([Condition::has_id([0])])) } )) }]) .check_has_id()); } #[test] #[should_panic] fn test_nested_condition_validation() { let _ = Filter::any([Condition::nested( "test", Filter::any([Condition::has_id([0])]), )]); } }