diff --git a/crates/meilisearch/tests/search/errors.rs b/crates/meilisearch/tests/search/errors.rs index 46a03e56f..8561aa490 100644 --- a/crates/meilisearch/tests/search/errors.rs +++ b/crates/meilisearch/tests/search/errors.rs @@ -434,7 +434,7 @@ async fn search_non_filterable_facets() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute is `title`.", + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute pattern is `title`.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" @@ -445,7 +445,7 @@ async fn search_non_filterable_facets() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute is `title`.", + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute pattern is `title`.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" @@ -468,7 +468,7 @@ async fn search_non_filterable_facets_multiple_filterable() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attributes are `genres, title`.", + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute patterns are `genres, title`.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" @@ -479,7 +479,7 @@ async fn search_non_filterable_facets_multiple_filterable() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attributes are `genres, title`.", + "message": "Invalid facet distribution, attribute `doggo` is not filterable. The available filterable attribute patterns are `genres, title`.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" @@ -532,7 +532,7 @@ async fn search_non_filterable_facets_multiple_facets() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid facet distribution, attributes `doggo, neko` are not filterable. The available filterable attributes are `genres, title`.", + "message": "Invalid facet distribution, attributes `doggo, neko` are not filterable. The available filterable attribute patterns are `genres, title`.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" @@ -543,7 +543,7 @@ async fn search_non_filterable_facets_multiple_facets() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid facet distribution, attributes `doggo, neko` are not filterable. The available filterable attributes are `genres, title`.", + "message": "Invalid facet distribution, attributes `doggo, neko` are not filterable. The available filterable attribute patterns are `genres, title`.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" @@ -1204,52 +1204,55 @@ async fn search_on_unknown_field_plus_joker() { #[actix_rt::test] async fn distinct_at_search_time() { - let server = Server::new_shared(); - let index = server.unique_index(); + let server = Server::new().await; + let index = server.index("test"); let (task, _) = index.create(None).await; index.wait_task(task.uid()).await.succeeded(); let (response, _code) = index.add_documents(json!([{"id": 1, "color": "Doggo", "machin": "Action"}]), None).await; index.wait_task(response.uid()).await.succeeded(); - let expected_response = json!({ - "message": format!("Index `{}`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. This index does not have configured filterable attributes.", index.uid), - "code": "invalid_search_distinct", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_search_distinct" - }); let (response, code) = index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await; - assert_eq!(response, expected_response); - assert_eq!(code, 400); + snapshot!(code, @"400 Bad Request"); + snapshot!(response, @r###" + { + "message": "Index `test`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. This index does not have configured filterable attributes.", + "code": "invalid_search_distinct", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_distinct" + } + "###); let (task, _) = index.update_settings_filterable_attributes(json!(["color", "machin"])).await; index.wait_task(task.uid()).await.succeeded(); - let expected_response = json!({ - "message": format!("Index `{}`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. Available filterable attributes are: `color, machin`.", index.uid), - "code": "invalid_search_distinct", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_search_distinct" - }); let (response, code) = index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await; - assert_eq!(response, expected_response); - assert_eq!(code, 400); + snapshot!(code, @"400 Bad Request"); + snapshot!(response, @r###" + { + "message": "Index `test`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. Available filterable attributes patterns are: `color, machin`.", + "code": "invalid_search_distinct", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_distinct" + } + "###); let (task, _) = index.update_settings_displayed_attributes(json!(["color"])).await; index.wait_task(task.uid()).await.succeeded(); - let expected_response = json!({ - "message": format!("Index `{}`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. Available filterable attributes are: `color, <..hidden-attributes>`.", index.uid), - "code": "invalid_search_distinct", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_search_distinct" - }); let (response, code) = index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await; - assert_eq!(response, expected_response); - assert_eq!(code, 400); + snapshot!(code, @"400 Bad Request"); + snapshot!(response, @r###" + { + "message": "Index `test`: Attribute `doggo.truc` is not filterable and thus, cannot be used as distinct attribute. Available filterable attributes patterns are: `color, <..hidden-attributes>`.", + "code": "invalid_search_distinct", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_search_distinct" + } + "###); let (response, code) = index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": true})).await; diff --git a/crates/meilisearch/tests/search/multi/mod.rs b/crates/meilisearch/tests/search/multi/mod.rs index e5c58268d..be0142c2d 100644 --- a/crates/meilisearch/tests/search/multi/mod.rs +++ b/crates/meilisearch/tests/search/multi/mod.rs @@ -3653,7 +3653,7 @@ async fn federation_non_faceted_for_an_index() { snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { - "message": "Inside `.federation.facetsByIndex.fruits-no-name`: Invalid facet distribution, attribute `name` is not filterable. The available filterable attributes are `BOOST, id`.\n - Note: index `fruits-no-name` used in `.queries[1]`", + "message": "Inside `.federation.facetsByIndex.fruits-no-name`: Invalid facet distribution, attribute `name` is not filterable. The available filterable attribute patterns are `BOOST, id`.\n - Note: index `fruits-no-name` used in `.queries[1]`", "code": "invalid_multi_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets" @@ -3675,7 +3675,7 @@ async fn federation_non_faceted_for_an_index() { snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { - "message": "Inside `.federation.facetsByIndex.fruits-no-name`: Invalid facet distribution, attribute `name` is not filterable. The available filterable attributes are `BOOST, id`.\n - Note: index `fruits-no-name` is not used in queries", + "message": "Inside `.federation.facetsByIndex.fruits-no-name`: Invalid facet distribution, attribute `name` is not filterable. The available filterable attribute patterns are `BOOST, id`.\n - Note: index `fruits-no-name` is not used in queries", "code": "invalid_multi_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets" diff --git a/crates/meilisearch/tests/similar/errors.rs b/crates/meilisearch/tests/similar/errors.rs index 75bd6e46b..29e87d4b2 100644 --- a/crates/meilisearch/tests/similar/errors.rs +++ b/crates/meilisearch/tests/similar/errors.rs @@ -452,18 +452,19 @@ async fn filter_invalid_attribute_array() { snapshot!(code, @"202 Accepted"); index.wait_task(value.uid()).await.succeeded(); - let expected_response = json!({ - "message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass", - "code": "invalid_similar_filter", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_similar_filter" - }); index .similar( json!({"id": 287947, "filter": ["many = Glass"], "embedder": "manual"}), |response, code| { - assert_eq!(response, expected_response); - assert_eq!(code, 400); + snapshot!(code, @"400 Bad Request"); + snapshot!(response, @r###" + { + "message": "Attribute `many` is not filterable. Available filterable attributes patterns are: `title`.\n1:5 many = Glass", + "code": "invalid_similar_filter", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_similar_filter" + } + "###); }, ) .await; @@ -492,18 +493,19 @@ async fn filter_invalid_attribute_string() { snapshot!(code, @"202 Accepted"); index.wait_task(value.uid()).await.succeeded(); - let expected_response = json!({ - "message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass", - "code": "invalid_similar_filter", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_similar_filter" - }); index .similar( json!({"id": 287947, "filter": "many = Glass", "embedder": "manual"}), |response, code| { - assert_eq!(response, expected_response); - assert_eq!(code, 400); + snapshot!(code, @"400 Bad Request"); + snapshot!(response, @r###" + { + "message": "Attribute `many` is not filterable. Available filterable attributes patterns are: `title`.\n1:5 many = Glass", + "code": "invalid_similar_filter", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_similar_filter" + } + "###); }, ) .await; diff --git a/crates/milli/src/error.rs b/crates/milli/src/error.rs index 67a770148..77017a3fd 100644 --- a/crates/milli/src/error.rs +++ b/crates/milli/src/error.rs @@ -121,10 +121,10 @@ only composed of alphanumeric characters (a-z A-Z 0-9), hyphens (-) and undersco and can not be more than 511 bytes.", .document_id.to_string() )] InvalidDocumentId { document_id: Value }, - #[error("Invalid facet distribution, {}", format_invalid_filter_distribution(.invalid_facets_name, .valid_facets_name))] + #[error("Invalid facet distribution, {}", format_invalid_filter_distribution(.invalid_facets_name, .valid_patterns))] InvalidFacetsDistribution { invalid_facets_name: BTreeSet, - valid_facets_name: BTreeSet, + valid_patterns: BTreeSet, }, #[error(transparent)] InvalidGeoField(#[from] GeoError), @@ -357,9 +357,9 @@ pub enum GeoError { fn format_invalid_filter_distribution( invalid_facets_name: &BTreeSet, - valid_facets_name: &BTreeSet, + valid_patterns: &BTreeSet, ) -> String { - if valid_facets_name.is_empty() { + if valid_patterns.is_empty() { return "this index does not have configured filterable attributes.".into(); } @@ -381,17 +381,17 @@ fn format_invalid_filter_distribution( .unwrap(), }; - match valid_facets_name.len() { + match valid_patterns.len() { 1 => write!( result, - " The available filterable attribute is `{}`.", - valid_facets_name.first().unwrap() + " The available filterable attribute pattern is `{}`.", + valid_patterns.first().unwrap() ) .unwrap(), _ => write!( result, - " The available filterable attributes are `{}`.", - valid_facets_name.iter().map(AsRef::as_ref).collect::>().join(", ") + " The available filterable attribute patterns are `{}`.", + valid_patterns.iter().map(AsRef::as_ref).collect::>().join(", ") ) .unwrap(), } diff --git a/crates/milli/src/search/facet/facet_distribution.rs b/crates/milli/src/search/facet/facet_distribution.rs index 5c41a0424..757c18598 100644 --- a/crates/milli/src/search/facet/facet_distribution.rs +++ b/crates/milli/src/search/facet/facet_distribution.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::attribute_patterns::match_field_legacy; use crate::facet::FacetType; -use crate::fields_ids_map::metadata::{FieldIdMapWithMetadata, Metadata, MetadataBuilder}; +use crate::filterable_attributes_rules::{filtered_matching_patterns, matching_features}; use crate::heed_codec::facet::{ FacetGroupKeyCodec, FieldDocIdFacetF64Codec, FieldDocIdFacetStringCodec, OrderedF64Codec, }; @@ -294,13 +294,13 @@ impl<'a> FacetDistribution<'a> { return Ok(Default::default()); }; - let fields_ids_map = self.index.fields_ids_map_with_metadata(self.rtxn)?; + let fields_ids_map = self.index.fields_ids_map(self.rtxn)?; let filterable_attributes_rules = self.index.filterable_attributes_rules(self.rtxn)?; - self.check_faceted_fields(&fields_ids_map, &filterable_attributes_rules)?; + self.check_faceted_fields(&filterable_attributes_rules)?; let mut distribution = BTreeMap::new(); - for (fid, name, metadata) in fields_ids_map.iter() { - if self.select_field(name, &metadata, &filterable_attributes_rules) { + for (fid, name) in fields_ids_map.iter() { + if self.select_field(name, &filterable_attributes_rules) { let min_value = if let Some(min_value) = crate::search::facet::facet_min_value( self.index, self.rtxn, @@ -331,16 +331,12 @@ impl<'a> FacetDistribution<'a> { pub fn execute(&self) -> Result>> { let fields_ids_map = self.index.fields_ids_map(self.rtxn)?; - let fields_ids_map = FieldIdMapWithMetadata::new( - fields_ids_map, - MetadataBuilder::from_index(self.index, self.rtxn)?, - ); let filterable_attributes_rules = self.index.filterable_attributes_rules(self.rtxn)?; - self.check_faceted_fields(&fields_ids_map, &filterable_attributes_rules)?; + self.check_faceted_fields(&filterable_attributes_rules)?; let mut distribution = BTreeMap::new(); - for (fid, name, metadata) in fields_ids_map.iter() { - if self.select_field(name, &metadata, &filterable_attributes_rules) { + for (fid, name) in fields_ids_map.iter() { + if self.select_field(name, &filterable_attributes_rules) { let order_by = self .facets .as_ref() @@ -358,11 +354,12 @@ impl<'a> FacetDistribution<'a> { fn select_field( &self, name: &str, - metadata: &Metadata, filterable_attributes_rules: &[FilterableAttributesRule], ) -> bool { // If the field is not filterable, we don't want to compute the facet distribution. - if !metadata.filterable_attributes_features(filterable_attributes_rules).is_filterable() { + if !matching_features(name, filterable_attributes_rules) + .map_or(false, |(_, features)| features.is_filterable()) + { return false; } @@ -378,41 +375,31 @@ impl<'a> FacetDistribution<'a> { /// Check if the fields in the facets are valid faceted fields. fn check_faceted_fields( &self, - fields_ids_map: &FieldIdMapWithMetadata, filterable_attributes_rules: &[FilterableAttributesRule], ) -> Result<()> { let mut invalid_facets = BTreeSet::new(); if let Some(facets) = &self.facets { for field in facets.keys() { - let is_valid_faceted_field = - fields_ids_map.id_with_metadata(field).map_or(false, |(_, metadata)| { - metadata - .filterable_attributes_features(filterable_attributes_rules) - .is_filterable() - }); - if !is_valid_faceted_field { + let is_valid_filterable_field = + matching_features(field, filterable_attributes_rules) + .map_or(false, |(_, features)| features.is_filterable()); + if !is_valid_filterable_field { invalid_facets.insert(field.to_string()); } } } if !invalid_facets.is_empty() { - let valid_facets_name = fields_ids_map - .iter() - .filter_map(|(_, name, metadata)| { - if metadata - .filterable_attributes_features(filterable_attributes_rules) - .is_filterable() - { - Some(name.to_string()) - } else { - None - } + let valid_patterns = + filtered_matching_patterns(filterable_attributes_rules, &|features| { + features.is_filterable() }) + .into_iter() + .map(String::from) .collect(); return Err(Error::UserError(UserError::InvalidFacetsDistribution { invalid_facets_name: invalid_facets, - valid_facets_name, + valid_patterns, })); }