diff --git a/src/search.rs b/src/search.rs index 53e2b872..117c7528 100644 --- a/src/search.rs +++ b/src/search.rs @@ -115,6 +115,9 @@ pub struct SearchResults { pub facet_distribution: Option>>, /// facet stats of the numerical facets requested in the `facet` search parameter. pub facet_stats: Option>, + /// Indicates whether facet counts are exhaustive (exact) rather than estimated. + /// Present when the `exhaustiveFacetCount` search parameter is used. + pub exhaustive_facet_count: Option, /// Processing time of the query. pub processing_time_ms: usize, /// Query originating the response. @@ -408,6 +411,13 @@ pub struct SearchQuery<'a, Http: HttpClient> { #[serde(skip_serializing_if = "Option::is_none")] pub retrieve_vectors: Option, + /// Request exhaustive facet counts up to the limit defined by `maxTotalHits`. + /// + /// When set to `true`, Meilisearch computes exact facet counts instead of approximate ones. + /// Default is `false`. + #[serde(skip_serializing_if = "Option::is_none")] + pub exhaustive_facet_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) federation_options: Option, } @@ -453,6 +463,7 @@ impl<'a, Http: HttpClient> SearchQuery<'a, Http> { hybrid: None, vector: None, retrieve_vectors: None, + exhaustive_facet_count: None, distinct: None, ranking_score_threshold: None, locales: None, @@ -721,6 +732,15 @@ impl<'a, Http: HttpClient> SearchQuery<'a, Http> { self.clone() } + /// Request exhaustive facet count in the response. + pub fn with_exhaustive_facet_count<'b>( + &'b mut self, + exhaustive: bool, + ) -> &'b mut SearchQuery<'a, Http> { + self.exhaustive_facet_count = Some(exhaustive); + self + } + /// Execute the query and fetch the results. pub async fn execute( &'a self, @@ -1090,6 +1110,7 @@ pub struct FacetSearchResponse { #[cfg(test)] pub(crate) mod tests { + use crate::errors::{ErrorCode, MeilisearchError}; use crate::{ client::*, key::{Action, KeyBuilder}, @@ -1968,6 +1989,64 @@ pub(crate) mod tests { Ok(()) } + #[meilisearch_test] + async fn test_search_with_exhaustive_facet_count( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + // Request exhaustive facet counts for a specific facet and ensure the server + // returns the exhaustive flag in the response. + let mut query = SearchQuery::new(&index); + query + .with_facets(Selectors::Some(&["kind"])) + .with_exhaustive_facet_count(true); + + let res = index.execute_query::(&query).await; + match res { + Ok(results) => { + assert!(results.exhaustive_facet_count.is_some()); + Ok(()) + } + Err(error) + if matches!( + error, + Error::Meilisearch(MeilisearchError { + error_code: ErrorCode::BadRequest, + .. + }) + ) => + { + // Server doesn't support this field on /search yet; treat as a skip. + Ok(()) + } + Err(e) => Err(e), + } + } + + #[test] + fn test_search_query_serialization_exhaustive_facet_count() { + // Build a query and ensure it serializes using the expected camelCase field name + let client = Client::new( + option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"), + Some(option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey")), + ) + .unwrap(); + let index = client.index("dummy"); + + let mut query = SearchQuery::new(&index); + query + .with_facets(Selectors::Some(&["kind"])) + .with_exhaustive_facet_count(true); + + let v = serde_json::to_value(&query).unwrap(); + assert_eq!( + v.get("exhaustiveFacetCount").and_then(|b| b.as_bool()), + Some(true) + ); + } + #[meilisearch_test] async fn test_facet_search_with_facet_query(client: Client, index: Index) -> Result<(), Error> { setup_test_index(&client, &index).await?;