diff --git a/crates/meilisearch/tests/search/filters.rs b/crates/meilisearch/tests/search/filters.rs index bb268ccf5..375a4ef63 100644 --- a/crates/meilisearch/tests/search/filters.rs +++ b/crates/meilisearch/tests/search/filters.rs @@ -623,3 +623,136 @@ async fn search_with_pattern_filter_settings_scenario_1() { ) .await; } + +#[actix_rt::test] +async fn test_filterable_attributes_priority() { + // Test that the filterable attributes priority is respected + + // check if doggos.name is filterable + test_settings_documents_indexing_swapping_and_search( + &NESTED_DOCUMENTS, + &json!({"filterableAttributes": [ + // deactivated filter + {"patterns": ["doggos.a*"], "features": {"facetSearch": false, "filter": {"equality": false, "comparison": false}}}, + // activated filter + {"patterns": ["doggos.*"]}, + ]}), + &json!({ + "filter": "doggos.name = bobby" + }), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response["hits"]), @r###" + [ + { + "id": 852, + "father": "jean", + "mother": "michelle", + "doggos": [ + { + "name": "bobby", + "age": 2 + }, + { + "name": "buddy", + "age": 4 + } + ], + "cattos": "pésti" + } + ] + "###); + }, + ) + .await; + + // check if doggos.name is filterable 2 + test_settings_documents_indexing_swapping_and_search( + &NESTED_DOCUMENTS, + &json!({"filterableAttributes": [ + // deactivated filter + {"patterns": ["doggos"], "features": {"facetSearch": false, "filter": {"equality": false, "comparison": false}}}, + // activated filter + {"patterns": ["doggos.*"]}, + ]}), + &json!({ + "filter": "doggos.name = bobby" + }), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response["hits"]), @r###" + [ + { + "id": 852, + "father": "jean", + "mother": "michelle", + "doggos": [ + { + "name": "bobby", + "age": 2 + }, + { + "name": "buddy", + "age": 4 + } + ], + "cattos": "pésti" + } + ] + "###); + }, + ) + .await; + + // check if doggos.age is not filterable + test_settings_documents_indexing_swapping_and_search( + &NESTED_DOCUMENTS, + &json!({"filterableAttributes": [ + // deactivated filter + {"patterns": ["doggos.a*"], "features": {"facetSearch": false, "filter": {"equality": false, "comparison": false}}}, + // activated filter + {"patterns": ["doggos.*"]}, + ]}), + &json!({ + "filter": "doggos.age > 2" + }), + |response, code| { + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Index `test`: Attribute `doggos.age` is not filterable. Available filterable attributes are: `doggos.age`, `doggos.name`.\n1:11 doggos.age > 2", + "code": "invalid_search_filter", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" + } + "###); + }, + ) + .await; + + // check if doggos is not filterable + test_settings_documents_indexing_swapping_and_search( + &NESTED_DOCUMENTS, + &json!({"filterableAttributes": [ + // deactivated filter + {"patterns": ["doggos"], "features": {"facetSearch": false, "filter": {"equality": false, "comparison": false}}}, + // activated filter + {"patterns": ["doggos.*"]}, + ]}), + &json!({ + "filter": "doggos EXISTS" + }), + |response, code| { + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Index `test`: Attribute `doggos` is not filterable. Available filterable attributes are: `doggos.age`, `doggos.name`.\n1:7 doggos EXISTS", + "code": "invalid_search_filter", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_filter" + } + "###); + }, + ) + .await; +} diff --git a/crates/milli/src/filterable_attributes_rules.rs b/crates/milli/src/filterable_attributes_rules.rs index 12e27572c..08bccee9b 100644 --- a/crates/milli/src/filterable_attributes_rules.rs +++ b/crates/milli/src/filterable_attributes_rules.rs @@ -342,15 +342,30 @@ fn match_pattern_by_features( filter: &impl Fn(&FilterableAttributesFeatures) -> bool, ) -> PatternMatch { let mut selection = PatternMatch::NoMatch; + + // `can_match` becomes false if the field name matches (PatternMatch::Match) any pattern that is not facet searchable or filterable, + // this ensures that the field doesn't match a pattern with a lower priority, however it can still match a pattern for a nested field as a parent (PatternMatch::Parent). + // See the test `search::filters::test_filterable_attributes_priority` for more details. + let mut can_match = true; + // Check if the field name matches any pattern that is facet searchable or filterable for pattern in filterable_attributes { - let features = pattern.features(); - if filter(&features) { - match pattern.match_str(field_name) { - PatternMatch::Match => return PatternMatch::Match, - PatternMatch::Parent => selection = PatternMatch::Parent, - PatternMatch::NoMatch => (), + match pattern.match_str(field_name) { + PatternMatch::Match => { + let features = pattern.features(); + if filter(&features) && can_match { + return PatternMatch::Match; + } else { + can_match = false; + } } + PatternMatch::Parent => { + let features = pattern.features(); + if filter(&features) { + selection = PatternMatch::Parent; + } + } + PatternMatch::NoMatch => (), } }