diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 42e528a4..b6d9ce05 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -570,14 +570,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 90beefee..66d1521c 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -749,9 +749,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 84262a18..4695f2ac 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 { @@ -207,8 +256,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>, @@ -338,16 +389,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, @@ -728,6 +792,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 @@ -1521,9 +1605,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 @@ -1532,7 +1617,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, @@ -1540,6 +1625,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 @@ -2812,7 +2918,113 @@ mod tests { use crate::client::*; use meilisearch_test_macro::meilisearch_test; - use serde_json::json; + 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) {