mirror of
				https://github.com/meilisearch/meilisearch.git
				synced 2025-10-26 13:36:27 +00:00 
			
		
		
		
	Enhance error messages for filterable attributes and improve error handling
This commit is contained in:
		
							
								
								
									
										2098
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2098
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -2,6 +2,7 @@ use std::collections::BTreeSet; | ||||
| use std::convert::Infallible; | ||||
| use std::fmt::Write; | ||||
| use std::{io, str}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use bstr::BString; | ||||
| use heed::{Error as HeedError, MdbError}; | ||||
| @@ -78,7 +79,7 @@ pub enum InternalError { | ||||
|     #[error(transparent)] | ||||
|     ArroyError(#[from] arroy::Error), | ||||
|     #[error(transparent)] | ||||
|     VectorEmbeddingError(#[from] crate::vector::Error), | ||||
|     VectorEmbeddingError(#[from] crate::vector::Error) | ||||
| } | ||||
|  | ||||
| #[derive(Error, Debug)] | ||||
| @@ -120,10 +121,34 @@ 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_patterns))] | ||||
|     #[error("Invalid facet distribution: {}", | ||||
|         if .invalid_facets_name.len() == 1 { | ||||
|             let field = .invalid_facets_name.iter().next().unwrap(); | ||||
|             match .matching_rule_indices.get(field) { | ||||
|                 Some(rule_index) => format!("Attribute `{}` matched rule #{} in filterableAttributes, but this rule does not enable filtering.\nHint: enable filtering in rule #{} by modifying the features.filter object\nHint: prepend another rule matching `{}` with appropriate filter features before rule #{}",  | ||||
|                     field, rule_index, rule_index, field, rule_index), | ||||
|                 None => match .valid_patterns.is_empty() { | ||||
|                     true => format!("Attribute `{}` is not filterable. This index does not have configured filterable attributes.", field), | ||||
|                     false => format!("Attribute `{}` is not filterable. Available filterable attributes patterns are: `{}`.",  | ||||
|                         field,  | ||||
|                         .valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")), | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             format!("Attributes `{}` are not filterable. {}",  | ||||
|                 .invalid_facets_name.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "), | ||||
|                 match .valid_patterns.is_empty() { | ||||
|                     true => "This index does not have configured filterable attributes.".to_string(), | ||||
|                     false => format!("Available filterable attributes patterns are: `{}`.",  | ||||
|                         .valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")), | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|     )] | ||||
|     InvalidFacetsDistribution { | ||||
|         invalid_facets_name: BTreeSet<String>, | ||||
|         valid_patterns: BTreeSet<String>, | ||||
|         matching_rule_indices: HashMap<String, usize>, | ||||
|     }, | ||||
|     #[error(transparent)] | ||||
|     InvalidGeoField(#[from] GeoError), | ||||
| @@ -137,7 +162,12 @@ and can not be more than 511 bytes.", .document_id.to_string() | ||||
|     InvalidFilter(String), | ||||
|     #[error("Invalid type for filter subexpression: expected: {}, found: {}.", .0.join(", "), .1)] | ||||
|     InvalidFilterExpression(&'static [&'static str], Value), | ||||
|     #[error("Filter operator `{operator}` is not allowed for the attribute `{field}`.\n  - Note: allowed operators: {}.\n  - Note: field `{field}` {} in `filterableAttributes`", allowed_operators.join(", "), format!("matched rule #{rule_index}"))] | ||||
|     #[error("Filter operator `{operator}` is not allowed for the attribute `{field}`.\n  - Note: allowed operators: {}.\n  - Note: field `{field}` matched rule #{rule_index} in `filterableAttributes`\n  - Hint: enable {} in rule #{rule_index} by modifying the features.filter object\n  - Hint: prepend another rule matching `{field}` with appropriate filter features before rule #{rule_index}",  | ||||
|         allowed_operators.join(", "),  | ||||
|         if operator == "=" || operator == "!=" || operator == "IN" {"equality"}  | ||||
|         else if operator == "<" || operator == ">" || operator == "<=" || operator == ">=" || operator == "TO" {"comparison"}  | ||||
|         else {"the appropriate filter operators"} | ||||
|     )] | ||||
|     FilterOperatorNotAllowed { | ||||
|         field: String, | ||||
|         allowed_operators: Vec<String>, | ||||
| @@ -157,33 +187,51 @@ and can not be more than 511 bytes.", .document_id.to_string() | ||||
|     InvalidSortableAttribute { field: String, valid_fields: BTreeSet<String>, hidden_fields: bool }, | ||||
|     #[error("Attribute `{}` is not filterable and thus, cannot be used as distinct attribute. {}", | ||||
|         .field, | ||||
|         match .valid_patterns.is_empty() { | ||||
|             true => "This index does not have configured filterable attributes.".to_string(), | ||||
|             false => format!("Available filterable attributes patterns are: `{}{}`.", | ||||
|         match (.valid_patterns.is_empty(), .matching_rule_index) { | ||||
|             // No rules match and no filterable attributes | ||||
|             (true, None) => "This index does not have configured filterable attributes.".to_string(), | ||||
|              | ||||
|             // No rules match but there are some filterable attributes | ||||
|             (false, None) => format!("Available filterable attributes patterns are: `{}{}`.", | ||||
|                     valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "), | ||||
|                     .hidden_fields.then_some(", <..hidden-attributes>").unwrap_or(""), | ||||
|                 ), | ||||
|              | ||||
|             // A rule matched but filtering isn't enabled | ||||
|             (_, Some(rule_index)) => format!("Note: this attribute matches rule #{} in filterableAttributes, but this rule does not enable filtering.\nHint: enable filtering in rule #{} by adding appropriate filter features.\nHint: prepend another rule matching {} with filter features before rule #{}", | ||||
|                     rule_index, rule_index, .field, rule_index | ||||
|                 ), | ||||
|         } | ||||
|     )] | ||||
|     InvalidDistinctAttribute { | ||||
|         field: String, | ||||
|         valid_patterns: BTreeSet<String>, | ||||
|         hidden_fields: bool, | ||||
|         matching_rule_index: Option<usize>, | ||||
|     }, | ||||
|     #[error("Attribute `{}` is not facet-searchable. {}", | ||||
|         .field, | ||||
|         match .valid_patterns.is_empty() { | ||||
|             true => "This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.".to_string(), | ||||
|             false => format!("Available facet-searchable attributes patterns are: `{}{}`. To make it facet-searchable add it to the `filterableAttributes` index settings.", | ||||
|         match (.valid_patterns.is_empty(), .matching_rule_index) { | ||||
|             // No rules match and no facet searchable attributes | ||||
|             (true, None) => "This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.".to_string(), | ||||
|              | ||||
|             // No rules match but there are some facet searchable attributes | ||||
|             (false, None) => format!("Available facet-searchable attributes patterns are: `{}{}`. To make it facet-searchable add it to the `filterableAttributes` index settings.", | ||||
|                     valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", "), | ||||
|                     .hidden_fields.then_some(", <..hidden-attributes>").unwrap_or(""), | ||||
|                 ), | ||||
|              | ||||
|             // A rule matched but facet search isn't enabled | ||||
|             (_, Some(rule_index)) => format!("Note: this attribute matches rule #{} in filterableAttributes, but this rule does not enable facetSearch.\nHint: enable facetSearch in rule #{} by adding `\"facetSearch\": true` to the rule.\nHint: prepend another rule matching {} with facetSearch: true before rule #{}", | ||||
|                     rule_index, rule_index, .field, rule_index | ||||
|                 ), | ||||
|         } | ||||
|     )] | ||||
|     InvalidFacetSearchFacetName { | ||||
|         field: String, | ||||
|         valid_patterns: BTreeSet<String>, | ||||
|         hidden_fields: bool, | ||||
|         matching_rule_index: Option<usize>, | ||||
|     }, | ||||
|     #[error("Attribute `{}` is not searchable. Available searchable attributes are: `{}{}`.", | ||||
|         .field, | ||||
| @@ -438,7 +486,7 @@ fn format_invalid_filter_distribution( | ||||
| /// ```ignore | ||||
| /// impl From<FieldIdMapMissingEntry> for Error { | ||||
| ///     fn from(error: FieldIdMapMissingEntry) -> Error { | ||||
| ///         Error::from(InternalError::from(error)) | ||||
| ///         Error::from(<InternalError>::from(error)) | ||||
| ///     } | ||||
| /// } | ||||
| /// ``` | ||||
|   | ||||
| @@ -378,13 +378,21 @@ impl<'a> FacetDistribution<'a> { | ||||
|         filterable_attributes_rules: &[FilterableAttributesRule], | ||||
|     ) -> Result<()> { | ||||
|         let mut invalid_facets = BTreeSet::new(); | ||||
|         let mut matching_rule_indices = HashMap::new(); | ||||
|          | ||||
|         if let Some(facets) = &self.facets { | ||||
|             for field in facets.keys() { | ||||
|                 let is_valid_filterable_field = | ||||
|                     matching_features(field, filterable_attributes_rules) | ||||
|                         .map_or(false, |(_, features)| features.is_filterable()); | ||||
|                 if !is_valid_filterable_field { | ||||
|                 let matched_rule = matching_features(field, filterable_attributes_rules); | ||||
|                 let is_filterable = matched_rule.map_or(false, |(_, features)| features.is_filterable()); | ||||
|                  | ||||
|                 if !is_filterable { | ||||
|                     invalid_facets.insert(field.to_string()); | ||||
|                      | ||||
|                     // If the field matched a rule but that rule doesn't enable filtering, | ||||
|                     // store the rule index for better error messages | ||||
|                     if let Some((rule_index, _)) = matched_rule { | ||||
|                         matching_rule_indices.insert(field.to_string(), rule_index); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -400,6 +408,7 @@ impl<'a> FacetDistribution<'a> { | ||||
|             return Err(Error::UserError(UserError::InvalidFacetsDistribution { | ||||
|                 invalid_facets_name: invalid_facets, | ||||
|                 valid_patterns, | ||||
|                 matching_rule_indices, | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -75,9 +75,10 @@ impl<'a> SearchForFacetValues<'a> { | ||||
|         let rtxn = self.search_query.rtxn; | ||||
|  | ||||
|         let filterable_attributes_rules = index.filterable_attributes_rules(rtxn)?; | ||||
|         if !matching_features(&self.facet, &filterable_attributes_rules) | ||||
|             .map_or(false, |(_, features)| features.is_facet_searchable()) | ||||
|         { | ||||
|         let matched_rule = matching_features(&self.facet, &filterable_attributes_rules); | ||||
|         let is_facet_searchable = matched_rule.map_or(false, |(_, features)| features.is_facet_searchable()); | ||||
|          | ||||
|         if !is_facet_searchable { | ||||
|             let matching_field_names = | ||||
|                 filtered_matching_patterns(&filterable_attributes_rules, &|features| { | ||||
|                     features.is_facet_searchable() | ||||
| @@ -85,10 +86,14 @@ impl<'a> SearchForFacetValues<'a> { | ||||
|             let (valid_patterns, hidden_fields) = | ||||
|                 index.remove_hidden_fields(rtxn, matching_field_names)?; | ||||
|                  | ||||
|             // Get the matching rule index if any rule matched the attribute | ||||
|             let matching_rule_index = matched_rule.map(|(rule_index, _)| rule_index); | ||||
|  | ||||
|             return Err(UserError::InvalidFacetSearchFacetName { | ||||
|                 field: self.facet.clone(), | ||||
|                 valid_patterns, | ||||
|                 hidden_fields, | ||||
|                 matching_rule_index, | ||||
|             } | ||||
|             .into()); | ||||
|         }; | ||||
|   | ||||
| @@ -190,9 +190,10 @@ impl<'a> Search<'a> { | ||||
|         if let Some(distinct) = &self.distinct { | ||||
|             let filterable_fields = ctx.index.filterable_attributes_rules(ctx.txn)?; | ||||
|             // check if the distinct field is in the filterable fields | ||||
|             if !matching_features(distinct, &filterable_fields) | ||||
|                 .map_or(false, |(_, features)| features.is_filterable()) | ||||
|             { | ||||
|             let matched_rule = matching_features(distinct, &filterable_fields); | ||||
|             let is_filterable = matched_rule.map_or(false, |(_, features)| features.is_filterable()); | ||||
|              | ||||
|             if !is_filterable { | ||||
|                 // if not, remove the hidden fields from the filterable fields to generate the error message | ||||
|                 let matching_patterns = | ||||
|                     filtered_matching_patterns(&filterable_fields, &|features| { | ||||
| @@ -200,11 +201,16 @@ impl<'a> Search<'a> { | ||||
|                     }); | ||||
|                 let (valid_patterns, hidden_fields) = | ||||
|                     ctx.index.remove_hidden_fields(ctx.txn, matching_patterns)?; | ||||
|                  | ||||
|                 // Get the matching rule index if any rule matched the attribute | ||||
|                 let matching_rule_index = matched_rule.map(|(rule_index, _)| rule_index); | ||||
|                  | ||||
|                 // and return the error | ||||
|                 return Err(Error::UserError(UserError::InvalidDistinctAttribute { | ||||
|                     field: distinct.clone(), | ||||
|                     valid_patterns, | ||||
|                     hidden_fields, | ||||
|                     matching_rule_index, | ||||
|                 })); | ||||
|             } | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user