From 8bd8e744f35db460750935a9107a9bcb51b798c2 Mon Sep 17 00:00:00 2001 From: Lucas Black Date: Fri, 9 May 2025 02:42:48 -0700 Subject: [PATCH 1/7] Attributes to search on supports nested wildcards --- crates/milli/src/search/new/mod.rs | 56 ++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/crates/milli/src/search/new/mod.rs b/crates/milli/src/search/new/mod.rs index 6e794ef53..21002c55a 100644 --- a/crates/milli/src/search/new/mod.rs +++ b/crates/milli/src/search/new/mod.rs @@ -120,17 +120,39 @@ impl<'ctx> SearchContext<'ctx> { let searchable_fields_weights = self.index.searchable_fields_and_weights(self.txn)?; let exact_attributes_ids = self.index.exact_attributes_ids(self.txn)?; - let mut wildcard = false; + let mut universal_wildcard = false; let mut restricted_fids = RestrictedFids::default(); for field_name in attributes_to_search_on { if field_name == "*" { - wildcard = true; + universal_wildcard = true; // we cannot early exit as we want to returns error in case of unknown fields continue; } let searchable_weight = searchable_fields_weights.iter().find(|(name, _, _)| name == field_name); + + // The field is not searchable but may contain a wildcard pattern + if searchable_weight.is_none() && field_name.contains("*") { + let matching_searchable_weights: Vec<_> = searchable_fields_weights + .iter() + .filter(|(name, _, _)| { + Self::matches_wildcard_pattern(field_name, name) + }) + .collect(); + + if !matching_searchable_weights.is_empty() { + for (_name, fid, weight) in matching_searchable_weights { + if exact_attributes_ids.contains(fid) { + restricted_fids.exact.push((*fid, *weight)); + } else { + restricted_fids.tolerant.push((*fid, *weight)); + } + } + continue; + } + } + let (fid, weight) = match searchable_weight { // The Field id exist and the field is searchable Some((_name, fid, weight)) => (*fid, *weight), @@ -160,7 +182,7 @@ impl<'ctx> SearchContext<'ctx> { }; } - if wildcard { + if universal_wildcard { self.restricted_fids = None; } else { self.restricted_fids = Some(restricted_fids); @@ -168,6 +190,34 @@ impl<'ctx> SearchContext<'ctx> { Ok(()) } + + fn matches_wildcard_pattern(wildcard_pattern: &str, name: &str) -> bool { + let wildcard_subfields: Vec<&str> = wildcard_pattern.split(".").collect(); + let name_subfields: Vec<&str> = name.split(".").collect(); + + // Deep wildcard matches all attributes after ('**') + if !wildcard_subfields.is_empty() && wildcard_subfields.last() == Some(&"**") { + let prefix_len = wildcard_subfields.len() - 1; + if prefix_len > name_subfields.len() { + return false; + } + + return wildcard_subfields[..prefix_len] + .iter() + .zip(name_subfields.iter()) + .all(|(wc, sf)| *wc == "*" || *wc == *sf); + } + + // Using single wildcard ('*') should match length (e.g. 'a.*.c' matches 'a.b.c') + // where '*' can match any single segment + if wildcard_subfields.len() != name_subfields.len() { + return false; + } + + wildcard_subfields.iter() + .zip(name_subfields.iter()) + .all(|(wc, sf)| *wc == "*" || *wc == *sf) + } } #[derive(Debug, Default)] From 150d1db86bd88c3375754afd80021189b32e16f9 Mon Sep 17 00:00:00 2001 From: Lucas Black Date: Tue, 13 May 2025 21:44:24 -0700 Subject: [PATCH 2/7] Implemented integration tests for restrict_searchable.rs on nested wildcard attributes --- .../tests/search/restrict_searchable.rs | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) diff --git a/crates/meilisearch/tests/search/restrict_searchable.rs b/crates/meilisearch/tests/search/restrict_searchable.rs index ce99c4047..80eef96db 100644 --- a/crates/meilisearch/tests/search/restrict_searchable.rs +++ b/crates/meilisearch/tests/search/restrict_searchable.rs @@ -416,3 +416,323 @@ async fn phrase_search_on_title() { ) .await; } + +static NESTED_SEARCH_DOCUMENTS: Lazy = Lazy::new(|| { + json!([ + { + "details": { + "title": "Shazam!", + "desc": "a Captain Marvel ersatz", + "weaknesses": ["magic", "requires transformation"], + "outfit": { + "has_cape": true + } + }, + "id": "1", + }, + { + "details": { + "title": "Captain Planet", + "desc": "He's not part of the Marvel Cinematic Universe", + "blue_skin": true, + "outfit": { + "has_cape": false + } + }, + "id": "2", + }, + { + "details": { + "title": "Captain Marvel", + "desc": "a Shazam ersatz", + "weaknesses": ["magic", "power instability"], + "outfit": { + "has_cape": false + } + }, + "id": "3", + }]) +}); + +#[actix_rt::test] +async fn nested_search_on_title_with_prefix_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Wildcard should match to 'details.' attribute + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["*.title"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_search_on_title_with_suffix_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Wildcard should match to any attribute inside 'details.' + // It's worth noting the difference between 'details.*' and '*.title' + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.*"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"3"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_search_all_details_with_deep_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Similar to matching all attributes on simple search documents with universal wildcard + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.**"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"3"); + }, + ) + .await; + + // Should return 2 documents (ids: 1 and 2) + index + .search( + json!({"q": "true", "attributesToSearchOn": ["details.**"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_search_all_details_restricted_set_with_any_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + let (task, _status_code) = index.update_settings_searchable_attributes(json!(["details.title"])).await; + index.wait_task(task.uid()).await.succeeded(); + + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.*"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + }, + ) + .await; + + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.**"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_search_no_searchable_attribute_set_with_any_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.*", "*.unknown"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"0"); + }, + ) + .await; + + let (task, _status_code) = index.update_settings_searchable_attributes(json!(["*"])).await; + index.wait_task(task.uid()).await.succeeded(); + + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.*", "*.unknown"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"0"); + }, + ) + .await; + + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.**",]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"0"); + }, + ) + .await; + + let (task, _status_code) = index.update_settings_searchable_attributes(json!(["*"])).await; + index.wait_task(task.uid()).await.succeeded(); + + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.*", "*.unknown", "*.title"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + }, + ) + .await; + + // We only match deep wild card at the end, otherwise we need to recursively match deep wildcards + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.**", "details.**"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"3"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_prefix_search_on_title_with_prefix_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Nested prefix search with wildcard prefix should return 2 documents (ids: 2 and 3). + index + .search( + json!({"q": "Captain Mar", "attributesToSearchOn": ["*.title"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_prefix_search_on_details_with_suffix_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + index + .search( + json!({"q": "Captain Mar", "attributesToSearchOn": ["details.*"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"3"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_prefix_search_on_weaknesses_with_deep_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Deep wildcard search on nested weaknesses should return 2 documents (ids: 1 and 3) + index + .search( + json!({"q": "mag", "attributesToSearchOn": ["details.**"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_search_on_title_matching_strategy_all() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Nested search matching strategy all should only return 1 document (ids: 3) + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["*.title"], "matchingStrategy": "all"}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"1"); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_attributes_ranking_rule_order_with_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Document 3 should appear before documents 1 and 2 + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["*.desc", "*.title"], "attributesToRetrieve": ["id"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "1" + }, + { + "id": "2" + } + ] + "### + ); + }, + ) + .await; +} + +#[actix_rt::test] +async fn nested_attributes_ranking_rule_order_with_deep_wildcard() { + let server = Server::new().await; + let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; + + // Document 3 should appear before documents 1 and 2 + index + .search( + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.**"], "attributesToRetrieve": ["id"]}), + |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "1" + }, + { + "id": "2" + } + ] + "### + ); + }, + ) + .await; +} From 3fbe1df770ca95fc53a9c061a7ef87eaf63ff117 Mon Sep 17 00:00:00 2001 From: Lucas Black Date: Wed, 14 May 2025 00:18:30 -0700 Subject: [PATCH 3/7] Updated nested_search_all_details_with_deep_wildcard() to test deeply nested attributes --- .../meilisearch/tests/search/restrict_searchable.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/meilisearch/tests/search/restrict_searchable.rs b/crates/meilisearch/tests/search/restrict_searchable.rs index 80eef96db..ffd612557 100644 --- a/crates/meilisearch/tests/search/restrict_searchable.rs +++ b/crates/meilisearch/tests/search/restrict_searchable.rs @@ -425,7 +425,11 @@ static NESTED_SEARCH_DOCUMENTS: Lazy = Lazy::new(|| { "desc": "a Captain Marvel ersatz", "weaknesses": ["magic", "requires transformation"], "outfit": { - "has_cape": true + "has_cape": true, + "colors": { + "primary": "red", + "secondary": "gold" + } } }, "id": "1", @@ -494,13 +498,13 @@ async fn nested_search_all_details_with_deep_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; - // Similar to matching all attributes on simple search documents with universal wildcard + // Deep wildcard should match deeply nested attributes index .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.**"]}), + json!({"q": "gold", "attributesToSearchOn": ["details.**"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"3"); + snapshot!(response["hits"].as_array().unwrap().len(), @"1"); }, ) .await; From 13b607bd68243f81be98ce852b9bf327e08142a1 Mon Sep 17 00:00:00 2001 From: Lucas Black Date: Sun, 18 May 2025 20:24:52 -0700 Subject: [PATCH 4/7] Removed matches_wildcard_pattern() and integrated match_pattern() into attributes_to_search_on(), updated test cases --- .../tests/search/restrict_searchable.rs | 57 ++++--------------- crates/milli/src/attribute_patterns.rs | 2 +- crates/milli/src/search/new/mod.rs | 31 +--------- 3 files changed, 13 insertions(+), 77 deletions(-) diff --git a/crates/meilisearch/tests/search/restrict_searchable.rs b/crates/meilisearch/tests/search/restrict_searchable.rs index ffd612557..db1082053 100644 --- a/crates/meilisearch/tests/search/restrict_searchable.rs +++ b/crates/meilisearch/tests/search/restrict_searchable.rs @@ -476,7 +476,7 @@ async fn nested_search_on_title_with_prefix_wildcard() { } #[actix_rt::test] -async fn nested_search_on_title_with_suffix_wildcard() { +async fn nested_search_with_suffix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; @@ -491,17 +491,11 @@ async fn nested_search_on_title_with_suffix_wildcard() { }, ) .await; -} -#[actix_rt::test] -async fn nested_search_all_details_with_deep_wildcard() { - let server = Server::new().await; - let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; - - // Deep wildcard should match deeply nested attributes + // Should return 1 document (ids: 1) index .search( - json!({"q": "gold", "attributesToSearchOn": ["details.**"]}), + json!({"q": "gold", "attributesToSearchOn": ["details.*"]}), |response, code| { snapshot!(code, @"200 OK"); snapshot!(response["hits"].as_array().unwrap().len(), @"1"); @@ -512,7 +506,7 @@ async fn nested_search_all_details_with_deep_wildcard() { // Should return 2 documents (ids: 1 and 2) index .search( - json!({"q": "true", "attributesToSearchOn": ["details.**"]}), + json!({"q": "true", "attributesToSearchOn": ["details.*"]}), |response, code| { snapshot!(code, @"200 OK"); snapshot!(response["hits"].as_array().unwrap().len(), @"2"); @@ -522,7 +516,7 @@ async fn nested_search_all_details_with_deep_wildcard() { } #[actix_rt::test] -async fn nested_search_all_details_restricted_set_with_any_wildcard() { +async fn nested_search_on_title_restricted_set_with_suffix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; let (task, _status_code) = index.update_settings_searchable_attributes(json!(["details.title"])).await; @@ -537,16 +531,6 @@ async fn nested_search_all_details_restricted_set_with_any_wildcard() { }, ) .await; - - index - .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.**"]}), - |response, code| { - snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"2"); - }, - ) - .await; } #[actix_rt::test] @@ -577,16 +561,6 @@ async fn nested_search_no_searchable_attribute_set_with_any_wildcard() { ) .await; - index - .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.**",]}), - |response, code| { - snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"0"); - }, - ) - .await; - let (task, _status_code) = index.update_settings_searchable_attributes(json!(["*"])).await; index.wait_task(task.uid()).await.succeeded(); @@ -599,17 +573,6 @@ async fn nested_search_no_searchable_attribute_set_with_any_wildcard() { }, ) .await; - - // We only match deep wild card at the end, otherwise we need to recursively match deep wildcards - index - .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.**", "details.**"]}), - |response, code| { - snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"3"); - }, - ) - .await; } #[actix_rt::test] @@ -646,14 +609,14 @@ async fn nested_prefix_search_on_details_with_suffix_wildcard() { } #[actix_rt::test] -async fn nested_prefix_search_on_weaknesses_with_deep_wildcard() { +async fn nested_prefix_search_on_weaknesses_with_suffix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; // Deep wildcard search on nested weaknesses should return 2 documents (ids: 1 and 3) index .search( - json!({"q": "mag", "attributesToSearchOn": ["details.**"]}), + json!({"q": "mag", "attributesToSearchOn": ["details.*"]}), |response, code| { snapshot!(code, @"200 OK"); snapshot!(response["hits"].as_array().unwrap().len(), @"2"); @@ -680,7 +643,7 @@ async fn nested_search_on_title_matching_strategy_all() { } #[actix_rt::test] -async fn nested_attributes_ranking_rule_order_with_wildcard() { +async fn nested_attributes_ranking_rule_order_with_prefix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; @@ -711,14 +674,14 @@ async fn nested_attributes_ranking_rule_order_with_wildcard() { } #[actix_rt::test] -async fn nested_attributes_ranking_rule_order_with_deep_wildcard() { +async fn nested_attributes_ranking_rule_order_with_suffix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; // Document 3 should appear before documents 1 and 2 index .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.**"], "attributesToRetrieve": ["id"]}), + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.*"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); snapshot!(json_string!(response["hits"]), diff --git a/crates/milli/src/attribute_patterns.rs b/crates/milli/src/attribute_patterns.rs index 00caa2a6d..8da6942a3 100644 --- a/crates/milli/src/attribute_patterns.rs +++ b/crates/milli/src/attribute_patterns.rs @@ -50,7 +50,7 @@ impl AttributePatterns { /// /// * `pattern` - The pattern to match against. /// * `str` - The string to match against the pattern. -fn match_pattern(pattern: &str, str: &str) -> PatternMatch { +pub fn match_pattern(pattern: &str, str: &str) -> PatternMatch { // If the pattern is a wildcard, return Match if pattern == "*" { return PatternMatch::Match; diff --git a/crates/milli/src/search/new/mod.rs b/crates/milli/src/search/new/mod.rs index 21002c55a..dfe0ddfc9 100644 --- a/crates/milli/src/search/new/mod.rs +++ b/crates/milli/src/search/new/mod.rs @@ -52,6 +52,7 @@ pub use self::geo_sort::Strategy as GeoSortStrategy; use self::graph_based_ranking_rule::Words; use self::interner::Interned; use self::vector_sort::VectorSort; +use crate::attribute_patterns::{match_pattern, PatternMatch}; use crate::constants::RESERVED_GEO_FIELD_NAME; use crate::index::PrefixSearch; use crate::localized_attributes_rules::LocalizedFieldIds; @@ -137,7 +138,7 @@ impl<'ctx> SearchContext<'ctx> { let matching_searchable_weights: Vec<_> = searchable_fields_weights .iter() .filter(|(name, _, _)| { - Self::matches_wildcard_pattern(field_name, name) + match_pattern(field_name, name) == PatternMatch::Match }) .collect(); @@ -190,34 +191,6 @@ impl<'ctx> SearchContext<'ctx> { Ok(()) } - - fn matches_wildcard_pattern(wildcard_pattern: &str, name: &str) -> bool { - let wildcard_subfields: Vec<&str> = wildcard_pattern.split(".").collect(); - let name_subfields: Vec<&str> = name.split(".").collect(); - - // Deep wildcard matches all attributes after ('**') - if !wildcard_subfields.is_empty() && wildcard_subfields.last() == Some(&"**") { - let prefix_len = wildcard_subfields.len() - 1; - if prefix_len > name_subfields.len() { - return false; - } - - return wildcard_subfields[..prefix_len] - .iter() - .zip(name_subfields.iter()) - .all(|(wc, sf)| *wc == "*" || *wc == *sf); - } - - // Using single wildcard ('*') should match length (e.g. 'a.*.c' matches 'a.b.c') - // where '*' can match any single segment - if wildcard_subfields.len() != name_subfields.len() { - return false; - } - - wildcard_subfields.iter() - .zip(name_subfields.iter()) - .all(|(wc, sf)| *wc == "*" || *wc == *sf) - } } #[derive(Debug, Default)] From 1594c54e2301bbda794c192efcc1ccc8016df6e0 Mon Sep 17 00:00:00 2001 From: Lucas Black Date: Mon, 19 May 2025 02:37:23 -0700 Subject: [PATCH 5/7] Provide more information about resulting documents on test case --- .../meilisearch/tests/search/restrict_searchable.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/meilisearch/tests/search/restrict_searchable.rs b/crates/meilisearch/tests/search/restrict_searchable.rs index db1082053..2232d961b 100644 --- a/crates/meilisearch/tests/search/restrict_searchable.rs +++ b/crates/meilisearch/tests/search/restrict_searchable.rs @@ -466,10 +466,19 @@ async fn nested_search_on_title_with_prefix_wildcard() { // Wildcard should match to 'details.' attribute index .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["*.title"]}), + json!({"q": "Captain Marvel", "attributesToSearchOn": ["*.title"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "2" + } + ]"###); }, ) .await; From c5ae43cac6ba37a97c0bb186763511aeefa712c7 Mon Sep 17 00:00:00 2001 From: Lucas Black Date: Tue, 20 May 2025 09:03:26 -0700 Subject: [PATCH 6/7] Updated all additional test cases --- .../tests/search/restrict_searchable.rs | 121 +++++++++++++++--- 1 file changed, 101 insertions(+), 20 deletions(-) diff --git a/crates/meilisearch/tests/search/restrict_searchable.rs b/crates/meilisearch/tests/search/restrict_searchable.rs index 2232d961b..2c8c86b5d 100644 --- a/crates/meilisearch/tests/search/restrict_searchable.rs +++ b/crates/meilisearch/tests/search/restrict_searchable.rs @@ -493,10 +493,22 @@ async fn nested_search_with_suffix_wildcard() { // It's worth noting the difference between 'details.*' and '*.title' index .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.*"]}), + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.*"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"3"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "1" + }, + { + "id": "2" + } + ]"###); }, ) .await; @@ -504,10 +516,16 @@ async fn nested_search_with_suffix_wildcard() { // Should return 1 document (ids: 1) index .search( - json!({"q": "gold", "attributesToSearchOn": ["details.*"]}), + json!({"q": "gold", "attributesToSearchOn": ["details.*"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"1"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "1" + } + ]"###); }, ) .await; @@ -515,10 +533,19 @@ async fn nested_search_with_suffix_wildcard() { // Should return 2 documents (ids: 1 and 2) index .search( - json!({"q": "true", "attributesToSearchOn": ["details.*"]}), + json!({"q": "true", "attributesToSearchOn": ["details.*"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "1" + }, + { + "id": "2" + } + ]"###); }, ) .await; @@ -533,10 +560,19 @@ async fn nested_search_on_title_restricted_set_with_suffix_wildcard() { index .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.*"]}), + json!({"q": "Captain Marvel", "attributesToSearchOn": ["details.*"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "2" + } + ]"###); }, ) .await; @@ -575,10 +611,19 @@ async fn nested_search_no_searchable_attribute_set_with_any_wildcard() { index .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.*", "*.unknown", "*.title"]}), + json!({"q": "Captain Marvel", "attributesToSearchOn": ["unknown.*", "*.unknown", "*.title"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "2" + } + ]"###); }, ) .await; @@ -589,13 +634,22 @@ async fn nested_prefix_search_on_title_with_prefix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; - // Nested prefix search with wildcard prefix should return 2 documents (ids: 2 and 3). + // Nested prefix search with prefix wildcard should return 2 documents (ids: 2 and 3). index .search( - json!({"q": "Captain Mar", "attributesToSearchOn": ["*.title"]}), + json!({"q": "Captain Mar", "attributesToSearchOn": ["*.title"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "2" + } + ]"###); }, ) .await; @@ -608,10 +662,22 @@ async fn nested_prefix_search_on_details_with_suffix_wildcard() { index .search( - json!({"q": "Captain Mar", "attributesToSearchOn": ["details.*"]}), + json!({"q": "Captain Mar", "attributesToSearchOn": ["details.*"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"3"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + }, + { + "id": "1" + }, + { + "id": "2" + } + ]"###); }, ) .await; @@ -622,13 +688,22 @@ async fn nested_prefix_search_on_weaknesses_with_suffix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; - // Deep wildcard search on nested weaknesses should return 2 documents (ids: 1 and 3) + // Wildcard search on nested weaknesses should return 2 documents (ids: 1 and 3) index .search( - json!({"q": "mag", "attributesToSearchOn": ["details.*"]}), + json!({"q": "mag", "attributesToSearchOn": ["details.*"], "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"2"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "1" + }, + { + "id": "3" + } + ]"###); }, ) .await; @@ -642,10 +717,16 @@ async fn nested_search_on_title_matching_strategy_all() { // Nested search matching strategy all should only return 1 document (ids: 3) index .search( - json!({"q": "Captain Marvel", "attributesToSearchOn": ["*.title"], "matchingStrategy": "all"}), + json!({"q": "Captain Marvel", "attributesToSearchOn": ["*.title"], "matchingStrategy": "all", "attributesToRetrieve": ["id"]}), |response, code| { snapshot!(code, @"200 OK"); - snapshot!(response["hits"].as_array().unwrap().len(), @"1"); + snapshot!(json_string!(response["hits"]), + @r###" + [ + { + "id": "3" + } + ]"###); }, ) .await; From f888f876352abc0a4103e2d85a58c48b1e06f594 Mon Sep 17 00:00:00 2001 From: Lucas Black Date: Wed, 21 May 2025 02:07:25 -0700 Subject: [PATCH 7/7] Updated formatting using RustFmt --- crates/meilisearch/tests/search/restrict_searchable.rs | 3 ++- crates/milli/src/search/new/mod.rs | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/meilisearch/tests/search/restrict_searchable.rs b/crates/meilisearch/tests/search/restrict_searchable.rs index 2c8c86b5d..8ef5db26d 100644 --- a/crates/meilisearch/tests/search/restrict_searchable.rs +++ b/crates/meilisearch/tests/search/restrict_searchable.rs @@ -555,7 +555,8 @@ async fn nested_search_with_suffix_wildcard() { async fn nested_search_on_title_restricted_set_with_suffix_wildcard() { let server = Server::new().await; let index = index_with_documents(&server, &NESTED_SEARCH_DOCUMENTS).await; - let (task, _status_code) = index.update_settings_searchable_attributes(json!(["details.title"])).await; + let (task, _status_code) = + index.update_settings_searchable_attributes(json!(["details.title"])).await; index.wait_task(task.uid()).await.succeeded(); index diff --git a/crates/milli/src/search/new/mod.rs b/crates/milli/src/search/new/mod.rs index dfe0ddfc9..0a3bc1b04 100644 --- a/crates/milli/src/search/new/mod.rs +++ b/crates/milli/src/search/new/mod.rs @@ -137,9 +137,7 @@ impl<'ctx> SearchContext<'ctx> { if searchable_weight.is_none() && field_name.contains("*") { let matching_searchable_weights: Vec<_> = searchable_fields_weights .iter() - .filter(|(name, _, _)| { - match_pattern(field_name, name) == PatternMatch::Match - }) + .filter(|(name, _, _)| match_pattern(field_name, name) == PatternMatch::Match) .collect(); if !matching_searchable_weights.is_empty() {