Revert metadata creation when computing the facet-distribution

This commit is contained in:
ManyTheFish 2025-03-10 17:05:41 +01:00
parent abef655849
commit 40c5f911fd
5 changed files with 85 additions and 93 deletions

View File

@ -434,7 +434,7 @@ async fn search_non_filterable_facets() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" 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", "code": "invalid_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_facets" "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!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" 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", "code": "invalid_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_facets" "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!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" 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", "code": "invalid_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_facets" "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!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" 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", "code": "invalid_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_facets" "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!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" 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", "code": "invalid_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_facets" "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!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" 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", "code": "invalid_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_search_facets" "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
@ -1204,52 +1204,55 @@ async fn search_on_unknown_field_plus_joker() {
#[actix_rt::test] #[actix_rt::test]
async fn distinct_at_search_time() { async fn distinct_at_search_time() {
let server = Server::new_shared(); let server = Server::new().await;
let index = server.unique_index(); let index = server.index("test");
let (task, _) = index.create(None).await; let (task, _) = index.create(None).await;
index.wait_task(task.uid()).await.succeeded(); index.wait_task(task.uid()).await.succeeded();
let (response, _code) = let (response, _code) =
index.add_documents(json!([{"id": 1, "color": "Doggo", "machin": "Action"}]), None).await; index.add_documents(json!([{"id": 1, "color": "Doggo", "machin": "Action"}]), None).await;
index.wait_task(response.uid()).await.succeeded(); 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) = let (response, code) =
index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await; index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await;
assert_eq!(response, expected_response); snapshot!(code, @"400 Bad Request");
assert_eq!(code, 400); 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; let (task, _) = index.update_settings_filterable_attributes(json!(["color", "machin"])).await;
index.wait_task(task.uid()).await.succeeded(); 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) = let (response, code) =
index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await; index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await;
assert_eq!(response, expected_response); snapshot!(code, @"400 Bad Request");
assert_eq!(code, 400); 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; let (task, _) = index.update_settings_displayed_attributes(json!(["color"])).await;
index.wait_task(task.uid()).await.succeeded(); 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) = let (response, code) =
index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await; index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": "doggo.truc"})).await;
assert_eq!(response, expected_response); snapshot!(code, @"400 Bad Request");
assert_eq!(code, 400); 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) = let (response, code) =
index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": true})).await; index.search_post(json!({"page": 0, "hitsPerPage": 2, "distinct": true})).await;

View File

@ -3653,7 +3653,7 @@ async fn federation_non_faceted_for_an_index() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" 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", "code": "invalid_multi_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets" "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"); snapshot!(code, @"400 Bad Request");
insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" 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", "code": "invalid_multi_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets" "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets"

View File

@ -452,18 +452,19 @@ async fn filter_invalid_attribute_array() {
snapshot!(code, @"202 Accepted"); snapshot!(code, @"202 Accepted");
index.wait_task(value.uid()).await.succeeded(); 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 index
.similar( .similar(
json!({"id": 287947, "filter": ["many = Glass"], "embedder": "manual"}), json!({"id": 287947, "filter": ["many = Glass"], "embedder": "manual"}),
|response, code| { |response, code| {
assert_eq!(response, expected_response); snapshot!(code, @"400 Bad Request");
assert_eq!(code, 400); 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; .await;
@ -492,18 +493,19 @@ async fn filter_invalid_attribute_string() {
snapshot!(code, @"202 Accepted"); snapshot!(code, @"202 Accepted");
index.wait_task(value.uid()).await.succeeded(); 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 index
.similar( .similar(
json!({"id": 287947, "filter": "many = Glass", "embedder": "manual"}), json!({"id": 287947, "filter": "many = Glass", "embedder": "manual"}),
|response, code| { |response, code| {
assert_eq!(response, expected_response); snapshot!(code, @"400 Bad Request");
assert_eq!(code, 400); 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; .await;

View File

@ -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() and can not be more than 511 bytes.", .document_id.to_string()
)] )]
InvalidDocumentId { document_id: Value }, 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 { InvalidFacetsDistribution {
invalid_facets_name: BTreeSet<String>, invalid_facets_name: BTreeSet<String>,
valid_facets_name: BTreeSet<String>, valid_patterns: BTreeSet<String>,
}, },
#[error(transparent)] #[error(transparent)]
InvalidGeoField(#[from] GeoError), InvalidGeoField(#[from] GeoError),
@ -357,9 +357,9 @@ pub enum GeoError {
fn format_invalid_filter_distribution( fn format_invalid_filter_distribution(
invalid_facets_name: &BTreeSet<String>, invalid_facets_name: &BTreeSet<String>,
valid_facets_name: &BTreeSet<String>, valid_patterns: &BTreeSet<String>,
) -> String { ) -> String {
if valid_facets_name.is_empty() { if valid_patterns.is_empty() {
return "this index does not have configured filterable attributes.".into(); return "this index does not have configured filterable attributes.".into();
} }
@ -381,17 +381,17 @@ fn format_invalid_filter_distribution(
.unwrap(), .unwrap(),
}; };
match valid_facets_name.len() { match valid_patterns.len() {
1 => write!( 1 => write!(
result, result,
" The available filterable attribute is `{}`.", " The available filterable attribute pattern is `{}`.",
valid_facets_name.first().unwrap() valid_patterns.first().unwrap()
) )
.unwrap(), .unwrap(),
_ => write!( _ => write!(
result, result,
" The available filterable attributes are `{}`.", " The available filterable attribute patterns are `{}`.",
valid_facets_name.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ") valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")
) )
.unwrap(), .unwrap(),
} }

View File

@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use crate::attribute_patterns::match_field_legacy; use crate::attribute_patterns::match_field_legacy;
use crate::facet::FacetType; 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::{ use crate::heed_codec::facet::{
FacetGroupKeyCodec, FieldDocIdFacetF64Codec, FieldDocIdFacetStringCodec, OrderedF64Codec, FacetGroupKeyCodec, FieldDocIdFacetF64Codec, FieldDocIdFacetStringCodec, OrderedF64Codec,
}; };
@ -294,13 +294,13 @@ impl<'a> FacetDistribution<'a> {
return Ok(Default::default()); 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)?; 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(); let mut distribution = BTreeMap::new();
for (fid, name, metadata) in fields_ids_map.iter() { for (fid, name) in fields_ids_map.iter() {
if self.select_field(name, &metadata, &filterable_attributes_rules) { if self.select_field(name, &filterable_attributes_rules) {
let min_value = if let Some(min_value) = crate::search::facet::facet_min_value( let min_value = if let Some(min_value) = crate::search::facet::facet_min_value(
self.index, self.index,
self.rtxn, self.rtxn,
@ -331,16 +331,12 @@ impl<'a> FacetDistribution<'a> {
pub fn execute(&self) -> Result<BTreeMap<String, IndexMap<String, u64>>> { pub fn execute(&self) -> Result<BTreeMap<String, IndexMap<String, u64>>> {
let fields_ids_map = self.index.fields_ids_map(self.rtxn)?; 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)?; 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(); let mut distribution = BTreeMap::new();
for (fid, name, metadata) in fields_ids_map.iter() { for (fid, name) in fields_ids_map.iter() {
if self.select_field(name, &metadata, &filterable_attributes_rules) { if self.select_field(name, &filterable_attributes_rules) {
let order_by = self let order_by = self
.facets .facets
.as_ref() .as_ref()
@ -358,11 +354,12 @@ impl<'a> FacetDistribution<'a> {
fn select_field( fn select_field(
&self, &self,
name: &str, name: &str,
metadata: &Metadata,
filterable_attributes_rules: &[FilterableAttributesRule], filterable_attributes_rules: &[FilterableAttributesRule],
) -> bool { ) -> bool {
// If the field is not filterable, we don't want to compute the facet distribution. // 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; return false;
} }
@ -378,41 +375,31 @@ impl<'a> FacetDistribution<'a> {
/// Check if the fields in the facets are valid faceted fields. /// Check if the fields in the facets are valid faceted fields.
fn check_faceted_fields( fn check_faceted_fields(
&self, &self,
fields_ids_map: &FieldIdMapWithMetadata,
filterable_attributes_rules: &[FilterableAttributesRule], filterable_attributes_rules: &[FilterableAttributesRule],
) -> Result<()> { ) -> Result<()> {
let mut invalid_facets = BTreeSet::new(); let mut invalid_facets = BTreeSet::new();
if let Some(facets) = &self.facets { if let Some(facets) = &self.facets {
for field in facets.keys() { for field in facets.keys() {
let is_valid_faceted_field = let is_valid_filterable_field =
fields_ids_map.id_with_metadata(field).map_or(false, |(_, metadata)| { matching_features(field, filterable_attributes_rules)
metadata .map_or(false, |(_, features)| features.is_filterable());
.filterable_attributes_features(filterable_attributes_rules) if !is_valid_filterable_field {
.is_filterable()
});
if !is_valid_faceted_field {
invalid_facets.insert(field.to_string()); invalid_facets.insert(field.to_string());
} }
} }
} }
if !invalid_facets.is_empty() { if !invalid_facets.is_empty() {
let valid_facets_name = fields_ids_map let valid_patterns =
.iter() filtered_matching_patterns(filterable_attributes_rules, &|features| {
.filter_map(|(_, name, metadata)| { features.is_filterable()
if metadata
.filterable_attributes_features(filterable_attributes_rules)
.is_filterable()
{
Some(name.to_string())
} else {
None
}
}) })
.into_iter()
.map(String::from)
.collect(); .collect();
return Err(Error::UserError(UserError::InvalidFacetsDistribution { return Err(Error::UserError(UserError::InvalidFacetsDistribution {
invalid_facets_name: invalid_facets, invalid_facets_name: invalid_facets,
valid_facets_name, valid_patterns,
})); }));
} }