From 7f656543daf8a0ca0ab1be4fcfe07749d96ef160 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Thu, 12 Dec 2024 13:41:56 +0100 Subject: [PATCH] Add federated multi search API Fixes #609 --- src/client.rs | 31 +++++++++++++++++++ src/search.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index c7d2ebea..061d5ddb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -128,6 +128,21 @@ impl Client { .await } + pub async fn execute_federated_multi_search_query< + T: 'static + DeserializeOwned + Send + Sync, + >( + &self, + body: &FederatedMultiSearchQuery<'_, '_, Http>, + ) -> Result, Error> { + self.http_client + .request::<(), &FederatedMultiSearchQuery, FederatedMultiSearchResponse>( + &format!("{}/multi-search", &self.host), + Method::Post { body, query: () }, + 200, + ) + .await + } + /// Make multiple search requests. /// /// # Example @@ -170,6 +185,22 @@ impl Client { /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` + /// + /// # Federated Search + /// + /// You can use [`MultiSearchQuery::with_federation`] to perform a [federated + /// search][1] where results from different indexes are merged and returned as + /// one list. + /// + /// When executing a federated query, the type parameter `T` is less clear, + /// as the documents in the different indexes potentially have different + /// fields and you might have one Rust type per index. In most cases, you + /// either want to create an enum with one variant per index and `#[serde + /// (untagged)]` attribute, or if you need more control, just pass + /// `serde_json::Map` and then deserialize that + /// into the appropriate target types later. + /// + /// [1]: https://www.meilisearch.com/docs/learn/multi_search/multi_search_vs_federated_search#what-is-federated-search #[must_use] pub fn multi_search(&self) -> MultiSearchQuery { MultiSearchQuery::new(self) diff --git a/src/search.rs b/src/search.rs index df23b048..2fe58ae1 100644 --- a/src/search.rs +++ b/src/search.rs @@ -55,6 +55,9 @@ pub struct SearchResult { pub ranking_score: Option, #[serde(rename = "_rankingScoreDetails")] pub ranking_score_details: Option>, + /// Only returned for federated multi search. + #[serde(rename = "_federation")] + pub federation: Option, } #[derive(Deserialize, Debug, Clone)] @@ -600,7 +603,6 @@ pub struct MultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> { pub queries: Vec>, } - #[allow(missing_docs)] impl<'a, 'b, Http: HttpClient> MultiSearchQuery<'a, 'b, Http> { #[must_use] @@ -618,6 +620,17 @@ impl<'a, 'b, Http: HttpClient> MultiSearchQuery<'a, 'b, Http> { self.queries.push(search_query); self } + /// Adds the `federation` parameter, making the search a federated search. + pub fn with_federation( + self, + federation: FederationOptions, + ) -> FederatedMultiSearchQuery<'a, 'b, Http> { + FederatedMultiSearchQuery { + client: self.client, + queries: self.queries, + federation: Some(federation), + } + } /// Execute the query and fetch the results. pub async fn execute( @@ -631,6 +644,77 @@ pub struct MultiSearchResponse { pub results: Vec>, } +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FederatedMultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> { + #[serde(skip_serializing)] + client: &'a Client, + #[serde(bound(serialize = ""))] + pub queries: Vec>, + pub federation: Option, +} + +/// The `federation` field of the multi search API. +/// See [the docs](https://www.meilisearch.com/docs/reference/api/multi_search#federation). +#[derive(Debug, Serialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct FederationOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub facets_by_index: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub merge_facets: Option, +} + +#[allow(missing_docs)] +impl<'a, 'b, Http: HttpClient> FederatedMultiSearchQuery<'a, 'b, Http> { + /// Execute the query and fetch the results. + pub async fn execute( + &'a self, + ) -> Result, Error> { + self.client + .execute_federated_multi_search_query::(self) + .await + } +} + +/// Returned by federated multi search. +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FederatedMultiSearchResponse { + /// Merged results of the query. + pub hits: Vec>, + + // TODO: are offset, limit and estimated_total_hits really non-optional? In + // my tests they are always returned, but that's not a proof. + /// Number of documents skipped. + pub offset: usize, + /// Number of results returned. + pub limit: usize, + /// Estimated total number of matches. + pub estimated_total_hits: usize, + + /// Distribution of the given facets. + pub facet_distribution: Option>>, + /// facet stats of the numerical facets requested in the `facet` search parameter. + pub facet_stats: Option>, + /// Processing time of the query. + pub processing_time_ms: usize, +} + +/// Returned for each hit in `_federation` when doing federated multi search. +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FederationHitInfo { + pub index_uid: String, + pub queries_position: usize, + // TOOD: not mentioned in the docs, is that optional? + pub weighted_ranking_score: f32, +} + #[cfg(test)] mod tests { use crate::{