From 152cb7632750de2825fbe71d39c49876d7cca609 Mon Sep 17 00:00:00 2001 From: Azan Ali Date: Fri, 30 Jan 2026 16:24:55 +0500 Subject: [PATCH] add `export` module for the `/export` route --- docker-compose.yml | 12 ++ src/export.rs | 281 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/tasks.rs | 7 ++ 4 files changed, 302 insertions(+) create mode 100644 src/export.rs diff --git a/docker-compose.yml b/docker-compose.yml index 3546879e..520627c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,16 +13,28 @@ services: working_dir: /home/package environment: - MEILISEARCH_URL=http://meilisearch:7700 + - MEILISEARCH_EXPORT_URL=http://meilisearch_export:7700 - CARGO_HOME=/vendor/cargo depends_on: - meilisearch + - meilisearch_export links: - meilisearch + - meilisearch_export volumes: - ./:/home/package - cargo:/vendor/cargo meilisearch: + image: getmeili/meilisearch-enterprise:latest + command: meilisearch --experimental-allowed-ip-networks "172.16.0.0/12" #docker internal IPs + ports: + - "7700" + environment: + - MEILI_MASTER_KEY=masterKey + - MEILI_NO_ANALYTICS=true + + meilisearch_export: image: getmeili/meilisearch-enterprise:latest ports: - "7700" diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 00000000..8ed01985 --- /dev/null +++ b/src/export.rs @@ -0,0 +1,281 @@ +use std::collections::HashMap; + +use serde::Serialize; + +use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo}; + +/// Represents an export query, used to migrate between Meilisearch instances. +/// +/// Body fields can be added via the builder pattern. +/// See [this page](https://www.meilisearch.com/docs/reference/api/export) for details. +/// +/// # Example +/// +/// ``` +/// # use serde::{Serialize, Deserialize}; +/// # use meilisearch_sdk::{client::Client, export::*, indexes::Index, tasks::TaskType, task_info::*}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # let MEILISEARCH_EXPORT_URL = option_env!("MEILISEARCH_EXPORT_URL").unwrap_or("http://localhost:7701"); +/// # let MEILISEARCH_EXPORT_API_KEY = option_env!("MEILISEARCH_EXPORT_API_KEY").unwrap_or("masterKey"); +/// # +/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// let task_info = ExportQuery::new( +/// &client, +/// MEILISEARCH_EXPORT_URL, +/// MEILISEARCH_EXPORT_API_KEY, +/// ) +/// .with_payload_size("50 MiB") +/// .execute() +/// .await +/// .unwrap(); +/// +/// assert!(matches!( +/// task_info, +/// TaskInfo { +/// update_type: TaskType::Export { .. }, +/// .. +/// } +/// )); +/// # +/// # }); +/// ``` +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExportQuery<'a, Http: HttpClient> { + #[serde(skip_serializing)] + pub client: &'a Client, + pub url: String, + pub api_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub payload_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub indexes: Option>, +} + +/// Export options for indexes +#[derive(Debug, Serialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct ExportQueryIndexOptions { + /// a [filter expression](https://www.meilisearch.com/docs/learn/filtering_and_sorting/filter_expression_reference) defining the subset of documents to export. Defaults to `null` + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option, + + /// if `true`, configures indexes in the target instance with the origin instance settings. Defaults to `false` + #[serde(skip_serializing_if = "Option::is_none")] + pub override_settings: Option, +} + +impl<'a, Http: HttpClient> ExportQuery<'a, Http> { + /// Create a new `ExportQuery` + /// + /// See [this page](https://www.meilisearch.com/docs/reference/api/export) for details. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::Client, export::*, indexes::Index, tasks::TaskType, task_info::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let export_query = ExportQuery::new( + /// &client, + /// "https://some-remote-meilisearch.instance", + /// "masterKey", + /// ); + /// # }); + /// ``` + #[must_use] + pub fn new( + client: &'a Client, + target_url: impl AsRef, + target_api_key: impl AsRef, + ) -> Self { + Self { + client, + url: target_url.as_ref().to_string(), + api_key: target_api_key.as_ref().to_string(), + payload_size: None, + indexes: None, + } + } + + /// The maximum size of each single data payload in a human-readable format such as `"100MiB"`. + pub fn with_payload_size(&mut self, payload_size: impl AsRef) -> &mut Self { + self.payload_size = Some(payload_size.as_ref().to_string()); + + self + } + + /// Patterns matching indexes you want to export, along with export settings for each one + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::Client, export::*, indexes::Index, tasks::TaskType, task_info::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let export_query = ExportQuery::new( + /// &client, + /// "https://some-remote-meilisearch.instance", + /// "masterKey", + /// ) + /// .with_indexes([ + /// ("user_*", ExportQueryIndexOptions { + /// filter: Some("name EXISTS".to_string()), + /// override_settings: Some(false) + /// }), + /// ("movies", ExportQueryIndexOptions { + /// filter: Some("genres = horror OR genres = comedy".to_string()), + /// override_settings: Some(true) + /// }), + /// ("orders", ExportQueryIndexOptions::default()) + /// ]); + /// # }); + /// ``` + pub fn with_indexes( + &mut self, + indexes: impl IntoIterator, ExportQueryIndexOptions)>, + ) -> &mut Self { + self.indexes = Some( + indexes + .into_iter() + .map(|(index, options)| (index.as_ref().to_string(), options)) + .collect(), + ); + + self + } + + /// Execute the export query + /// + /// # Example + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::Client, export::*, indexes::Index, tasks::TaskType, task_info::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let MEILISEARCH_EXPORT_URL = option_env!("MEILISEARCH_EXPORT_URL").unwrap_or("http://localhost:7701"); + /// # let MEILISEARCH_EXPORT_API_KEY = option_env!("MEILISEARCH_EXPORT_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let task_info = ExportQuery::new( + /// &client, + /// MEILISEARCH_EXPORT_URL, + /// MEILISEARCH_EXPORT_API_KEY, + /// ) + /// .with_payload_size("50 MiB") + /// .execute() + /// .await + /// .unwrap(); + /// + /// assert!(matches!( + /// task_info, + /// TaskInfo { + /// update_type: TaskType::Export { .. }, + /// .. + /// } + /// )); + /// # + /// # }); + /// ``` + pub async fn execute(&self) -> Result { + self.client + .http_client + .request::<(), &Self, TaskInfo>( + &format!("{}/export", self.client.host), + Method::Post { + query: (), + body: self, + }, + 200, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{client::*, indexes::Index}; + use meilisearch_test_macro::meilisearch_test; + + fn get_export_client() -> Result { + let export_url = option_env!("MEILISEARCH_EXPORT_URL").unwrap_or("http://localhost:7701"); + let export_api_key = option_env!("MEILISEARCH_EXPORT_API_KEY").unwrap_or("masterKey"); + + Client::new(export_url, Some(export_api_key)) + } + + #[meilisearch_test] + async fn test_export_with_default_options(client: Client, index: Index) -> Result<(), Error> { + let export_client = get_export_client()?; + + let export_result = ExportQuery::new( + &client, + &export_client.host, + export_client.api_key.as_ref().unwrap(), + ) + .execute() + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await; + + assert!(export_result.is_ok()); + + let exported_index = export_client.get_index(&index.uid).await; + assert!(exported_index.is_ok()); + + export_client + .wait_for_task(exported_index.unwrap().delete().await.unwrap(), None, None) + .await + .unwrap(); + + Ok(()) + } + + #[meilisearch_test] + async fn test_export(client: Client, index: Index) -> Result<(), Error> { + let export_client = get_export_client()?; + + let export_result = ExportQuery::new( + &client, + &export_client.host, + export_client.api_key.as_ref().unwrap(), + ) + .with_indexes([(&index.uid, ExportQueryIndexOptions::default())]) + .with_payload_size("50 MiB") + .execute() + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await; + + assert!(export_result.is_ok()); + + let exported_index = export_client.get_index(&index.uid).await; + assert!(exported_index.is_ok()); + + export_client + .wait_for_task(exported_index.unwrap().delete().await.unwrap(), None, None) + .await + .unwrap(); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index eb0d182f..4fdcdd08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -242,6 +242,8 @@ pub mod documents; pub mod dumps; /// Module containing the [`errors::Error`] struct. pub mod errors; +/// Module containing [ExportQuery](export::ExportQuery). +pub mod export; /// Module related to runtime and instance features. pub mod features; /// Module containing the Index struct. diff --git a/src/tasks.rs b/src/tasks.rs index 34750de0..c8c16cb4 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -52,6 +52,9 @@ pub enum TaskType { IndexCompaction { details: Option, }, + Export { + details: Option, + }, } #[derive(Debug, Clone, Deserialize)] @@ -117,6 +120,10 @@ pub struct DumpCreation { pub dump_uid: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Export {} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IndexSwap {