Skip to content

Commit

Permalink
Add federated multi search API
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasKalbertodt committed Dec 16, 2024
1 parent f4d5758 commit 7f65654
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 1 deletion.
31 changes: 31 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ impl<Http: HttpClient> Client<Http> {
.await
}

pub async fn execute_federated_multi_search_query<
T: 'static + DeserializeOwned + Send + Sync,
>(
&self,
body: &FederatedMultiSearchQuery<'_, '_, Http>,
) -> Result<FederatedMultiSearchResponse<T>, Error> {
self.http_client
.request::<(), &FederatedMultiSearchQuery<Http>, FederatedMultiSearchResponse<T>>(
&format!("{}/multi-search", &self.host),
Method::Post { body, query: () },
200,
)
.await
}

/// Make multiple search requests.
///
/// # Example
Expand Down Expand Up @@ -170,6 +185,22 @@ impl<Http: HttpClient> Client<Http> {
/// # 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<String, serde_json::Value>` 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<Http> {
MultiSearchQuery::new(self)
Expand Down
86 changes: 85 additions & 1 deletion src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub struct SearchResult<T> {
pub ranking_score: Option<f64>,
#[serde(rename = "_rankingScoreDetails")]
pub ranking_score_details: Option<Map<String, Value>>,
/// Only returned for federated multi search.
#[serde(rename = "_federation")]
pub federation: Option<FederationHitInfo>,
}

#[derive(Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -600,7 +603,6 @@ pub struct MultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> {
pub queries: Vec<SearchQuery<'b, Http>>,
}


#[allow(missing_docs)]
impl<'a, 'b, Http: HttpClient> MultiSearchQuery<'a, 'b, Http> {
#[must_use]
Expand All @@ -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<T: 'static + DeserializeOwned + Send + Sync>(
Expand All @@ -631,6 +644,77 @@ pub struct MultiSearchResponse<T> {
pub results: Vec<SearchResults<T>>,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FederatedMultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> {
#[serde(skip_serializing)]
client: &'a Client<Http>,
#[serde(bound(serialize = ""))]
pub queries: Vec<SearchQuery<'b, Http>>,
pub federation: Option<FederationOptions>,
}

/// 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<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub facets_by_index: Option<HashMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub merge_facets: Option<bool>,
}

#[allow(missing_docs)]
impl<'a, 'b, Http: HttpClient> FederatedMultiSearchQuery<'a, 'b, Http> {
/// Execute the query and fetch the results.
pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
&'a self,
) -> Result<FederatedMultiSearchResponse<T>, Error> {
self.client
.execute_federated_multi_search_query::<T>(self)
.await
}
}

/// Returned by federated multi search.
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FederatedMultiSearchResponse<T> {
/// Merged results of the query.
pub hits: Vec<SearchResult<T>>,

// 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<HashMap<String, HashMap<String, usize>>>,
/// facet stats of the numerical facets requested in the `facet` search parameter.
pub facet_stats: Option<HashMap<String, FacetStats>>,
/// 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::{
Expand Down

0 comments on commit 7f65654

Please sign in to comment.