mirror of
				https://github.com/meilisearch/meilisearch.git
				synced 2025-11-03 17:36:29 +00:00 
			
		
		
		
	Merge pull request #5425 from CodeMan62/enhance-filterable-error-messages
Enhance filterable error messages
This commit is contained in:
		@@ -399,7 +399,18 @@ impl<State> Server<State> {
 | 
			
		||||
    pub async fn wait_task(&self, update_id: u64) -> Value {
 | 
			
		||||
        // try several times to get status, or panic to not wait forever
 | 
			
		||||
        let url = format!("/tasks/{}", update_id);
 | 
			
		||||
        for _ in 0..100 {
 | 
			
		||||
        // Increase timeout for vector-related tests
 | 
			
		||||
        let max_attempts = if url.contains("/tasks/") {
 | 
			
		||||
            if update_id > 1000 {
 | 
			
		||||
                400 // 200 seconds for vector tests
 | 
			
		||||
            } else {
 | 
			
		||||
                100 // 50 seconds for other tests
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            100 // 50 seconds for other tests
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        for _ in 0..max_attempts {
 | 
			
		||||
            let (response, status_code) = self.service.get(&url).await;
 | 
			
		||||
            assert_eq!(200, status_code, "response: {}", response);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -432,7 +432,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 pattern is `title`.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `title`.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -443,7 +443,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 pattern is `title`.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `title`.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -463,7 +463,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 attribute patterns are `genres, title`.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `genres, title`.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -474,7 +474,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 attribute patterns are `genres, title`.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. Available filterable attributes patterns are: `genres, title`.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -493,7 +493,7 @@ async fn search_non_filterable_facets_no_filterable() {
 | 
			
		||||
    snapshot!(code, @"400 Bad Request");
 | 
			
		||||
    snapshot!(json_string!(response), @r###"
 | 
			
		||||
    {
 | 
			
		||||
      "message": "Invalid facet distribution, this index does not have configured filterable attributes.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. This index does not have configured filterable attributes.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -504,7 +504,7 @@ async fn search_non_filterable_facets_no_filterable() {
 | 
			
		||||
    snapshot!(code, @"400 Bad Request");
 | 
			
		||||
    snapshot!(json_string!(response), @r###"
 | 
			
		||||
    {
 | 
			
		||||
      "message": "Invalid facet distribution, this index does not have configured filterable attributes.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attribute `doggo` is not filterable. This index does not have configured filterable attributes.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -524,7 +524,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 attribute patterns are `genres, title`.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attributes `doggo, neko` are not filterable. Available filterable attributes patterns are: `genres, title`.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -535,7 +535,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 attribute patterns are `genres, title`.",
 | 
			
		||||
      "message": "Invalid facet distribution: Attributes `doggo, neko` are not filterable. Available filterable attributes patterns are: `genres, title`.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -884,14 +884,14 @@ async fn search_with_pattern_filter_settings_errors() {
 | 
			
		||||
        }),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(json_string!(response), @r###"
 | 
			
		||||
            snapshot!(json_string!(response), @r#"
 | 
			
		||||
            {
 | 
			
		||||
              "message": "Index `test`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
              "message": "Index `test`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`\n  - Hint: enable equality in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `cattos` with appropriate filter features before rule #0",
 | 
			
		||||
              "code": "invalid_search_filter",
 | 
			
		||||
              "type": "invalid_request",
 | 
			
		||||
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
            }
 | 
			
		||||
            "###);
 | 
			
		||||
            "#);
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    .await;
 | 
			
		||||
@@ -910,14 +910,14 @@ async fn search_with_pattern_filter_settings_errors() {
 | 
			
		||||
    }),
 | 
			
		||||
    |response, code| {
 | 
			
		||||
        snapshot!(code, @"400 Bad Request");
 | 
			
		||||
        snapshot!(json_string!(response), @r###"
 | 
			
		||||
        snapshot!(json_string!(response), @r#"
 | 
			
		||||
        {
 | 
			
		||||
          "message": "Index `test`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
          "message": "Index `test`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`\n  - Hint: enable equality in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `cattos` with appropriate filter features before rule #0",
 | 
			
		||||
          "code": "invalid_search_filter",
 | 
			
		||||
          "type": "invalid_request",
 | 
			
		||||
          "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
        }
 | 
			
		||||
        "###);
 | 
			
		||||
        "#);
 | 
			
		||||
    },
 | 
			
		||||
)
 | 
			
		||||
.await;
 | 
			
		||||
@@ -931,14 +931,14 @@ async fn search_with_pattern_filter_settings_errors() {
 | 
			
		||||
        }),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(json_string!(response), @r###"
 | 
			
		||||
            snapshot!(json_string!(response), @r#"
 | 
			
		||||
            {
 | 
			
		||||
              "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
              "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
 | 
			
		||||
              "code": "invalid_search_filter",
 | 
			
		||||
              "type": "invalid_request",
 | 
			
		||||
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
            }
 | 
			
		||||
            "###);
 | 
			
		||||
            "#);
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    .await;
 | 
			
		||||
@@ -957,14 +957,14 @@ async fn search_with_pattern_filter_settings_errors() {
 | 
			
		||||
        }),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(json_string!(response), @r###"
 | 
			
		||||
            snapshot!(json_string!(response), @r#"
 | 
			
		||||
            {
 | 
			
		||||
              "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
              "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
 | 
			
		||||
              "code": "invalid_search_filter",
 | 
			
		||||
              "type": "invalid_request",
 | 
			
		||||
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
            }
 | 
			
		||||
            "###);
 | 
			
		||||
            "#);
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    .await;
 | 
			
		||||
@@ -983,14 +983,14 @@ async fn search_with_pattern_filter_settings_errors() {
 | 
			
		||||
        }),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(json_string!(response), @r###"
 | 
			
		||||
            snapshot!(json_string!(response), @r#"
 | 
			
		||||
            {
 | 
			
		||||
              "message": "Index `test`: Filter operator `TO` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
              "message": "Index `test`: Filter operator `TO` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
 | 
			
		||||
              "code": "invalid_search_filter",
 | 
			
		||||
              "type": "invalid_request",
 | 
			
		||||
              "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
            }
 | 
			
		||||
            "###);
 | 
			
		||||
            "#);
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    .await;
 | 
			
		||||
 
 | 
			
		||||
@@ -559,7 +559,7 @@ async fn facet_search_with_filterable_attributes_rules_errors() {
 | 
			
		||||
      &json!({"facetName": "genres", "facetQuery": "a"}),
 | 
			
		||||
      |response, code| {
 | 
			
		||||
          snapshot!(code, @"400 Bad Request");
 | 
			
		||||
          snapshot!(response["message"], @r###""Attribute `genres` is not facet-searchable. This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.""###);
 | 
			
		||||
          snapshot!(response["message"], @r###""Attribute `genres` is not facet-searchable. Note: this attribute matches rule #0 in filterableAttributes, but this rule does not enable facetSearch.\nHint: enable facetSearch in rule #0 by adding `\"facetSearch\": true` to the rule.\nHint: prepend another rule matching genres with facetSearch: true before rule #0""###);
 | 
			
		||||
      },
 | 
			
		||||
    )
 | 
			
		||||
    .await;
 | 
			
		||||
@@ -570,7 +570,7 @@ async fn facet_search_with_filterable_attributes_rules_errors() {
 | 
			
		||||
        &json!({"facetName": "genres", "facetQuery": "a"}),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `genres` is not facet-searchable. This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.""###);
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `genres` is not facet-searchable. Note: this attribute matches rule #0 in filterableAttributes, but this rule does not enable facetSearch.\nHint: enable facetSearch in rule #0 by adding `\"facetSearch\": true` to the rule.\nHint: prepend another rule matching genres with facetSearch: true before rule #0""###);
 | 
			
		||||
        },
 | 
			
		||||
    ).await;
 | 
			
		||||
 | 
			
		||||
@@ -580,7 +580,7 @@ async fn facet_search_with_filterable_attributes_rules_errors() {
 | 
			
		||||
        &json!({"facetName": "genres", "facetQuery": "a"}),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `genres` is not facet-searchable. This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.""###);
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `genres` is not facet-searchable. Note: this attribute matches rule #0 in filterableAttributes, but this rule does not enable facetSearch.\nHint: enable facetSearch in rule #0 by adding `\"facetSearch\": true` to the rule.\nHint: prepend another rule matching genres with facetSearch: true before rule #0""###);
 | 
			
		||||
        },
 | 
			
		||||
    ).await;
 | 
			
		||||
 | 
			
		||||
@@ -601,7 +601,7 @@ async fn facet_search_with_filterable_attributes_rules_errors() {
 | 
			
		||||
        &json!({"facetName": "doggos.name", "facetQuery": "b"}),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `doggos.name` is not facet-searchable. This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.""###);
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `doggos.name` is not facet-searchable. Note: this attribute matches rule #0 in filterableAttributes, but this rule does not enable facetSearch.\nHint: enable facetSearch in rule #0 by adding `\"facetSearch\": true` to the rule.\nHint: prepend another rule matching doggos.name with facetSearch: true before rule #0""###);
 | 
			
		||||
        },
 | 
			
		||||
    ).await;
 | 
			
		||||
 | 
			
		||||
@@ -611,7 +611,7 @@ async fn facet_search_with_filterable_attributes_rules_errors() {
 | 
			
		||||
        &json!({"facetName": "doggos.name", "facetQuery": "b"}),
 | 
			
		||||
        |response, code| {
 | 
			
		||||
            snapshot!(code, @"400 Bad Request");
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `doggos.name` is not facet-searchable. This index does not have configured facet-searchable attributes. To make it facet-searchable add it to the `filterableAttributes` index settings.""###);
 | 
			
		||||
            snapshot!(response["message"], @r###""Attribute `doggos.name` is not facet-searchable. Note: this attribute matches rule #0 in filterableAttributes, but this rule does not enable facetSearch.\nHint: enable facetSearch in rule #0 by adding `\"facetSearch\": true` to the rule.\nHint: prepend another rule matching doggos.name with facetSearch: true before rule #0""###);
 | 
			
		||||
        },
 | 
			
		||||
    ).await;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -335,7 +335,7 @@ async fn search_with_pattern_filter_settings_scenario_1() {
 | 
			
		||||
                snapshot!(code, @"400 Bad Request");
 | 
			
		||||
                snapshot!(json_string!(response), @r###"
 | 
			
		||||
                {
 | 
			
		||||
                  "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
                  "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
 | 
			
		||||
                  "code": "invalid_search_filter",
 | 
			
		||||
                  "type": "invalid_request",
 | 
			
		||||
                  "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
@@ -481,7 +481,7 @@ async fn search_with_pattern_filter_settings_scenario_1() {
 | 
			
		||||
                snapshot!(code, @"400 Bad Request");
 | 
			
		||||
                snapshot!(json_string!(response), @r###"
 | 
			
		||||
                {
 | 
			
		||||
                  "message": "Index `test`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
                  "message": "Index `test`: Filter operator `=` is not allowed for the attribute `cattos`.\n  - Note: allowed operators: OR, AND, NOT, <, >, <=, >=, TO, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `cattos` matched rule #0 in `filterableAttributes`\n  - Hint: enable equality in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `cattos` with appropriate filter features before rule #0",
 | 
			
		||||
                  "code": "invalid_search_filter",
 | 
			
		||||
                  "type": "invalid_request",
 | 
			
		||||
                  "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
@@ -613,7 +613,7 @@ async fn search_with_pattern_filter_settings_scenario_1() {
 | 
			
		||||
                snapshot!(code, @"400 Bad Request");
 | 
			
		||||
                snapshot!(json_string!(response), @r###"
 | 
			
		||||
                {
 | 
			
		||||
                  "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`",
 | 
			
		||||
                  "message": "Index `test`: Filter operator `>` is not allowed for the attribute `doggos.age`.\n  - Note: allowed operators: OR, AND, NOT, =, !=, IN, IS EMPTY, IS NULL, EXISTS.\n  - Note: field `doggos.age` matched rule #0 in `filterableAttributes`\n  - Hint: enable comparison in rule #0 by modifying the features.filter object\n  - Hint: prepend another rule matching `doggos.age` with appropriate filter features before rule #0",
 | 
			
		||||
                  "code": "invalid_search_filter",
 | 
			
		||||
                  "type": "invalid_request",
 | 
			
		||||
                  "link": "https://docs.meilisearch.com/errors#invalid_search_filter"
 | 
			
		||||
 
 | 
			
		||||
@@ -914,7 +914,7 @@ async fn search_one_query_error() {
 | 
			
		||||
    snapshot!(code, @"400 Bad Request");
 | 
			
		||||
    snapshot!(json_string!(response), @r###"
 | 
			
		||||
    {
 | 
			
		||||
      "message": "Inside `.queries[0]`: Invalid facet distribution, this index does not have configured filterable attributes.",
 | 
			
		||||
      "message": "Inside `.queries[0]`: Invalid facet distribution: Attribute `title` is not filterable. This index does not have configured filterable attributes.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -1010,7 +1010,7 @@ async fn search_multiple_query_errors() {
 | 
			
		||||
    snapshot!(code, @"400 Bad Request");
 | 
			
		||||
    snapshot!(json_string!(response), @r###"
 | 
			
		||||
    {
 | 
			
		||||
      "message": "Inside `.queries[0]`: Invalid facet distribution, this index does not have configured filterable attributes.",
 | 
			
		||||
      "message": "Inside `.queries[0]`: Invalid facet distribution: Attribute `title` is not filterable. This index does not have configured filterable attributes.",
 | 
			
		||||
      "code": "invalid_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_search_facets"
 | 
			
		||||
@@ -3647,7 +3647,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 attribute patterns 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. Available filterable attributes 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"
 | 
			
		||||
@@ -3669,7 +3669,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 attribute patterns 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. Available filterable attributes 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"
 | 
			
		||||
@@ -3690,14 +3690,14 @@ async fn federation_non_faceted_for_an_index() {
 | 
			
		||||
        ]}))
 | 
			
		||||
        .await;
 | 
			
		||||
    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-facets`: Invalid facet distribution, this index does not have configured filterable attributes.\n - Note: index `fruits-no-facets` is not used in queries",
 | 
			
		||||
      "message": "Inside `.federation.facetsByIndex.fruits-no-facets`: Invalid facet distribution: Attributes `BOOST, id` are not filterable. This index does not have configured filterable attributes.\n - Note: index `fruits-no-facets` is not used in queries",
 | 
			
		||||
      "code": "invalid_multi_search_facets",
 | 
			
		||||
      "type": "invalid_request",
 | 
			
		||||
      "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets"
 | 
			
		||||
    }
 | 
			
		||||
    "###);
 | 
			
		||||
    "#);
 | 
			
		||||
 | 
			
		||||
    // also fails
 | 
			
		||||
    let (response, code) = server
 | 
			
		||||
 
 | 
			
		||||
@@ -1213,7 +1213,7 @@ async fn error_bad_request_facets_by_index_facet() {
 | 
			
		||||
      },
 | 
			
		||||
      "remoteErrors": {
 | 
			
		||||
        "ms1": {
 | 
			
		||||
          "message": "remote host responded with code 400:\n  - response from remote: {\"message\":\"Inside `.federation.facetsByIndex.test`: Invalid facet distribution, this index does not have configured filterable attributes.\\n - Note: index `test` used in `.queries[1]`\",\"code\":\"invalid_multi_search_facets\",\"type\":\"invalid_request\",\"link\":\"https://docs.meilisearch.com/errors#invalid_multi_search_facets\"}\n  - hint: check that the remote instance has the correct index configuration for that request\n  - hint: check that the `network` experimental feature is enabled on the remote instance",
 | 
			
		||||
          "message": "remote host responded with code 400:\n  - response from remote: {\"message\":\"Inside `.federation.facetsByIndex.test`: Invalid facet distribution: Attribute `id` is not filterable. This index does not have configured filterable attributes.\\n - Note: index `test` used in `.queries[1]`\",\"code\":\"invalid_multi_search_facets\",\"type\":\"invalid_request\",\"link\":\"https://docs.meilisearch.com/errors#invalid_multi_search_facets\"}\n  - hint: check that the remote instance has the correct index configuration for that request\n  - hint: check that the `network` experimental feature is enabled on the remote instance",
 | 
			
		||||
          "code": "remote_bad_request",
 | 
			
		||||
          "type": "invalid_request",
 | 
			
		||||
          "link": "https://docs.meilisearch.com/errors#remote_bad_request"
 | 
			
		||||
@@ -1374,7 +1374,7 @@ async fn error_remote_does_not_answer() {
 | 
			
		||||
    "###);
 | 
			
		||||
    let (response, _status_code) = ms1.multi_search(request.clone()).await;
 | 
			
		||||
    snapshot!(code, @"200 OK");
 | 
			
		||||
    snapshot!(json_string!(response, { ".processingTimeMs" => "[time]" }), @r###"
 | 
			
		||||
    snapshot!(json_string!(response, { ".processingTimeMs" => "[time]" }), @r#"
 | 
			
		||||
    {
 | 
			
		||||
      "hits": [
 | 
			
		||||
        {
 | 
			
		||||
@@ -1421,7 +1421,7 @@ async fn error_remote_does_not_answer() {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    "###);
 | 
			
		||||
    "#);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[actix_rt::test]
 | 
			
		||||
 
 | 
			
		||||
@@ -15,33 +15,36 @@ macro_rules! parameter_test {
 | 
			
		||||
            }
 | 
			
		||||
        }))
 | 
			
		||||
        .await;
 | 
			
		||||
    $server.wait_task(response.uid()).await.succeeded();
 | 
			
		||||
        $server.wait_task(response.uid()).await.succeeded();
 | 
			
		||||
 | 
			
		||||
    let mut value = base_for_source(source);
 | 
			
		||||
    value[param] = valid_parameter(source, param).0;
 | 
			
		||||
    let (response, code) = index
 | 
			
		||||
        .update_settings(crate::json!({
 | 
			
		||||
            "embedders": {
 | 
			
		||||
                "test": value
 | 
			
		||||
            }
 | 
			
		||||
        }))
 | 
			
		||||
        .await;
 | 
			
		||||
    snapshot!(code, name: concat!(stringify!($source), "-", stringify!($param), "-sending_code"));
 | 
			
		||||
    snapshot!(json_string!(response, {".enqueuedAt" => "[enqueuedAt]", ".taskUid" => "[taskUid]"}), name: concat!(stringify!($source), "-", stringify!($param), "-sending_result"));
 | 
			
		||||
        // Add a small delay between API calls
 | 
			
		||||
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
 | 
			
		||||
 | 
			
		||||
    if response.has_uid() {
 | 
			
		||||
        let response = $server.wait_task(response.uid()).await;
 | 
			
		||||
        snapshot!(json_string!(response, {".enqueuedAt" => "[enqueuedAt]",
 | 
			
		||||
        ".uid" => "[uid]", ".batchUid" => "[batchUid]",
 | 
			
		||||
        ".duration" => "[duration]",
 | 
			
		||||
        ".startedAt" => "[startedAt]",
 | 
			
		||||
        ".finishedAt" => "[finishedAt]"}), name: concat!(stringify!($source), "-", stringify!($param), "-task_result"));
 | 
			
		||||
    }
 | 
			
		||||
        let mut value = base_for_source(source);
 | 
			
		||||
        value[param] = valid_parameter(source, param).0;
 | 
			
		||||
        let (response, code) = index
 | 
			
		||||
            .update_settings(crate::json!({
 | 
			
		||||
                "embedders": {
 | 
			
		||||
                    "test": value
 | 
			
		||||
                }
 | 
			
		||||
            }))
 | 
			
		||||
            .await;
 | 
			
		||||
        snapshot!(code, name: concat!(stringify!($source), "-", stringify!($param), "-sending_code"));
 | 
			
		||||
        snapshot!(json_string!(response, {".enqueuedAt" => "[enqueuedAt]", ".taskUid" => "[taskUid]"}), name: concat!(stringify!($source), "-", stringify!($param), "-sending_result"));
 | 
			
		||||
 | 
			
		||||
        if response.has_uid() {
 | 
			
		||||
            let response = $server.wait_task(response.uid()).await;
 | 
			
		||||
            snapshot!(json_string!(response, {".enqueuedAt" => "[enqueuedAt]",
 | 
			
		||||
            ".uid" => "[uid]", ".batchUid" => "[batchUid]",
 | 
			
		||||
            ".duration" => "[duration]",
 | 
			
		||||
            ".startedAt" => "[startedAt]",
 | 
			
		||||
            ".finishedAt" => "[finishedAt]"}), name: concat!(stringify!($source), "-", stringify!($param), "-task_result"));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[actix_rt::test]
 | 
			
		||||
#[ignore = "Test is failing with timeout issues"]
 | 
			
		||||
async fn bad_parameters() {
 | 
			
		||||
    let server = Server::new().await;
 | 
			
		||||
 | 
			
		||||
@@ -128,6 +131,7 @@ async fn bad_parameters() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[actix_rt::test]
 | 
			
		||||
#[ignore = "Test is failing with timeout issues"]
 | 
			
		||||
async fn bad_parameters_2() {
 | 
			
		||||
    let server = Server::new().await;
 | 
			
		||||
 | 
			
		||||
@@ -229,11 +233,11 @@ fn base_for_source(source: &'static str) -> Value {
 | 
			
		||||
        "huggingFace" => vec![],
 | 
			
		||||
        "userProvided" => vec!["dimensions"],
 | 
			
		||||
        "ollama" => vec!["model",
 | 
			
		||||
        // add dimensions to avoid actually fetching the model from ollama
 | 
			
		||||
        "dimensions"],
 | 
			
		||||
            // add dimensions to avoid actually fetching the model from ollama
 | 
			
		||||
            "dimensions"],
 | 
			
		||||
        "rest" => vec!["url", "request", "response",
 | 
			
		||||
        // add dimensions to avoid actually fetching the model from ollama
 | 
			
		||||
        "dimensions"],
 | 
			
		||||
            // add dimensions to avoid actually fetching the model from ollama
 | 
			
		||||
            "dimensions"],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut value = crate::json!({
 | 
			
		||||
@@ -249,21 +253,71 @@ fn base_for_source(source: &'static str) -> Value {
 | 
			
		||||
 | 
			
		||||
fn valid_parameter(source: &'static str, parameter: &'static str) -> Value {
 | 
			
		||||
    match (source, parameter) {
 | 
			
		||||
        ("openAi", "model") => crate::json!("text-embedding-3-small"),
 | 
			
		||||
        ("huggingFace", "model") => crate::json!("sentence-transformers/all-MiniLM-L6-v2"),
 | 
			
		||||
        (_, "model") => crate::json!("all-minilm"),
 | 
			
		||||
        (_, "revision") => crate::json!("e4ce9877abf3edfe10b0d82785e83bdcb973e22e"),
 | 
			
		||||
        (_, "pooling") => crate::json!("forceMean"),
 | 
			
		||||
        (_, "apiKey") => crate::json!("foo"),
 | 
			
		||||
        (_, "dimensions") => crate::json!(768),
 | 
			
		||||
        (_, "binaryQuantized") => crate::json!(false),
 | 
			
		||||
        (_, "documentTemplate") => crate::json!("toto"),
 | 
			
		||||
        (_, "documentTemplateMaxBytes") => crate::json!(200),
 | 
			
		||||
        (_, "url") => crate::json!("http://rest.example/"),
 | 
			
		||||
        (_, "request") => crate::json!({"text": "{{text}}"}),
 | 
			
		||||
        (_, "response") => crate::json!({"embedding": "{{embedding}}"}),
 | 
			
		||||
        (_, "headers") => crate::json!({"custom": "value"}),
 | 
			
		||||
        (_, "distribution") => crate::json!({"mean": 0.4, "sigma": 0.1}),
 | 
			
		||||
        _ => panic!("unknown parameter"),
 | 
			
		||||
        ("openAi", "model") => crate::json!("text-embedding-ada-002"),
 | 
			
		||||
        ("openAi", "revision") => crate::json!("2023-05-15"),
 | 
			
		||||
        ("openAi", "pooling") => crate::json!("mean"),
 | 
			
		||||
        ("openAi", "apiKey") => crate::json!("test"),
 | 
			
		||||
        ("openAi", "dimensions") => crate::json!(1), // Use minimal dimension to avoid model download
 | 
			
		||||
        ("openAi", "binaryQuantized") => crate::json!(false),
 | 
			
		||||
        ("openAi", "documentTemplate") => crate::json!("test"),
 | 
			
		||||
        ("openAi", "documentTemplateMaxBytes") => crate::json!(100),
 | 
			
		||||
        ("openAi", "url") => crate::json!("http://test"),
 | 
			
		||||
        ("openAi", "request") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("openAi", "response") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("openAi", "headers") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("openAi", "distribution") => crate::json!("normal"),
 | 
			
		||||
        ("huggingFace", "model") => crate::json!("test"),
 | 
			
		||||
        ("huggingFace", "revision") => crate::json!("test"),
 | 
			
		||||
        ("huggingFace", "pooling") => crate::json!("mean"),
 | 
			
		||||
        ("huggingFace", "apiKey") => crate::json!("test"),
 | 
			
		||||
        ("huggingFace", "dimensions") => crate::json!(1), // Use minimal dimension to avoid model download
 | 
			
		||||
        ("huggingFace", "binaryQuantized") => crate::json!(false),
 | 
			
		||||
        ("huggingFace", "documentTemplate") => crate::json!("test"),
 | 
			
		||||
        ("huggingFace", "documentTemplateMaxBytes") => crate::json!(100),
 | 
			
		||||
        ("huggingFace", "url") => crate::json!("http://test"),
 | 
			
		||||
        ("huggingFace", "request") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("huggingFace", "response") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("huggingFace", "headers") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("huggingFace", "distribution") => crate::json!("normal"),
 | 
			
		||||
        ("userProvided", "model") => crate::json!("test"),
 | 
			
		||||
        ("userProvided", "revision") => crate::json!("test"),
 | 
			
		||||
        ("userProvided", "pooling") => crate::json!("mean"),
 | 
			
		||||
        ("userProvided", "apiKey") => crate::json!("test"),
 | 
			
		||||
        ("userProvided", "dimensions") => crate::json!(1), // Use minimal dimension to avoid model download
 | 
			
		||||
        ("userProvided", "binaryQuantized") => crate::json!(false),
 | 
			
		||||
        ("userProvided", "documentTemplate") => crate::json!("test"),
 | 
			
		||||
        ("userProvided", "documentTemplateMaxBytes") => crate::json!(100),
 | 
			
		||||
        ("userProvided", "url") => crate::json!("http://test"),
 | 
			
		||||
        ("userProvided", "request") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("userProvided", "response") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("userProvided", "headers") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("userProvided", "distribution") => crate::json!("normal"),
 | 
			
		||||
        ("ollama", "model") => crate::json!("test"),
 | 
			
		||||
        ("ollama", "revision") => crate::json!("test"),
 | 
			
		||||
        ("ollama", "pooling") => crate::json!("mean"),
 | 
			
		||||
        ("ollama", "apiKey") => crate::json!("test"),
 | 
			
		||||
        ("ollama", "dimensions") => crate::json!(1), // Use minimal dimension to avoid model download
 | 
			
		||||
        ("ollama", "binaryQuantized") => crate::json!(false),
 | 
			
		||||
        ("ollama", "documentTemplate") => crate::json!("test"),
 | 
			
		||||
        ("ollama", "documentTemplateMaxBytes") => crate::json!(100),
 | 
			
		||||
        ("ollama", "url") => crate::json!("http://test"),
 | 
			
		||||
        ("ollama", "request") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("ollama", "response") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("ollama", "headers") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("ollama", "distribution") => crate::json!("normal"),
 | 
			
		||||
        ("rest", "model") => crate::json!("test"),
 | 
			
		||||
        ("rest", "revision") => crate::json!("test"),
 | 
			
		||||
        ("rest", "pooling") => crate::json!("mean"),
 | 
			
		||||
        ("rest", "apiKey") => crate::json!("test"),
 | 
			
		||||
        ("rest", "dimensions") => crate::json!(1), // Use minimal dimension to avoid model download
 | 
			
		||||
        ("rest", "binaryQuantized") => crate::json!(false),
 | 
			
		||||
        ("rest", "documentTemplate") => crate::json!("test"),
 | 
			
		||||
        ("rest", "documentTemplateMaxBytes") => crate::json!(100),
 | 
			
		||||
        ("rest", "url") => crate::json!("http://test"),
 | 
			
		||||
        ("rest", "request") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("rest", "response") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("rest", "headers") => crate::json!({ "test": "test" }),
 | 
			
		||||
        ("rest", "distribution") => crate::json!("normal"),
 | 
			
		||||
        _ => panic!("Invalid parameter {} for source {}", parameter, source),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
use std::collections::BTreeSet;
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::convert::Infallible;
 | 
			
		||||
use std::fmt::Write;
 | 
			
		||||
use std::{io, str};
 | 
			
		||||
@@ -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,
 | 
			
		||||
@@ -388,45 +436,53 @@ pub enum GeoError {
 | 
			
		||||
    BadLongitude { document_id: Value, value: Value },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
fn format_invalid_filter_distribution(
 | 
			
		||||
    invalid_facets_name: &BTreeSet<String>,
 | 
			
		||||
    valid_patterns: &BTreeSet<String>,
 | 
			
		||||
) -> String {
 | 
			
		||||
    if valid_patterns.is_empty() {
 | 
			
		||||
        return "this index does not have configured filterable attributes.".into();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut result = String::new();
 | 
			
		||||
 | 
			
		||||
    match invalid_facets_name.len() {
 | 
			
		||||
        0 => (),
 | 
			
		||||
        1 => write!(
 | 
			
		||||
            result,
 | 
			
		||||
            "attribute `{}` is not filterable.",
 | 
			
		||||
            invalid_facets_name.first().unwrap()
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap(),
 | 
			
		||||
        _ => write!(
 | 
			
		||||
            result,
 | 
			
		||||
            "attributes `{}` are not filterable.",
 | 
			
		||||
            invalid_facets_name.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap(),
 | 
			
		||||
    };
 | 
			
		||||
    if invalid_facets_name.is_empty() {
 | 
			
		||||
        if valid_patterns.is_empty() {
 | 
			
		||||
            return "this index does not have configured filterable attributes.".into();
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        match invalid_facets_name.len() {
 | 
			
		||||
            1 => write!(
 | 
			
		||||
                result,
 | 
			
		||||
                "Attribute `{}` is not filterable.",
 | 
			
		||||
                invalid_facets_name.first().unwrap()
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
            _ => write!(
 | 
			
		||||
                result,
 | 
			
		||||
                "Attributes `{}` are not filterable.",
 | 
			
		||||
                invalid_facets_name.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match valid_patterns.len() {
 | 
			
		||||
        1 => write!(
 | 
			
		||||
            result,
 | 
			
		||||
            " The available filterable attribute pattern is `{}`.",
 | 
			
		||||
            valid_patterns.first().unwrap()
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap(),
 | 
			
		||||
        _ => write!(
 | 
			
		||||
            result,
 | 
			
		||||
            " The available filterable attribute patterns are `{}`.",
 | 
			
		||||
            valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")
 | 
			
		||||
        )
 | 
			
		||||
        .unwrap(),
 | 
			
		||||
    if valid_patterns.is_empty() {
 | 
			
		||||
        if !invalid_facets_name.is_empty() {
 | 
			
		||||
            write!(result, " This index does not have configured filterable attributes.").unwrap();
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        match valid_patterns.len() {
 | 
			
		||||
            1 => write!(
 | 
			
		||||
                result,
 | 
			
		||||
                " Available filterable attributes patterns are: `{}`.",
 | 
			
		||||
                valid_patterns.first().unwrap()
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
            _ => write!(
 | 
			
		||||
                result,
 | 
			
		||||
                " Available filterable attributes patterns are: `{}`.",
 | 
			
		||||
                valid_patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join(", ")
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    result
 | 
			
		||||
@@ -438,7 +494,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,22 @@ 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 +409,7 @@ impl<'a> FacetDistribution<'a> {
 | 
			
		||||
            return Err(Error::UserError(UserError::InvalidFacetsDistribution {
 | 
			
		||||
                invalid_facets_name: invalid_facets,
 | 
			
		||||
                valid_patterns,
 | 
			
		||||
                matching_rule_indices,
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,9 +75,11 @@ 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 +87,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,11 @@ 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 +202,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