Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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<FilterableAttribute> = 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: |-
Expand Down
6 changes: 5 additions & 1 deletion src/documents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
222 changes: 217 additions & 5 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,55 @@ pub struct FacetingSettings {
pub sort_facet_values_by: Option<BTreeMap<String, FacetSortValue>>,
}

/// 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<String>,
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<String> 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 {
Expand Down Expand Up @@ -207,8 +256,10 @@ pub struct Settings {
#[serde(skip_serializing_if = "Option::is_none")]
pub ranking_rules: Option<Vec<String>>,
/// 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<Vec<String>>,
pub filterable_attributes: Option<Vec<FilterableAttribute>>,
Comment on lines +259 to +262
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Verify migration path for breaking change to filterable_attributes field.

The type change from Option<Vec<String>> to Option<Vec<FilterableAttribute>> is a breaking change for code that directly accesses Settings.filterable_attributes. While the builder methods maintain backward compatibility, users who construct Settings via struct literals or access the field directly will need to update their code.

Consider:

  • Documenting this breaking change in the changelog/migration guide
  • Verifying that the most common usage patterns (via builder methods) remain unaffected

Run the following script to identify potential usage patterns in the codebase:


🏁 Script executed:

#!/bin/bash
# Find direct field access or struct construction patterns
rg -nP --type=rust -C3 'Settings\s*\{.*filterable_attributes' -g '!src/settings.rs'
rg -nP --type=rust -C3 '\.filterable_attributes\s*=' -g '!src/settings.rs'

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Broader search for filterable_attributes usage (any pattern)
echo "=== General usage of filterable_attributes ==="
rg -nP --type=rust 'filterable_attributes' -g '!src/settings.rs' | head -20

echo -e "\n=== Builder method patterns ==="
rg -nP --type=rust 'with_filterable_attributes|set_filterable_attributes' -C2

echo -e "\n=== Check for FilterableAttribute type definition ==="
rg -nP --type=rust 'struct FilterableAttribute|enum FilterableAttribute|type FilterableAttribute'

echo -e "\n=== Check settings.rs for builder methods ==="
rg -nP --type=rust -A5 'fn.*filterable_attributes' src/settings.rs

Length of output: 11128


Breaking change confirmed: direct field access to filterable_attributes will require updates.

Direct field access exists in the codebase (e.g., documents.rs lines 754, 760 access filterable_attributes directly). Code that constructs Settings via struct literals or directly accesses the field must be updated to work with Vec<FilterableAttribute> instead of Vec<String>.

The builder methods (with_filterable_attributes(), set_filterable_attributes()) maintain backward compatibility and continue accepting strings, so most typical usage patterns are unaffected. However, any code iterating over the field or relying on String methods will break. Consider documenting this as a breaking change in the migration guide.

/// Attributes to sort.
#[serde(skip_serializing_if = "Option::is_none")]
pub sortable_attributes: Option<Vec<String>>,
Expand Down Expand Up @@ -338,16 +389,29 @@ impl Settings {
filterable_attributes: impl IntoIterator<Item = impl AsRef<str>>,
) -> 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<Item = FilterableAttribute>,
) -> Settings {
Settings {
filterable_attributes: Some(filterable_attributes.into_iter().collect()),
..self
}
}

#[must_use]
pub fn with_sortable_attributes(
self,
Expand Down Expand Up @@ -728,6 +792,26 @@ impl<Http: HttpClient> Index<Http> {
.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<Vec<FilterableAttribute>, Error> {
self.client
.http_client
.request::<(), (), Vec<FilterableAttribute>>(
&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
Expand Down Expand Up @@ -1521,9 +1605,10 @@ impl<Http: HttpClient> Index<Http> {
&self,
filterable_attributes: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<TaskInfo, Error> {
// Backward-compatible helper: accept a list of attribute names.
self.client
.http_client
.request::<(), Vec<String>, TaskInfo>(
.request::<(), Vec<FilterableAttribute>, TaskInfo>(
&format!(
"{}/indexes/{}/settings/filterable-attributes",
self.client.host, self.uid
Expand All @@ -1532,14 +1617,35 @@ impl<Http: HttpClient> Index<Http> {
query: (),
body: filterable_attributes
.into_iter()
.map(|v| v.as_ref().to_string())
.map(|v| FilterableAttribute::Attribute(v.as_ref().to_string()))
.collect(),
},
202,
)
.await
}

/// Update filterable attributes using mixed syntax.
pub async fn set_filterable_attributes_advanced(
&self,
filterable_attributes: impl IntoIterator<Item = FilterableAttribute>,
) -> Result<TaskInfo, Error> {
self.client
.http_client
.request::<(), Vec<FilterableAttribute>, 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
Expand Down Expand Up @@ -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) {
Expand Down
Loading