From 40dd7ead01a96b3d6a9903d9fa4ccf7f3d5d1d0e Mon Sep 17 00:00:00 2001 From: hmacr Date: Tue, 5 Sep 2023 21:02:59 +0530 Subject: [PATCH 1/5] Add facet_search API functionality --- .code-samples.meilisearch.yaml | 17 +++ src/indexes.rs | 50 +++++++ src/search.rs | 261 +++++++++++++++++++++++++++++++++ 3 files changed, 328 insertions(+) diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index b840d99e..f8cbf144 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1656,3 +1656,20 @@ multi_search_1: |- .execute::() .await .unwrap(); +facet_search_1: |- + let client = client::new("http://localhost:7700", Some("apiKey")); + let res = client.index("movies") + .facet_search("genres") + .with_facet_query("fiction") + .with_filter("rating > 3") + .execute() + .await + .unwrap(); +facet_search_2: |- + let client = client::new("http://localhost:7700", Some("apiKey")); + let res = client.index("movies") + .facet_search("genres") + .with_facet_query("c") + .execute() + .await + .unwrap(); \ No newline at end of file diff --git a/src/indexes.rs b/src/indexes.rs index 069789e6..c6dd482c 100644 --- a/src/indexes.rs +++ b/src/indexes.rs @@ -268,6 +268,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 3f98ed41..a3d51313 100644 --- a/src/search.rs +++ b/src/search.rs @@ -539,6 +539,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::{client::*, search::*}; @@ -1113,4 +1260,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(()) + } } From fa60ea53e12fecf0b6160b57a00888d078de0861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9mentine=20U=2E=20-=20curqui?= Date: Tue, 12 Sep 2023 11:38:55 +0200 Subject: [PATCH 2/5] Update .code-samples.meilisearch.yaml --- .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 f8cbf144..304d1405 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1672,4 +1672,4 @@ facet_search_2: |- .with_facet_query("c") .execute() .await - .unwrap(); \ No newline at end of file + .unwrap(); From eb7c533116514138fbe262f134ad3a04145c7799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9mentine=20U=2E=20-=20curqui?= Date: Mon, 15 Apr 2024 19:49:38 +0200 Subject: [PATCH 3/5] Update .code-samples.meilisearch.yaml --- .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 fd6540df..c4bc3355 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1628,7 +1628,7 @@ reset_proximity_precision_settings_1: |- .unwrap(); facet_search_1: |- let client = client::new("http://localhost:7700", Some("apiKey")); - let res = client.index("movies") + let res = client.index("books") .facet_search("genres") .with_facet_query("fiction") .with_filter("rating > 3") From 5da8f05b99d54b76e0000b1178396112c2911d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9mentine=20U=2E=20-=20curqui?= Date: Mon, 15 Apr 2024 19:50:02 +0200 Subject: [PATCH 4/5] Update .code-samples.meilisearch.yaml --- .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 c4bc3355..2b93cb41 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1637,7 +1637,7 @@ facet_search_1: |- .unwrap(); facet_search_2: |- let client = client::new("http://localhost:7700", Some("apiKey")); - let res = client.index("movies") + let res = client.index("books") .facet_search("genres") .with_facet_query("c") .execute() From a23fe34e9525eef62b53f91172b060664ecf7707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9mentine=20U=2E=20-=20curqui?= Date: Mon, 15 Apr 2024 19:51:22 +0200 Subject: [PATCH 5/5] Update .code-samples.meilisearch.yaml --- .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 2b93cb41..45801224 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1635,7 +1635,7 @@ facet_search_1: |- .execute() .await .unwrap(); -facet_search_2: |- +facet_search_3: |- let client = client::new("http://localhost:7700", Some("apiKey")); let res = client.index("books") .facet_search("genres")