Skip to content

Commit f7d92b1

Browse files
authored
Merge pull request #512 from hmacr/issue-503
Add facet_search API functionality
2 parents 90a153c + f3ad0e2 commit f7d92b1

File tree

3 files changed

+326
-0
lines changed

3 files changed

+326
-0
lines changed

.code-samples.meilisearch.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,21 @@ reset_proximity_precision_settings_1: |-
16631663
.reset_proximity_precision()
16641664
.await
16651665
.unwrap();
1666+
facet_search_1: |-
1667+
let client = client::new("http://localhost:7700", Some("apiKey"));
1668+
let res = client.index("books")
1669+
.facet_search("genres")
1670+
.with_facet_query("fiction")
1671+
.with_filter("rating > 3")
1672+
.execute()
1673+
.await
1674+
.unwrap();
1675+
facet_search_3: |-
1676+
let client = client::new("http://localhost:7700", Some("apiKey"));
1677+
let res = client.index("books")
1678+
.facet_search("genres")
1679+
.with_facet_query("c")
1680+
.execute()
16661681
get_search_cutoff_1: |-
16671682
let search_cutoff_ms: String = client
16681683
.index("movies")

src/indexes.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,56 @@ impl<Http: HttpClient> Index<Http> {
279279
SearchQuery::new(self)
280280
}
281281

282+
/// Returns the facet stats matching a specific query in the index.
283+
///
284+
/// See also [`Index::facet_search`].
285+
///
286+
/// # Example
287+
///
288+
/// ```
289+
/// # use serde::{Serialize, Deserialize};
290+
/// # use meilisearch_sdk::{client::*, indexes::*, search::*};
291+
/// #
292+
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
293+
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
294+
/// #
295+
/// #[derive(Serialize, Deserialize, Debug)]
296+
/// struct Movie {
297+
/// name: String,
298+
/// genre: String,
299+
/// }
300+
/// # futures::executor::block_on(async move {
301+
/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY));
302+
/// let movies = client.index("execute_query");
303+
///
304+
/// // add some documents
305+
/// # 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();
306+
/// # movies.set_filterable_attributes(["genre"]).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
307+
///
308+
/// let query = FacetSearchQuery::new(&movies, "genre").with_facet_query("scifi").build();
309+
/// let res = movies.execute_facet_query(&query).await.unwrap();
310+
///
311+
/// assert!(res.facet_hits.len() > 0);
312+
/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
313+
/// # });
314+
/// ```
315+
pub async fn execute_facet_query(
316+
&self,
317+
body: &FacetSearchQuery<'_>,
318+
) -> Result<FacetSearchResponse, Error> {
319+
request::<(), &FacetSearchQuery, FacetSearchResponse>(
320+
&format!("{}/indexes/{}/facet-search", self.client.host, self.uid),
321+
self.client.get_api_key(),
322+
Method::Post { body, query: () },
323+
200,
324+
)
325+
.await
326+
}
327+
328+
pub fn facet_search<'a>(&'a self, facet_name: &'a str) -> FacetSearchQuery<'a> {
329+
FacetSearchQuery::new(self, facet_name)
330+
}
331+
282332
/// Get one document using its unique id.
283333
///
284334
/// Serde is needed. Add `serde = {version="1.0", features=["derive"]}` in the dependencies section of your Cargo.toml.

src/search.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,153 @@ pub struct MultiSearchResponse<T> {
662662
pub results: Vec<SearchResults<T>>,
663663
}
664664

665+
/// A struct representing a facet-search query.
666+
///
667+
/// You can add search parameters using the builder syntax.
668+
///
669+
/// See [this page](https://www.meilisearch.com/docs/reference/api/facet_search) for the official list and description of all parameters.
670+
///
671+
/// # Examples
672+
///
673+
/// ```
674+
/// # use serde::{Serialize, Deserialize};
675+
/// # use meilisearch_sdk::{client::*, indexes::*, search::*};
676+
/// #
677+
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
678+
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
679+
/// #
680+
/// #[derive(Serialize)]
681+
/// struct Movie {
682+
/// name: String,
683+
/// genre: String,
684+
/// }
685+
/// # futures::executor::block_on(async move {
686+
/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY));
687+
/// let movies = client.index("execute_query");
688+
///
689+
/// // add some documents
690+
/// # 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();
691+
/// # movies.set_filterable_attributes(["genre"]).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
692+
///
693+
/// let query = FacetSearchQuery::new(&movies, "genre").with_facet_query("scifi").build();
694+
/// let res = movies.execute_facet_query(&query).await.unwrap();
695+
///
696+
/// assert!(res.facet_hits.len() > 0);
697+
/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
698+
/// # });
699+
/// ```
700+
///
701+
/// ```
702+
/// # use meilisearch_sdk::{Client, SearchQuery, Index};
703+
/// #
704+
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
705+
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
706+
/// #
707+
/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY));
708+
/// # let index = client.index("facet_search_query_builder_build");
709+
/// let query = index.facet_search("kind")
710+
/// .with_facet_query("space")
711+
/// .build(); // you can also execute() instead of build()
712+
/// ```
713+
714+
#[derive(Debug, Serialize, Clone)]
715+
#[serde(rename_all = "camelCase")]
716+
pub struct FacetSearchQuery<'a> {
717+
#[serde(skip_serializing)]
718+
index: &'a Index,
719+
/// The facet name to search values on.
720+
pub facet_name: &'a str,
721+
/// The search query for the facet values.
722+
#[serde(skip_serializing_if = "Option::is_none")]
723+
pub facet_query: Option<&'a str>,
724+
/// The text that will be searched for among the documents.
725+
#[serde(skip_serializing_if = "Option::is_none")]
726+
#[serde(rename = "q")]
727+
pub search_query: Option<&'a str>,
728+
/// Filter applied to documents.
729+
///
730+
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/advanced/filtering) to learn the syntax.
731+
#[serde(skip_serializing_if = "Option::is_none")]
732+
pub filter: Option<Filter<'a>>,
733+
/// Defines the strategy on how to handle search queries containing multiple words.
734+
#[serde(skip_serializing_if = "Option::is_none")]
735+
pub matching_strategy: Option<MatchingStrategies>,
736+
}
737+
738+
#[allow(missing_docs)]
739+
impl<'a> FacetSearchQuery<'a> {
740+
pub fn new(index: &'a Index, facet_name: &'a str) -> FacetSearchQuery<'a> {
741+
FacetSearchQuery {
742+
index,
743+
facet_name,
744+
facet_query: None,
745+
search_query: None,
746+
filter: None,
747+
matching_strategy: None,
748+
}
749+
}
750+
751+
pub fn with_facet_query<'b>(
752+
&'b mut self,
753+
facet_query: &'a str,
754+
) -> &'b mut FacetSearchQuery<'a> {
755+
self.facet_query = Some(facet_query);
756+
self
757+
}
758+
759+
pub fn with_search_query<'b>(
760+
&'b mut self,
761+
search_query: &'a str,
762+
) -> &'b mut FacetSearchQuery<'a> {
763+
self.search_query = Some(search_query);
764+
self
765+
}
766+
767+
pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut FacetSearchQuery<'a> {
768+
self.filter = Some(Filter::new(Either::Left(filter)));
769+
self
770+
}
771+
772+
pub fn with_array_filter<'b>(
773+
&'b mut self,
774+
filter: Vec<&'a str>,
775+
) -> &'b mut FacetSearchQuery<'a> {
776+
self.filter = Some(Filter::new(Either::Right(filter)));
777+
self
778+
}
779+
780+
pub fn with_matching_strategy<'b>(
781+
&'b mut self,
782+
matching_strategy: MatchingStrategies,
783+
) -> &'b mut FacetSearchQuery<'a> {
784+
self.matching_strategy = Some(matching_strategy);
785+
self
786+
}
787+
788+
pub fn build(&mut self) -> FacetSearchQuery<'a> {
789+
self.clone()
790+
}
791+
792+
pub async fn execute(&'a self) -> Result<FacetSearchResponse, Error> {
793+
self.index.execute_facet_query(self).await
794+
}
795+
}
796+
797+
#[derive(Debug, Deserialize)]
798+
#[serde(rename_all = "camelCase")]
799+
pub struct FacetHit {
800+
pub value: String,
801+
pub count: usize,
802+
}
803+
804+
#[derive(Debug, Deserialize)]
805+
#[serde(rename_all = "camelCase")]
806+
pub struct FacetSearchResponse {
807+
pub facet_hits: Vec<FacetHit>,
808+
pub facet_query: Option<String>,
809+
pub processing_time_ms: usize,
810+
}
811+
665812
#[cfg(test)]
666813
mod tests {
667814
use crate::{
@@ -1307,4 +1454,118 @@ mod tests {
13071454

13081455
Ok(())
13091456
}
1457+
1458+
#[meilisearch_test]
1459+
async fn test_facet_search_base(client: Client, index: Index) -> Result<(), Error> {
1460+
setup_test_index(&client, &index).await?;
1461+
let res = index.facet_search("kind").execute().await?;
1462+
assert_eq!(res.facet_hits.len(), 2);
1463+
Ok(())
1464+
}
1465+
1466+
#[meilisearch_test]
1467+
async fn test_facet_search_with_facet_query(client: Client, index: Index) -> Result<(), Error> {
1468+
setup_test_index(&client, &index).await?;
1469+
let res = index
1470+
.facet_search("kind")
1471+
.with_facet_query("title")
1472+
.execute()
1473+
.await?;
1474+
assert_eq!(res.facet_hits.len(), 1);
1475+
assert_eq!(res.facet_hits[0].value, "title");
1476+
assert_eq!(res.facet_hits[0].count, 8);
1477+
Ok(())
1478+
}
1479+
1480+
#[meilisearch_test]
1481+
async fn test_facet_search_with_search_query(
1482+
client: Client,
1483+
index: Index,
1484+
) -> Result<(), Error> {
1485+
setup_test_index(&client, &index).await?;
1486+
let res = index
1487+
.facet_search("kind")
1488+
.with_search_query("Harry Potter")
1489+
.execute()
1490+
.await?;
1491+
assert_eq!(res.facet_hits.len(), 1);
1492+
assert_eq!(res.facet_hits[0].value, "title");
1493+
assert_eq!(res.facet_hits[0].count, 7);
1494+
Ok(())
1495+
}
1496+
1497+
#[meilisearch_test]
1498+
async fn test_facet_search_with_filter(client: Client, index: Index) -> Result<(), Error> {
1499+
setup_test_index(&client, &index).await?;
1500+
let res = index
1501+
.facet_search("kind")
1502+
.with_filter("value = \"The Social Network\"")
1503+
.execute()
1504+
.await?;
1505+
assert_eq!(res.facet_hits.len(), 1);
1506+
assert_eq!(res.facet_hits[0].value, "title");
1507+
assert_eq!(res.facet_hits[0].count, 1);
1508+
1509+
let res = index
1510+
.facet_search("kind")
1511+
.with_filter("NOT value = \"The Social Network\"")
1512+
.execute()
1513+
.await?;
1514+
assert_eq!(res.facet_hits.len(), 2);
1515+
Ok(())
1516+
}
1517+
1518+
#[meilisearch_test]
1519+
async fn test_facet_search_with_array_filter(
1520+
client: Client,
1521+
index: Index,
1522+
) -> Result<(), Error> {
1523+
setup_test_index(&client, &index).await?;
1524+
let res = index
1525+
.facet_search("kind")
1526+
.with_array_filter(vec![
1527+
"value = \"The Social Network\"",
1528+
"value = \"The Social Network\"",
1529+
])
1530+
.execute()
1531+
.await?;
1532+
assert_eq!(res.facet_hits.len(), 1);
1533+
assert_eq!(res.facet_hits[0].value, "title");
1534+
assert_eq!(res.facet_hits[0].count, 1);
1535+
Ok(())
1536+
}
1537+
1538+
#[meilisearch_test]
1539+
async fn test_facet_search_with_matching_strategy_all(
1540+
client: Client,
1541+
index: Index,
1542+
) -> Result<(), Error> {
1543+
setup_test_index(&client, &index).await?;
1544+
let res = index
1545+
.facet_search("kind")
1546+
.with_search_query("Harry Styles")
1547+
.with_matching_strategy(MatchingStrategies::ALL)
1548+
.execute()
1549+
.await?;
1550+
assert_eq!(res.facet_hits.len(), 0);
1551+
Ok(())
1552+
}
1553+
1554+
#[meilisearch_test]
1555+
async fn test_facet_search_with_matching_strategy_last(
1556+
client: Client,
1557+
index: Index,
1558+
) -> Result<(), Error> {
1559+
setup_test_index(&client, &index).await?;
1560+
let res = index
1561+
.facet_search("kind")
1562+
.with_search_query("Harry Styles")
1563+
.with_matching_strategy(MatchingStrategies::LAST)
1564+
.execute()
1565+
.await?;
1566+
assert_eq!(res.facet_hits.len(), 1);
1567+
assert_eq!(res.facet_hits[0].value, "title");
1568+
assert_eq!(res.facet_hits[0].count, 7);
1569+
Ok(())
1570+
}
13101571
}

0 commit comments

Comments
 (0)