Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ git-fetch-with-cli = true
[registries.crates-io]
protocol = "git"

# `ruvector-filter` carries `#![recursion_limit = "4096"]` (PR #389) to
# survive trait-resolution overflow when serde_json's `Serializer` blanket
# impl recurses through the crate's expression types. The deeper resolution
# in turn drives rustc's own process stack past the default 8 MB on x86_64
# Linux, so cargo test/cargo check must run with a larger thread stack.
# 16 MB is rustc's documented suggested value when this happens.
# `ruvector-filter` no longer needs a raised stack: its `FilterExpression`
# logical variants (`And`/`Or`/`Not`) are struct-shaped now, which removed
# the trait-resolution recursion that used to overflow rustc's process
# stack, and `cargo test -p ruvector-filter` builds and runs clean even at
# a 2 MB `RUST_MIN_STACK` (well under the 8 MB default). This setting is
# kept anyway because no workspace-wide measurement has been done to show
# every other crate is fine without it — treat it as still load-bearing
# for the workspace as a whole until that measurement happens.
[env]
RUST_MIN_STACK = "16777216"

Expand Down
12 changes: 6 additions & 6 deletions crates/ruvector-filter/src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ impl<'a> FilterEvaluator<'a> {
top_left,
bottom_right,
} => self.evaluate_geo_bbox(field, *top_left, *bottom_right),
FilterExpression::And(filters) => self.evaluate_and(filters),
FilterExpression::Or(filters) => self.evaluate_or(filters),
FilterExpression::Not(filter) => self.evaluate_not(filter),
FilterExpression::And { exprs } => self.evaluate_and(exprs),
FilterExpression::Or { exprs } => self.evaluate_or(exprs),
FilterExpression::Not { expr } => self.evaluate_not(expr),
FilterExpression::Exists { field } => self.evaluate_exists(field),
FilterExpression::IsNull { field } => self.evaluate_is_null(field),
}
Expand Down Expand Up @@ -103,9 +103,9 @@ impl<'a> FilterEvaluator<'a> {
FilterExpression::Match { field, text } => Self::get_field_value(payload, field)
.and_then(|v| v.as_str())
.is_some_and(|s| s.to_lowercase().contains(&text.to_lowercase())),
FilterExpression::And(filters) => filters.iter().all(|f| self.matches(payload, f)),
FilterExpression::Or(filters) => filters.iter().any(|f| self.matches(payload, f)),
FilterExpression::Not(filter) => !self.matches(payload, filter),
FilterExpression::And { exprs } => exprs.iter().all(|f| self.matches(payload, f)),
FilterExpression::Or { exprs } => exprs.iter().any(|f| self.matches(payload, f)),
FilterExpression::Not { expr } => !self.matches(payload, expr),
FilterExpression::Exists { field } => Self::get_field_value(payload, field).is_some(),
FilterExpression::IsNull { field } => {
Self::get_field_value(payload, field).is_none_or(|v| v.is_null())
Expand Down
59 changes: 50 additions & 9 deletions crates/ruvector-filter/src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,15 @@ pub enum FilterExpression {
},

// Logical operators
And(Vec<FilterExpression>),
Or(Vec<FilterExpression>),
Not(Box<FilterExpression>),
And {
exprs: Vec<FilterExpression>,
},
Or {
exprs: Vec<FilterExpression>,
},
Not {
expr: Box<FilterExpression>,
},

// Existence check
Exists {
Expand Down Expand Up @@ -176,19 +182,21 @@ impl FilterExpression {

/// Create an AND filter
pub fn and(filters: Vec<FilterExpression>) -> Self {
Self::And(filters)
Self::And { exprs: filters }
}

/// Create an OR filter
pub fn or(filters: Vec<FilterExpression>) -> Self {
Self::Or(filters)
Self::Or { exprs: filters }
}

/// Create a NOT filter
// Public API constructor mirrors `and`/`or`; not the `std::ops::Not` trait.
#[allow(clippy::should_implement_trait)]
pub fn not(filter: FilterExpression) -> Self {
Self::Not(Box::new(filter))
Self::Not {
expr: Box::new(filter),
}
}

/// Create an EXISTS filter
Expand Down Expand Up @@ -231,12 +239,12 @@ impl FilterExpression {
| Self::IsNull { field } => {
fields.push(field.clone());
}
Self::And(exprs) | Self::Or(exprs) => {
Self::And { exprs } | Self::Or { exprs } => {
for expr in exprs {
expr.collect_fields(fields);
}
}
Self::Not(expr) => {
Self::Not { expr } => {
expr.collect_fields(fields);
}
}
Expand All @@ -257,7 +265,7 @@ mod tests {
FilterExpression::eq("status", json!("active")),
FilterExpression::gte("age", json!(18)),
]);
assert!(matches!(filter, FilterExpression::And(_)));
assert!(matches!(filter, FilterExpression::And { .. }));
}

#[test]
Expand All @@ -281,4 +289,37 @@ mod tests {
let deserialized: FilterExpression = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, FilterExpression::Eq { .. }));
}

#[test]
fn test_logical_operator_round_trip() {
let filter = FilterExpression::and(vec![
FilterExpression::eq("status", json!("active")),
FilterExpression::not(FilterExpression::or(vec![
FilterExpression::lt("score", json!(10)),
FilterExpression::exists("banned"),
])),
]);

// Exact wire shape, per the `#[serde(tag = "type", rename_all = "snake_case")]`
// enum definition: each variant is `{"type": <snake_case variant name>, ...struct fields}`.
let expected = json!({
"type": "and",
"exprs": [
{"type": "eq", "field": "status", "value": "active"},
{"type": "not", "expr": {
"type": "or",
"exprs": [
{"type": "lt", "field": "score", "value": 10},
{"type": "exists", "field": "banned"},
]
}},
]
});

let actual = serde_json::to_value(&filter).unwrap();
assert_eq!(actual, expected);

let decoded: FilterExpression = serde_json::from_value(actual).unwrap();
assert_eq!(serde_json::to_value(&decoded).unwrap(), expected);
}
}
2 changes: 0 additions & 2 deletions crates/ruvector-filter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![recursion_limit = "4096"]

//! # rUvector Filter
//!
//! Advanced payload indexing and filtering for rUvector.
Expand Down
Loading