diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 6b401409..45801224 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1626,3 +1626,20 @@ reset_proximity_precision_settings_1: |- .reset_proximity_precision() .await .unwrap(); +facet_search_1: |- + let client = client::new("http://localhost:7700", Some("apiKey")); + let res = client.index("books") + .facet_search("genres") + .with_facet_query("fiction") + .with_filter("rating > 3") + .execute() + .await + .unwrap(); +facet_search_3: |- + let client = client::new("http://localhost:7700", Some("apiKey")); + let res = client.index("books") + .facet_search("genres") + .with_facet_query("c") + .execute() + .await + .unwrap(); diff --git a/src/indexes.rs b/src/indexes.rs index 05c728f1..1f4285bd 100644 --- a/src/indexes.rs +++ b/src/indexes.rs @@ -279,6 +279,56 @@ impl Index { SearchQuery::new(self) } + /// Returns the facet stats matching a specific query in the index. + /// + /// See also [`Index::facet_search`]. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, search::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug)] + /// struct Movie { + /// name: String, + /// genre: String, + /// } + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)); + /// let movies = client.index("execute_query"); + /// + /// // add some documents + /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), genre:String::from("scifi")},Movie{name:String::from("Inception"), genre:String::from("drama")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # movies.set_filterable_attributes(["genre"]).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let query = FacetSearchQuery::new(&movies, "genre").with_facet_query("scifi").build(); + /// let res = movies.execute_facet_query(&query).await.unwrap(); + /// + /// assert!(res.facet_hits.len() > 0); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn execute_facet_query( + &self, + body: &FacetSearchQuery<'_>, + ) -> Result { + request::<(), &FacetSearchQuery, FacetSearchResponse>( + &format!("{}/indexes/{}/facet-search", self.client.host, self.uid), + self.client.get_api_key(), + Method::Post { body, query: () }, + 200, + ) + .await + } + + pub fn facet_search<'a>(&'a self, facet_name: &'a str) -> FacetSearchQuery<'a> { + FacetSearchQuery::new(self, facet_name) + } + /// Get one document using its unique id. /// /// Serde is needed. Add `serde = {version="1.0", features=["derive"]}` in the dependencies section of your Cargo.toml. diff --git a/src/search.rs b/src/search.rs index ec96d9d7..50f2324b 100644 --- a/src/search.rs +++ b/src/search.rs @@ -601,6 +601,153 @@ pub struct MultiSearchResponse { pub results: Vec>, } +/// A struct representing a facet-search query. +/// +/// You can add search parameters using the builder syntax. +/// +/// See [this page](https://www.meilisearch.com/docs/reference/api/facet_search) for the official list and description of all parameters. +/// +/// # Examples +/// +/// ``` +/// # use serde::{Serialize, Deserialize}; +/// # use meilisearch_sdk::{client::*, indexes::*, search::*}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// #[derive(Serialize)] +/// struct Movie { +/// name: String, +/// genre: String, +/// } +/// # futures::executor::block_on(async move { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)); +/// let movies = client.index("execute_query"); +/// +/// // add some documents +/// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), genre:String::from("scifi")},Movie{name:String::from("Inception"), genre:String::from("drama")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// # movies.set_filterable_attributes(["genre"]).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// +/// let query = FacetSearchQuery::new(&movies, "genre").with_facet_query("scifi").build(); +/// let res = movies.execute_facet_query(&query).await.unwrap(); +/// +/// assert!(res.facet_hits.len() > 0); +/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// # }); +/// ``` +/// +/// ``` +/// # use meilisearch_sdk::{Client, SearchQuery, Index}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)); +/// # let index = client.index("facet_search_query_builder_build"); +/// let query = index.facet_search("kind") +/// .with_facet_query("space") +/// .build(); // you can also execute() instead of build() +/// ``` + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FacetSearchQuery<'a> { + #[serde(skip_serializing)] + index: &'a Index, + /// The facet name to search values on. + pub facet_name: &'a str, + /// The search query for the facet values. + #[serde(skip_serializing_if = "Option::is_none")] + pub facet_query: Option<&'a str>, + /// The text that will be searched for among the documents. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "q")] + pub search_query: Option<&'a str>, + /// Filter applied to documents. + /// + /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/advanced/filtering) to learn the syntax. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option>, + /// Defines the strategy on how to handle search queries containing multiple words. + #[serde(skip_serializing_if = "Option::is_none")] + pub matching_strategy: Option, +} + +#[allow(missing_docs)] +impl<'a> FacetSearchQuery<'a> { + pub fn new(index: &'a Index, facet_name: &'a str) -> FacetSearchQuery<'a> { + FacetSearchQuery { + index, + facet_name, + facet_query: None, + search_query: None, + filter: None, + matching_strategy: None, + } + } + + pub fn with_facet_query<'b>( + &'b mut self, + facet_query: &'a str, + ) -> &'b mut FacetSearchQuery<'a> { + self.facet_query = Some(facet_query); + self + } + + pub fn with_search_query<'b>( + &'b mut self, + search_query: &'a str, + ) -> &'b mut FacetSearchQuery<'a> { + self.search_query = Some(search_query); + self + } + + pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut FacetSearchQuery<'a> { + self.filter = Some(Filter::new(Either::Left(filter))); + self + } + + pub fn with_array_filter<'b>( + &'b mut self, + filter: Vec<&'a str>, + ) -> &'b mut FacetSearchQuery<'a> { + self.filter = Some(Filter::new(Either::Right(filter))); + self + } + + pub fn with_matching_strategy<'b>( + &'b mut self, + matching_strategy: MatchingStrategies, + ) -> &'b mut FacetSearchQuery<'a> { + self.matching_strategy = Some(matching_strategy); + self + } + + pub fn build(&mut self) -> FacetSearchQuery<'a> { + self.clone() + } + + pub async fn execute(&'a self) -> Result { + self.index.execute_facet_query(self).await + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FacetHit { + pub value: String, + pub count: usize, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FacetSearchResponse { + pub facet_hits: Vec, + pub facet_query: Option, + pub processing_time_ms: usize, +} + #[cfg(test)] mod tests { use crate::{ @@ -1174,4 +1321,118 @@ mod tests { Ok(()) } + + #[meilisearch_test] + async fn test_facet_search_base(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let res = index.facet_search("kind").execute().await?; + assert_eq!(res.facet_hits.len(), 2); + Ok(()) + } + + #[meilisearch_test] + async fn test_facet_search_with_facet_query(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let res = index + .facet_search("kind") + .with_facet_query("title") + .execute() + .await?; + assert_eq!(res.facet_hits.len(), 1); + assert_eq!(res.facet_hits[0].value, "title"); + assert_eq!(res.facet_hits[0].count, 8); + Ok(()) + } + + #[meilisearch_test] + async fn test_facet_search_with_search_query( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let res = index + .facet_search("kind") + .with_search_query("Harry Potter") + .execute() + .await?; + assert_eq!(res.facet_hits.len(), 1); + assert_eq!(res.facet_hits[0].value, "title"); + assert_eq!(res.facet_hits[0].count, 7); + Ok(()) + } + + #[meilisearch_test] + async fn test_facet_search_with_filter(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let res = index + .facet_search("kind") + .with_filter("value = \"The Social Network\"") + .execute() + .await?; + assert_eq!(res.facet_hits.len(), 1); + assert_eq!(res.facet_hits[0].value, "title"); + assert_eq!(res.facet_hits[0].count, 1); + + let res = index + .facet_search("kind") + .with_filter("NOT value = \"The Social Network\"") + .execute() + .await?; + assert_eq!(res.facet_hits.len(), 2); + Ok(()) + } + + #[meilisearch_test] + async fn test_facet_search_with_array_filter( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let res = index + .facet_search("kind") + .with_array_filter(vec![ + "value = \"The Social Network\"", + "value = \"The Social Network\"", + ]) + .execute() + .await?; + assert_eq!(res.facet_hits.len(), 1); + assert_eq!(res.facet_hits[0].value, "title"); + assert_eq!(res.facet_hits[0].count, 1); + Ok(()) + } + + #[meilisearch_test] + async fn test_facet_search_with_matching_strategy_all( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let res = index + .facet_search("kind") + .with_search_query("Harry Styles") + .with_matching_strategy(MatchingStrategies::ALL) + .execute() + .await?; + assert_eq!(res.facet_hits.len(), 0); + Ok(()) + } + + #[meilisearch_test] + async fn test_facet_search_with_matching_strategy_last( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let res = index + .facet_search("kind") + .with_search_query("Harry Styles") + .with_matching_strategy(MatchingStrategies::LAST) + .execute() + .await?; + assert_eq!(res.facet_hits.len(), 1); + assert_eq!(res.facet_hits[0].value, "title"); + assert_eq!(res.facet_hits[0].count, 7); + Ok(()) + } }