From b926f133508b01d6b2b68296e240a224242e61b0 Mon Sep 17 00:00:00 2001 From: Kumar Ujjawal Date: Mon, 10 Nov 2025 13:07:40 +0530 Subject: [PATCH 1/3] Add filterableAttributes syntax API to settings --- .code-samples.meilisearch.yaml | 24 +++++-- src/documents.rs | 6 +- src/settings.rs | 114 +++++++++++++++++++++++++++++++-- 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 6bdef025..400b3337 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -546,14 +546,30 @@ get_filterable_attributes_1: |- .await .unwrap(); update_filterable_attributes_1: |- - let filterable_attributes = [ - "genres", - "director" + use meilisearch_sdk::settings::{ + FilterableAttribute, + FilterableAttributesSettings, + FilterFeatures, + FilterFeatureModes, + }; + + // Mixed legacy + new syntax + let filterable_attributes: Vec = vec![ + // legacy: plain attribute name + "author".into(), + // new syntax: settings object + FilterableAttribute::Settings(FilterableAttributesSettings { + attribute_patterns: vec!["genre".to_string()], + features: FilterFeatures { + facet_search: true, + filter: FilterFeatureModes { equality: true, comparison: false }, + }, + }), ]; let task: TaskInfo = client .index("movies") - .set_filterable_attributes(&filterable_attributes) + .set_filterable_attributes_advanced(&filterable_attributes) .await .unwrap(); reset_filterable_attributes_1: |- diff --git a/src/documents.rs b/src/documents.rs index 63a36caa..07bf9b24 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -703,9 +703,13 @@ Hint: It might not be working because you're not up to date with the Meilisearch ); assert!(video_settings.displayed_attributes.unwrap().is_empty()); + use crate::settings::FilterableAttribute; assert_eq!( movie_settings.filterable_attributes.unwrap(), - ["release_date", "genres"] + vec![ + FilterableAttribute::Attribute("release_date".to_string()), + FilterableAttribute::Attribute("genres".to_string()), + ] ); assert!(video_settings.filterable_attributes.unwrap().is_empty()); diff --git a/src/settings.rs b/src/settings.rs index ba1c05e2..32bf2a3a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -64,6 +64,55 @@ pub struct FacetingSettings { pub sort_facet_values_by: Option>, } +/// Filterable attribute settings. +/// +/// Meilisearch supports a mixed syntax: either a plain attribute name +/// (string) or an object describing patterns and feature flags. This SDK +/// models it with `FilterableAttribute` (untagged enum) and associated +/// settings structs. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FilterFeatureModes { + pub equality: bool, + pub comparison: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FilterFeatures { + pub facet_search: bool, + pub filter: FilterFeatureModes, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct FilterableAttributesSettings { + #[serde(rename = "attributePatterns")] + pub attribute_patterns: Vec, + pub features: FilterFeatures, +} + +/// A filterable attribute definition, either a plain attribute name or a +/// settings object. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(untagged)] +pub enum FilterableAttribute { + Attribute(String), + Settings(FilterableAttributesSettings), +} + +impl From for FilterableAttribute { + fn from(value: String) -> Self { + FilterableAttribute::Attribute(value) + } +} + +impl From<&str> for FilterableAttribute { + fn from(value: &str) -> Self { + FilterableAttribute::Attribute(value.to_string()) + } +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub enum EmbedderSource { @@ -193,8 +242,10 @@ pub struct Settings { #[serde(skip_serializing_if = "Option::is_none")] pub ranking_rules: Option>, /// Attributes to use for [filtering](https://www.meilisearch.com/docs/learn/advanced/filtering). + /// + /// Supports both plain attribute names and settings objects. #[serde(skip_serializing_if = "Option::is_none")] - pub filterable_attributes: Option>, + pub filterable_attributes: Option>, /// Attributes to sort. #[serde(skip_serializing_if = "Option::is_none")] pub sortable_attributes: Option>, @@ -324,16 +375,29 @@ impl Settings { filterable_attributes: impl IntoIterator>, ) -> Settings { Settings { + // Legacy helper accepting a list of attribute names. filterable_attributes: Some( filterable_attributes .into_iter() - .map(|v| v.as_ref().to_string()) + .map(|v| FilterableAttribute::Attribute(v.as_ref().to_string())) .collect(), ), ..self } } + /// Set filterable attributes using mixed syntax. + #[must_use] + pub fn with_filterable_attributes_advanced( + self, + filterable_attributes: impl IntoIterator, + ) -> Settings { + Settings { + filterable_attributes: Some(filterable_attributes.into_iter().collect()), + ..self + } + } + #[must_use] pub fn with_sortable_attributes( self, @@ -714,6 +778,26 @@ impl Index { .await } + /// Get filterable attributes using mixed syntax. + /// + /// Returns a list that can contain plain attribute names (strings) and/or + /// settings objects. + pub async fn get_filterable_attributes_advanced( + &self, + ) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/filterable-attributes", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + /// Get [sortable attributes](https://www.meilisearch.com/docs/reference/api/settings#sortable-attributes) of the [Index]. /// /// # Example @@ -1507,9 +1591,10 @@ impl Index { &self, filterable_attributes: impl IntoIterator>, ) -> Result { + // Backward-compatible helper: accept a list of attribute names. self.client .http_client - .request::<(), Vec, TaskInfo>( + .request::<(), Vec, TaskInfo>( &format!( "{}/indexes/{}/settings/filterable-attributes", self.client.host, self.uid @@ -1518,7 +1603,7 @@ impl Index { query: (), body: filterable_attributes .into_iter() - .map(|v| v.as_ref().to_string()) + .map(|v| FilterableAttribute::Attribute(v.as_ref().to_string())) .collect(), }, 202, @@ -1526,6 +1611,27 @@ impl Index { .await } + /// Update filterable attributes using mixed syntax. + pub async fn set_filterable_attributes_advanced( + &self, + filterable_attributes: impl IntoIterator, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/filterable-attributes", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: filterable_attributes.into_iter().collect(), + }, + 202, + ) + .await + } + /// Update [sortable attributes](https://www.meilisearch.com/docs/reference/api/settings#sortable-attributes) of the [Index]. /// /// # Example From 873f11284655d21a7b988e76c693134a259ee951 Mon Sep 17 00:00:00 2001 From: Kumar Ujjawal Date: Mon, 10 Nov 2025 14:14:46 +0530 Subject: [PATCH 2/3] Fixed code samples for fileterable attributes --- .code-samples.meilisearch.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 400b3337..7a8e68cd 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -569,7 +569,7 @@ update_filterable_attributes_1: |- let task: TaskInfo = client .index("movies") - .set_filterable_attributes_advanced(&filterable_attributes) + .set_filterable_attributes_advanced(filterable_attributes) .await .unwrap(); reset_filterable_attributes_1: |- From 89f6c6b133fe079cfa348fe8b013fde2bc9e1cc9 Mon Sep 17 00:00:00 2001 From: Kumar Ujjawal Date: Mon, 10 Nov 2025 14:51:31 +0530 Subject: [PATCH 3/3] Added more tests for filterableAttribute --- src/settings.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/settings.rs b/src/settings.rs index 32bf2a3a..378f703d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2904,6 +2904,113 @@ mod tests { use crate::client::*; use meilisearch_test_macro::meilisearch_test; + use serde_json::{json, to_string}; + + #[test] + fn test_settings_with_filterable_attributes_advanced_builder() { + let attrs = vec![ + FilterableAttribute::from("author"), + FilterableAttribute::Settings(FilterableAttributesSettings { + attribute_patterns: vec!["genre".to_string()], + features: FilterFeatures { + facet_search: true, + filter: FilterFeatureModes { + equality: true, + comparison: false, + }, + }, + }), + ]; + + let settings = Settings::new().with_filterable_attributes_advanced(attrs.clone()); + assert_eq!(settings.filterable_attributes, Some(attrs)); + } + + #[meilisearch_test] + async fn test_set_filterable_attributes_advanced_request_body() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + + let index = client.index("test_filterable"); + + let payload = vec![ + FilterableAttribute::from("author"), + FilterableAttribute::Settings(FilterableAttributesSettings { + attribute_patterns: vec!["genre".to_string()], + features: FilterFeatures { + facet_search: true, + filter: FilterFeatureModes { + equality: true, + comparison: false, + }, + }, + }), + ]; + + let expected_body = to_string(&payload).unwrap(); + let path = "/indexes/test_filterable/settings/filterable-attributes"; + let mock_res = s + .mock("PUT", path) + .match_header("content-type", "application/json") + .match_body(expected_body.as_str()) + .with_status(202) + .create_async() + .await; + + let _ = index.set_filterable_attributes_advanced(payload).await; + mock_res.assert_async().await; + Ok(()) + } + + #[meilisearch_test] + async fn test_get_filterable_attributes_advanced_response() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let index = client.index("test_filterable"); + + let body = json!([ + "author", + { + "attributePatterns": ["genre"], + "features": { + "facetSearch": true, + "filter": { "equality": true, "comparison": false } + } + } + ]); + + let path = "/indexes/test_filterable/settings/filterable-attributes"; + let mock_res = s + .mock("GET", path) + .with_status(200) + .with_body(body.to_string()) + .create_async() + .await; + + let attrs = index.get_filterable_attributes_advanced().await.unwrap(); + mock_res.assert_async().await; + + assert_eq!( + attrs, + vec![ + FilterableAttribute::Attribute("author".to_string()), + FilterableAttribute::Settings(FilterableAttributesSettings { + attribute_patterns: vec!["genre".to_string()], + features: FilterFeatures { + facet_search: true, + filter: FilterFeatureModes { + equality: true, + comparison: false, + }, + }, + }), + ] + ); + + Ok(()) + } #[meilisearch_test] async fn test_set_faceting_settings(client: Client, index: Index) {