Skip to content

Commit fd31479

Browse files
meili-bors[bot]hmacrcurquiza
authored
Merge #512
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]>
2 parents cb32534 + a23fe34 commit fd31479

File tree

3 files changed

+328
-0
lines changed

3 files changed

+328
-0
lines changed

.code-samples.meilisearch.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,3 +1626,20 @@ reset_proximity_precision_settings_1: |-
16261626
.reset_proximity_precision()
16271627
.await
16281628
.unwrap();
1629+
facet_search_1: |-
1630+
let client = client::new("http://localhost:7700", Some("apiKey"));
1631+
let res = client.index("books")
1632+
.facet_search("genres")
1633+
.with_facet_query("fiction")
1634+
.with_filter("rating > 3")
1635+
.execute()
1636+
.await
1637+
.unwrap();
1638+
facet_search_3: |-
1639+
let client = client::new("http://localhost:7700", Some("apiKey"));
1640+
let res = client.index("books")
1641+
.facet_search("genres")
1642+
.with_facet_query("c")
1643+
.execute()
1644+
.await
1645+
.unwrap();

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
@@ -601,6 +601,153 @@ pub struct MultiSearchResponse<T> {
601601
pub results: Vec<SearchResults<T>>,
602602
}
603603

604+
/// A struct representing a facet-search query.
605+
///
606+
/// You can add search parameters using the builder syntax.
607+
///
608+
/// See [this page](https://www.meilisearch.com/docs/reference/api/facet_search) for the official list and description of all parameters.
609+
///
610+
/// # Examples
611+
///
612+
/// ```
613+
/// # use serde::{Serialize, Deserialize};
614+
/// # use meilisearch_sdk::{client::*, indexes::*, search::*};
615+
/// #
616+
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
617+
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
618+
/// #
619+
/// #[derive(Serialize)]
620+
/// struct Movie {
621+
/// name: String,
622+
/// genre: String,
623+
/// }
624+
/// # futures::executor::block_on(async move {
625+
/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY));
626+
/// let movies = client.index("execute_query");
627+
///
628+
/// // add some documents
629+
/// # 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();
630+
/// # movies.set_filterable_attributes(["genre"]).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
631+
///
632+
/// let query = FacetSearchQuery::new(&movies, "genre").with_facet_query("scifi").build();
633+
/// let res = movies.execute_facet_query(&query).await.unwrap();
634+
///
635+
/// assert!(res.facet_hits.len() > 0);
636+
/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
637+
/// # });
638+
/// ```
639+
///
640+
/// ```
641+
/// # use meilisearch_sdk::{Client, SearchQuery, Index};
642+
/// #
643+
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
644+
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
645+
/// #
646+
/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY));
647+
/// # let index = client.index("facet_search_query_builder_build");
648+
/// let query = index.facet_search("kind")
649+
/// .with_facet_query("space")
650+
/// .build(); // you can also execute() instead of build()
651+
/// ```
652+
653+
#[derive(Debug, Serialize, Clone)]
654+
#[serde(rename_all = "camelCase")]
655+
pub struct FacetSearchQuery<'a> {
656+
#[serde(skip_serializing)]
657+
index: &'a Index,
658+
/// The facet name to search values on.
659+
pub facet_name: &'a str,
660+
/// The search query for the facet values.
661+
#[serde(skip_serializing_if = "Option::is_none")]
662+
pub facet_query: Option<&'a str>,
663+
/// The text that will be searched for among the documents.
664+
#[serde(skip_serializing_if = "Option::is_none")]
665+
#[serde(rename = "q")]
666+
pub search_query: Option<&'a str>,
667+
/// Filter applied to documents.
668+
///
669+
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/advanced/filtering) to learn the syntax.
670+
#[serde(skip_serializing_if = "Option::is_none")]
671+
pub filter: Option<Filter<'a>>,
672+
/// Defines the strategy on how to handle search queries containing multiple words.
673+
#[serde(skip_serializing_if = "Option::is_none")]
674+
pub matching_strategy: Option<MatchingStrategies>,
675+
}
676+
677+
#[allow(missing_docs)]
678+
impl<'a> FacetSearchQuery<'a> {
679+
pub fn new(index: &'a Index, facet_name: &'a str) -> FacetSearchQuery<'a> {
680+
FacetSearchQuery {
681+
index,
682+
facet_name,
683+
facet_query: None,
684+
search_query: None,
685+
filter: None,
686+
matching_strategy: None,
687+
}
688+
}
689+
690+
pub fn with_facet_query<'b>(
691+
&'b mut self,
692+
facet_query: &'a str,
693+
) -> &'b mut FacetSearchQuery<'a> {
694+
self.facet_query = Some(facet_query);
695+
self
696+
}
697+
698+
pub fn with_search_query<'b>(
699+
&'b mut self,
700+
search_query: &'a str,
701+
) -> &'b mut FacetSearchQuery<'a> {
702+
self.search_query = Some(search_query);
703+
self
704+
}
705+
706+
pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut FacetSearchQuery<'a> {
707+
self.filter = Some(Filter::new(Either::Left(filter)));
708+
self
709+
}
710+
711+
pub fn with_array_filter<'b>(
712+
&'b mut self,
713+
filter: Vec<&'a str>,
714+
) -> &'b mut FacetSearchQuery<'a> {
715+
self.filter = Some(Filter::new(Either::Right(filter)));
716+
self
717+
}
718+
719+
pub fn with_matching_strategy<'b>(
720+
&'b mut self,
721+
matching_strategy: MatchingStrategies,
722+
) -> &'b mut FacetSearchQuery<'a> {
723+
self.matching_strategy = Some(matching_strategy);
724+
self
725+
}
726+
727+
pub fn build(&mut self) -> FacetSearchQuery<'a> {
728+
self.clone()
729+
}
730+
731+
pub async fn execute(&'a self) -> Result<FacetSearchResponse, Error> {
732+
self.index.execute_facet_query(self).await
733+
}
734+
}
735+
736+
#[derive(Debug, Deserialize)]
737+
#[serde(rename_all = "camelCase")]
738+
pub struct FacetHit {
739+
pub value: String,
740+
pub count: usize,
741+
}
742+
743+
#[derive(Debug, Deserialize)]
744+
#[serde(rename_all = "camelCase")]
745+
pub struct FacetSearchResponse {
746+
pub facet_hits: Vec<FacetHit>,
747+
pub facet_query: Option<String>,
748+
pub processing_time_ms: usize,
749+
}
750+
604751
#[cfg(test)]
605752
mod tests {
606753
use crate::{
@@ -1174,4 +1321,118 @@ mod tests {
11741321

11751322
Ok(())
11761323
}
1324+
1325+
#[meilisearch_test]
1326+
async fn test_facet_search_base(client: Client, index: Index) -> Result<(), Error> {
1327+
setup_test_index(&client, &index).await?;
1328+
let res = index.facet_search("kind").execute().await?;
1329+
assert_eq!(res.facet_hits.len(), 2);
1330+
Ok(())
1331+
}
1332+
1333+
#[meilisearch_test]
1334+
async fn test_facet_search_with_facet_query(client: Client, index: Index) -> Result<(), Error> {
1335+
setup_test_index(&client, &index).await?;
1336+
let res = index
1337+
.facet_search("kind")
1338+
.with_facet_query("title")
1339+
.execute()
1340+
.await?;
1341+
assert_eq!(res.facet_hits.len(), 1);
1342+
assert_eq!(res.facet_hits[0].value, "title");
1343+
assert_eq!(res.facet_hits[0].count, 8);
1344+
Ok(())
1345+
}
1346+
1347+
#[meilisearch_test]
1348+
async fn test_facet_search_with_search_query(
1349+
client: Client,
1350+
index: Index,
1351+
) -> Result<(), Error> {
1352+
setup_test_index(&client, &index).await?;
1353+
let res = index
1354+
.facet_search("kind")
1355+
.with_search_query("Harry Potter")
1356+
.execute()
1357+
.await?;
1358+
assert_eq!(res.facet_hits.len(), 1);
1359+
assert_eq!(res.facet_hits[0].value, "title");
1360+
assert_eq!(res.facet_hits[0].count, 7);
1361+
Ok(())
1362+
}
1363+
1364+
#[meilisearch_test]
1365+
async fn test_facet_search_with_filter(client: Client, index: Index) -> Result<(), Error> {
1366+
setup_test_index(&client, &index).await?;
1367+
let res = index
1368+
.facet_search("kind")
1369+
.with_filter("value = \"The Social Network\"")
1370+
.execute()
1371+
.await?;
1372+
assert_eq!(res.facet_hits.len(), 1);
1373+
assert_eq!(res.facet_hits[0].value, "title");
1374+
assert_eq!(res.facet_hits[0].count, 1);
1375+
1376+
let res = index
1377+
.facet_search("kind")
1378+
.with_filter("NOT value = \"The Social Network\"")
1379+
.execute()
1380+
.await?;
1381+
assert_eq!(res.facet_hits.len(), 2);
1382+
Ok(())
1383+
}
1384+
1385+
#[meilisearch_test]
1386+
async fn test_facet_search_with_array_filter(
1387+
client: Client,
1388+
index: Index,
1389+
) -> Result<(), Error> {
1390+
setup_test_index(&client, &index).await?;
1391+
let res = index
1392+
.facet_search("kind")
1393+
.with_array_filter(vec![
1394+
"value = \"The Social Network\"",
1395+
"value = \"The Social Network\"",
1396+
])
1397+
.execute()
1398+
.await?;
1399+
assert_eq!(res.facet_hits.len(), 1);
1400+
assert_eq!(res.facet_hits[0].value, "title");
1401+
assert_eq!(res.facet_hits[0].count, 1);
1402+
Ok(())
1403+
}
1404+
1405+
#[meilisearch_test]
1406+
async fn test_facet_search_with_matching_strategy_all(
1407+
client: Client,
1408+
index: Index,
1409+
) -> Result<(), Error> {
1410+
setup_test_index(&client, &index).await?;
1411+
let res = index
1412+
.facet_search("kind")
1413+
.with_search_query("Harry Styles")
1414+
.with_matching_strategy(MatchingStrategies::ALL)
1415+
.execute()
1416+
.await?;
1417+
assert_eq!(res.facet_hits.len(), 0);
1418+
Ok(())
1419+
}
1420+
1421+
#[meilisearch_test]
1422+
async fn test_facet_search_with_matching_strategy_last(
1423+
client: Client,
1424+
index: Index,
1425+
) -> Result<(), Error> {
1426+
setup_test_index(&client, &index).await?;
1427+
let res = index
1428+
.facet_search("kind")
1429+
.with_search_query("Harry Styles")
1430+
.with_matching_strategy(MatchingStrategies::LAST)
1431+
.execute()
1432+
.await?;
1433+
assert_eq!(res.facet_hits.len(), 1);
1434+
assert_eq!(res.facet_hits[0].value, "title");
1435+
assert_eq!(res.facet_hits[0].count, 7);
1436+
Ok(())
1437+
}
11771438
}

0 commit comments

Comments
 (0)