Skip to content

Commit

Permalink
Merge #512
Browse files Browse the repository at this point in the history
512: Add facet_search API functionality r=curquiza a=hmacr

# Pull Request

## Related issue
Fixes #503 

## What does this PR do?
- Add functionality to use the facet-search API
- Add code samples for the new method

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?

Thank you so much for contributing to Meilisearch!


Co-authored-by: hmacr <[email protected]>
Co-authored-by: Clémentine U. - curqui <[email protected]>
  • Loading branch information
3 people authored Apr 15, 2024
2 parents cb32534 + a23fe34 commit fd31479
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 0 deletions.
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_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();
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(())
}
}

0 comments on commit fd31479

Please sign in to comment.