Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add facet_search API functionality #512

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
17 changes: 17 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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_2: |-
curquiza marked this conversation as resolved.
Show resolved Hide resolved
let client = client::new("http://localhost:7700", Some("apiKey"));
let res = client.index("movies")
curquiza marked this conversation as resolved.
Show resolved Hide resolved
.facet_search("genres")
.with_facet_query("c")
.execute()
.await
.unwrap();
50 changes: 50 additions & 0 deletions src/indexes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,56 @@ impl<Http: HttpClient> Index<Http> {
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<FacetSearchResponse, Error> {
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.
Expand Down
261 changes: 261 additions & 0 deletions src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,153 @@ pub struct MultiSearchResponse<T> {
pub results: Vec<SearchResults<T>>,
}

/// 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<Filter<'a>>,
/// Defines the strategy on how to handle search queries containing multiple words.
#[serde(skip_serializing_if = "Option::is_none")]
pub matching_strategy: Option<MatchingStrategies>,
}

#[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<FacetSearchResponse, Error> {
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<FacetHit>,
pub facet_query: Option<String>,
pub processing_time_ms: usize,
}

#[cfg(test)]
mod tests {
use crate::{
Expand Down Expand Up @@ -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(())
}
}
Loading