From 7f400fb83b0f606b4ed0272316a097b358a030f9 Mon Sep 17 00:00:00 2001 From: meili-bot <74670311+meili-bot@users.noreply.github.com> Date: Fri, 12 May 2023 18:38:10 -0300 Subject: [PATCH 1/3] Add delete_document_with method for Meilisearch v1.2 --- src/documents.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++--- src/indexes.rs | 57 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/documents.rs b/src/documents.rs index 79c31856..61ccdf35 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -1,3 +1,4 @@ +use crate::task_info::TaskInfo; use async_trait::async_trait; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -301,11 +302,36 @@ impl<'a> DocumentsQuery<'a> { } } +#[derive(Debug, Clone, Serialize)] +pub struct DocumentDeletionQuery<'a> { + #[serde(skip_serializing)] + pub index: &'a Index, + + /// Filters to apply. + /// + /// Read the [dedicated guide](https://docs.meilisearch.com/reference/features/filtering.html) to learn the syntax. + pub filter: &'a str, +} + +impl<'a> DocumentDeletionQuery<'a> { + pub fn new(index: &Index) -> DocumentDeletionQuery { + DocumentDeletionQuery { index, filter: "" } + } + + pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentDeletionQuery<'a> { + self.filter = filter; + self + } + + pub async fn execute(&self) -> Result { + self.index.delete_documents_with(self).await + } +} + #[cfg(test)] mod tests { use super::*; - use crate::{client::*, indexes::*}; - use ::meilisearch_sdk::documents::IndexConfig; + use crate::{client::*, errors::*, indexes::*}; use meilisearch_test_macro::meilisearch_test; use serde::{Deserialize, Serialize}; @@ -371,7 +397,6 @@ mod tests { #[meilisearch_test] async fn test_get_documents_with_execute(client: Client, index: Index) -> Result<(), Error> { setup_test_index(&client, &index).await?; - // let documents = index.get_documents(None, None, None).await.unwrap(); let documents = DocumentsQuery::new(&index) .with_limit(1) .with_offset(1) @@ -387,6 +412,41 @@ mod tests { Ok(()) } + #[meilisearch_test] + async fn test_delete_documents_with(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + index + .set_filterable_attributes(["id"]) + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await + .unwrap(); + let mut query = DocumentDeletionQuery::new(&index); + query.with_filter("id = 1"); + + index + .delete_documents_with(&query) + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await + .unwrap(); + let document_result = index.get_document::("1").await; + + match document_result { + Ok(_) => panic!("The test was expecting no documents to be returned but got one."), + Err(e) => match e { + Error::Meilisearch(err) => { + assert_eq!(err.error_code, ErrorCode::DocumentNotFound); + } + _ => panic!("The error was expected to be a Meilisearch error, but it was not."), + }, + } + + Ok(()) + } + #[meilisearch_test] async fn test_get_documents_with_only_one_param( client: Client, diff --git a/src/indexes.rs b/src/indexes.rs index 05c118aa..804c0bf6 100644 --- a/src/indexes.rs +++ b/src/indexes.rs @@ -1,6 +1,6 @@ use crate::{ client::Client, - documents::{DocumentQuery, DocumentsQuery, DocumentsResults}, + documents::{DocumentDeletionQuery, DocumentQuery, DocumentsQuery, DocumentsResults}, errors::Error, request::*, search::*, @@ -926,6 +926,61 @@ impl Index { .await } + /// Delete a selection of documents with filters. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, documents::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Serialize, Deserialize, Debug)] + /// # struct Movie { + /// # name: String, + /// # id: String, + /// # } + /// # + /// # + /// # futures::executor::block_on(async move { + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)); + /// let index = client.index("delete_documents_with"); + /// # + /// # index.set_filterable_attributes(["id"]); + /// # // add some documents + /// # index.add_or_replace(&[Movie{id:String::from("1"), name: String::from("First movie") }, Movie{id:String::from("1"), name: String::from("First movie") }], Some("id")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let mut query = DocumentDeletionQuery::new(&index); + /// query.with_filter("id = 1"); + /// // delete some documents + /// index.delete_documents_with(&query) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn delete_documents_with( + &self, + query: &DocumentDeletionQuery<'_>, + ) -> Result { + request::<(), &DocumentDeletionQuery, TaskInfo>( + &format!("{}/indexes/{}/documents/delete", self.client.host, self.uid), + self.client.get_api_key(), + Method::Post { + query: (), + body: query, + }, + 202, + ) + .await + } + /// Alias for the [`Index::update`] method. pub async fn set_primary_key( &mut self, From cfde2b8b5e3aac0009724c478903b83aa3bca43b Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Mon, 22 May 2023 18:12:30 +0200 Subject: [PATCH 2/3] Add original_filter detail in DocumentDeletion task type --- src/tasks.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tasks.rs b/src/tasks.rs index 21a816f7..ff23d45d 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -66,6 +66,7 @@ pub struct DocumentAdditionOrUpdate { pub struct DocumentDeletion { pub provided_ids: Option, pub deleted_documents: Option, + pub original_filter: String, } #[derive(Debug, Clone, Deserialize)] From d525db95129ce45911285de30310153bfeecc84b Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 23 May 2023 11:54:47 +0200 Subject: [PATCH 3/3] Change filter to become optional in DocumentDeletionQuery --- src/documents.rs | 175 +++++++++++++++++++++++++++++++++++++++++++---- src/errors.rs | 52 ++++++++++++++ src/indexes.rs | 35 +++++++++- src/request.rs | 28 +++++--- src/tasks.rs | 2 +- 5 files changed, 270 insertions(+), 22 deletions(-) diff --git a/src/documents.rs b/src/documents.rs index 61ccdf35..20d77a23 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -186,6 +186,13 @@ pub struct DocumentsQuery<'a> { /// The fields that should appear in the documents. By default all of the fields are present. #[serde(skip_serializing_if = "Option::is_none")] pub fields: Option>, + + /// Filters to apply. + /// + /// Available since v1.2 of Meilisearch + /// Read the [dedicated guide](https://docs.meilisearch.com/reference/features/filtering.html) to learn the syntax. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option<&'a str>, } impl<'a> DocumentsQuery<'a> { @@ -195,6 +202,7 @@ impl<'a> DocumentsQuery<'a> { offset: None, limit: None, fields: None, + filter: None, } } @@ -265,6 +273,11 @@ impl<'a> DocumentsQuery<'a> { self } + pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentsQuery<'a> { + self.filter = Some(filter); + self + } + /// Execute the get documents query. /// /// # Example @@ -310,16 +323,19 @@ pub struct DocumentDeletionQuery<'a> { /// Filters to apply. /// /// Read the [dedicated guide](https://docs.meilisearch.com/reference/features/filtering.html) to learn the syntax. - pub filter: &'a str, + pub filter: Option<&'a str>, } impl<'a> DocumentDeletionQuery<'a> { pub fn new(index: &Index) -> DocumentDeletionQuery { - DocumentDeletionQuery { index, filter: "" } + DocumentDeletionQuery { + index, + filter: None, + } } pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentDeletionQuery<'a> { - self.filter = filter; + self.filter = Some(filter); self } @@ -417,21 +433,17 @@ mod tests { setup_test_index(&client, &index).await?; index .set_filterable_attributes(["id"]) - .await - .unwrap() + .await? .wait_for_completion(&client, None, None) - .await - .unwrap(); + .await?; + let mut query = DocumentDeletionQuery::new(&index); query.with_filter("id = 1"); - index .delete_documents_with(&query) - .await - .unwrap() + .await? .wait_for_completion(&client, None, None) - .await - .unwrap(); + .await?; let document_result = index.get_document::("1").await; match document_result { @@ -447,6 +459,35 @@ mod tests { Ok(()) } + #[meilisearch_test] + async fn test_delete_documents_with_filter_not_filterable( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = DocumentDeletionQuery::new(&index); + query.with_filter("id = 1"); + let error = index + .delete_documents_with(&query) + .await? + .wait_for_completion(&client, None, None) + .await?; + + let error = error.unwrap_failure(); + + assert!(matches!( + error, + MeilisearchError { + error_code: ErrorCode::InvalidDocumentFilter, + error_type: ErrorType::InvalidRequest, + .. + } + )); + + Ok(()) + } + #[meilisearch_test] async fn test_get_documents_with_only_one_param( client: Client, @@ -467,6 +508,116 @@ mod tests { Ok(()) } + #[meilisearch_test] + async fn test_get_documents_with_filter(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + index + .set_filterable_attributes(["id"]) + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await + .unwrap(); + + let documents = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await?; + + assert_eq!(documents.results.len(), 1); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_error_hint() -> Result<(), Error> { + let url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + let client = Client::new(format!("{}/hello", url), Some("masterKey")); + let index = client.index("test_get_documents_with_filter_wrong_ms_version"); + + let documents = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await; + + let error = documents.unwrap_err(); + + let message = Some("Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.".to_string()); + let url = "http://localhost:7700/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch".to_string(); + let status_code = 404; + let displayed_error = "MeilisearchCommunicationError: The server responded with a 404. Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.\nurl: http://localhost:7700/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch"; + + match &error { + Error::MeilisearchCommunication(error) => { + assert_eq!(error.status_code, status_code); + assert_eq!(error.message, message); + assert_eq!(error.url, url); + } + _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."), + }; + assert_eq!(format!("{}", error), displayed_error); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_error_hint_meilisearch_api_error( + index: Index, + client: Client, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let error = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await + .unwrap_err(); + + let message = "Attribute `id` is not filterable. This index does not have configured filterable attributes. +1:3 id = 1 +Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.".to_string(); + let displayed_error = "Meilisearch invalid_request: invalid_document_filter: Attribute `id` is not filterable. This index does not have configured filterable attributes. +1:3 id = 1 +Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.. https://docs.meilisearch.com/errors#invalid_document_filter"; + + match &error { + Error::Meilisearch(error) => { + assert_eq!(error.error_message, message); + } + _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."), + }; + assert_eq!(format!("{}", error), displayed_error); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_invalid_filter( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + // Does not work because `id` is not filterable + let error = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await + .unwrap_err(); + + assert!(matches!( + error, + Error::Meilisearch(MeilisearchError { + error_code: ErrorCode::InvalidDocumentFilter, + error_type: ErrorType::InvalidRequest, + .. + }) + )); + + Ok(()) + } + #[meilisearch_test] async fn test_settings_generated_by_macro(client: Client, index: Index) -> Result<(), Error> { setup_test_index(&client, &index).await?; diff --git a/src/errors.rs b/src/errors.rs index 8fcaa177..e0d04026 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,6 +11,8 @@ pub enum Error { /// Also check out: #[error(transparent)] Meilisearch(#[from] MeilisearchError), + #[error(transparent)] + MeilisearchCommunication(#[from] MeilisearchCommunicationError), /// There is no Meilisearch server listening on the [specified host] /// (../client/struct.Client.html#method.new). #[error("The Meilisearch server can't be reached.")] @@ -65,6 +67,30 @@ pub enum Error { InvalidUuid4Version, } +#[derive(Debug, Clone, Deserialize, Error)] +#[serde(rename_all = "camelCase")] + +pub struct MeilisearchCommunicationError { + pub status_code: u16, + pub message: Option, + pub url: String, +} + +impl std::fmt::Display for MeilisearchCommunicationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "MeilisearchCommunicationError: The server responded with a {}.", + self.status_code + )?; + if let Some(message) = &self.message { + write!(f, " {}", message)?; + } + write!(f, "\nurl: {}", self.url)?; + Ok(()) + } +} + #[derive(Debug, Clone, Deserialize, Error)] #[serde(rename_all = "camelCase")] #[error("Meilisearch {}: {}: {}. {}", .error_type, .error_code, .error_message, .error_link)] @@ -162,6 +188,8 @@ pub enum ErrorCode { InvalidIndexOffset, InvalidIndexLimit, InvalidIndexPrimaryKey, + InvalidDocumentFilter, + MissingDocumentFilter, InvalidDocumentFields, InvalidDocumentLimit, InvalidDocumentOffset, @@ -234,6 +262,8 @@ pub enum ErrorCode { Unknown, } +pub const MEILISEARCH_VERSION_HINT: &str = "Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method"; + impl std::fmt::Display for ErrorCode { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!( @@ -311,6 +341,28 @@ mod test { assert_eq!(error.to_string(), ("Meilisearch internal: index_creation_failed: The cool error message.. https://the best link eveer")); + let error: MeilisearchCommunicationError = MeilisearchCommunicationError { + status_code: 404, + message: Some("Hint: something.".to_string()), + url: "http://localhost:7700/something".to_string(), + }; + + assert_eq!( + error.to_string(), + ("MeilisearchCommunicationError: The server responded with a 404. Hint: something.\nurl: http://localhost:7700/something") + ); + + let error: MeilisearchCommunicationError = MeilisearchCommunicationError { + status_code: 404, + message: None, + url: "http://localhost:7700/something".to_string(), + }; + + assert_eq!( + error.to_string(), + ("MeilisearchCommunicationError: The server responded with a 404.\nurl: http://localhost:7700/something") + ); + let error = Error::UnreachableServer; assert_eq!( error.to_string(), diff --git a/src/indexes.rs b/src/indexes.rs index 804c0bf6..fee4e448 100644 --- a/src/indexes.rs +++ b/src/indexes.rs @@ -1,7 +1,7 @@ use crate::{ client::Client, documents::{DocumentDeletionQuery, DocumentQuery, DocumentsQuery, DocumentsResults}, - errors::Error, + errors::{Error, MeilisearchCommunicationError, MeilisearchError, MEILISEARCH_VERSION_HINT}, request::*, search::*, task_info::TaskInfo, @@ -466,6 +466,39 @@ impl Index { &self, documents_query: &DocumentsQuery<'_>, ) -> Result, Error> { + if documents_query.filter.is_some() { + let url = format!("{}/indexes/{}/documents/fetch", self.client.host, self.uid); + return request::<(), &DocumentsQuery, DocumentsResults>( + &url, + self.client.get_api_key(), + Method::Post { + body: documents_query, + query: (), + }, + 200, + ) + .await + .map_err(|err| match err { + Error::MeilisearchCommunication(error) => { + Error::MeilisearchCommunication(MeilisearchCommunicationError { + status_code: error.status_code, + url: error.url, + message: Some(format!("{}.", MEILISEARCH_VERSION_HINT)), + }) + } + Error::Meilisearch(error) => Error::Meilisearch(MeilisearchError { + error_code: error.error_code, + error_link: error.error_link, + error_type: error.error_type, + error_message: format!( + "{}\n{}.", + error.error_message, MEILISEARCH_VERSION_HINT + ), + }), + _ => err, + }); + } + let url = format!("{}/indexes/{}/documents", self.client.host, self.uid); request::<&DocumentsQuery, (), DocumentsResults>( &url, diff --git a/src/request.rs b/src/request.rs index 2a6e474b..eef9977f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,4 +1,4 @@ -use crate::errors::{Error, MeilisearchError}; +use crate::errors::{Error, MeilisearchCommunicationError, MeilisearchError}; use log::{error, trace, warn}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::{from_str, to_string}; @@ -116,7 +116,8 @@ pub(crate) async fn request< body = "null".to_string(); } - parse_response(status, expected_status_code, &body) + parse_response(status, expected_status_code, &body, url.to_string()) + // parse_response(status, expected_status_code, body) } #[cfg(not(target_arch = "wasm32"))] @@ -214,7 +215,7 @@ pub(crate) async fn stream_request< body = "null".to_string(); } - parse_response(status, expected_status_code, &body) + parse_response(status, expected_status_code, &body, url.to_string()) } #[cfg(target_arch = "wasm32")] @@ -318,9 +319,9 @@ pub(crate) async fn request< if let Some(t) = text.as_string() { if t.is_empty() { - parse_response(status, expected_status_code, "null") + parse_response(status, expected_status_code, "null", url.to_string()) } else { - parse_response(status, expected_status_code, &t) + parse_response(status, expected_status_code, &t, url.to_string()) } } else { error!("Invalid response"); @@ -332,6 +333,7 @@ fn parse_response( status_code: u16, expected_status_code: u16, body: &str, + url: String, ) -> Result { if status_code == expected_status_code { match from_str::(body) { @@ -345,16 +347,26 @@ fn parse_response( } }; } - // TODO: create issue where it is clear what the HTTP error is - // ParseError(Error("invalid type: null, expected struct MeilisearchError", line: 1, column: 4)) warn!( "Expected response code {}, got {}", expected_status_code, status_code ); + match from_str::(body) { Ok(e) => Err(Error::from(e)), - Err(e) => Err(Error::ParseError(e)), + Err(e) => { + if status_code >= 400 { + return Err(Error::MeilisearchCommunication( + MeilisearchCommunicationError { + status_code, + message: None, + url, + }, + )); + } + Err(Error::ParseError(e)) + } } } diff --git a/src/tasks.rs b/src/tasks.rs index ff23d45d..c1a781cb 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -66,7 +66,7 @@ pub struct DocumentAdditionOrUpdate { pub struct DocumentDeletion { pub provided_ids: Option, pub deleted_documents: Option, - pub original_filter: String, + pub original_filter: Option, } #[derive(Debug, Clone, Deserialize)]