Skip to content

Commit cd3021c

Browse files
authored
Add filter parameter on get_documents for Meilisearch v1.2 (#473)
1 parent 70043d5 commit cd3021c

File tree

4 files changed

+234
-11
lines changed

4 files changed

+234
-11
lines changed

src/documents.rs

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ pub struct DocumentsQuery<'a> {
185185
/// The fields that should appear in the documents. By default all of the fields are present.
186186
#[serde(skip_serializing_if = "Option::is_none")]
187187
pub fields: Option<Vec<&'a str>>,
188+
189+
/// Filters to apply.
190+
///
191+
/// Available since v1.2 of Meilisearch
192+
/// Read the [dedicated guide](https://docs.meilisearch.com/reference/features/filtering.html) to learn the syntax.
193+
#[serde(skip_serializing_if = "Option::is_none")]
194+
pub filter: Option<&'a str>,
188195
}
189196

190197
impl<'a> DocumentsQuery<'a> {
@@ -194,6 +201,7 @@ impl<'a> DocumentsQuery<'a> {
194201
offset: None,
195202
limit: None,
196203
fields: None,
204+
filter: None,
197205
}
198206
}
199207

@@ -264,6 +272,11 @@ impl<'a> DocumentsQuery<'a> {
264272
self
265273
}
266274

275+
pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentsQuery<'a> {
276+
self.filter = Some(filter);
277+
self
278+
}
279+
267280
/// Execute the get documents query.
268281
///
269282
/// # Example
@@ -304,8 +317,7 @@ impl<'a> DocumentsQuery<'a> {
304317
#[cfg(test)]
305318
mod tests {
306319
use super::*;
307-
use crate::{client::*, indexes::*};
308-
use ::meilisearch_sdk::documents::IndexConfig;
320+
use crate::{client::*, errors::*, indexes::*};
309321
use meilisearch_test_macro::meilisearch_test;
310322
use serde::{Deserialize, Serialize};
311323

@@ -407,6 +419,116 @@ mod tests {
407419
Ok(())
408420
}
409421

422+
#[meilisearch_test]
423+
async fn test_get_documents_with_filter(client: Client, index: Index) -> Result<(), Error> {
424+
setup_test_index(&client, &index).await?;
425+
426+
index
427+
.set_filterable_attributes(["id"])
428+
.await
429+
.unwrap()
430+
.wait_for_completion(&client, None, None)
431+
.await
432+
.unwrap();
433+
434+
let documents = DocumentsQuery::new(&index)
435+
.with_filter("id = 1")
436+
.execute::<MyObject>()
437+
.await?;
438+
439+
assert_eq!(documents.results.len(), 1);
440+
441+
Ok(())
442+
}
443+
444+
#[meilisearch_test]
445+
async fn test_get_documents_with_error_hint() -> Result<(), Error> {
446+
let url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
447+
let client = Client::new(format!("{}/hello", url), Some("masterKey"));
448+
let index = client.index("test_get_documents_with_filter_wrong_ms_version");
449+
450+
let documents = DocumentsQuery::new(&index)
451+
.with_filter("id = 1")
452+
.execute::<MyObject>()
453+
.await;
454+
455+
let error = documents.unwrap_err();
456+
457+
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());
458+
let url = "http://localhost:7700/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch".to_string();
459+
let status_code = 404;
460+
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";
461+
462+
match &error {
463+
Error::MeilisearchCommunication(error) => {
464+
assert_eq!(error.status_code, status_code);
465+
assert_eq!(error.message, message);
466+
assert_eq!(error.url, url);
467+
}
468+
_ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."),
469+
};
470+
assert_eq!(format!("{}", error), displayed_error);
471+
472+
Ok(())
473+
}
474+
475+
#[meilisearch_test]
476+
async fn test_get_documents_with_error_hint_meilisearch_api_error(
477+
index: Index,
478+
client: Client,
479+
) -> Result<(), Error> {
480+
setup_test_index(&client, &index).await?;
481+
482+
let error = DocumentsQuery::new(&index)
483+
.with_filter("id = 1")
484+
.execute::<MyObject>()
485+
.await
486+
.unwrap_err();
487+
488+
let message = "Attribute `id` is not filterable. This index does not have configured filterable attributes.
489+
1:3 id = 1
490+
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();
491+
let displayed_error = "Meilisearch invalid_request: invalid_document_filter: Attribute `id` is not filterable. This index does not have configured filterable attributes.
492+
1:3 id = 1
493+
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";
494+
495+
match &error {
496+
Error::Meilisearch(error) => {
497+
assert_eq!(error.error_message, message);
498+
}
499+
_ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."),
500+
};
501+
assert_eq!(format!("{}", error), displayed_error);
502+
503+
Ok(())
504+
}
505+
506+
#[meilisearch_test]
507+
async fn test_get_documents_with_invalid_filter(
508+
client: Client,
509+
index: Index,
510+
) -> Result<(), Error> {
511+
setup_test_index(&client, &index).await?;
512+
513+
// Does not work because `id` is not filterable
514+
let error = DocumentsQuery::new(&index)
515+
.with_filter("id = 1")
516+
.execute::<MyObject>()
517+
.await
518+
.unwrap_err();
519+
520+
assert!(matches!(
521+
error,
522+
Error::Meilisearch(MeilisearchError {
523+
error_code: ErrorCode::InvalidDocumentFilter,
524+
error_type: ErrorType::InvalidRequest,
525+
..
526+
})
527+
));
528+
529+
Ok(())
530+
}
531+
410532
#[meilisearch_test]
411533
async fn test_settings_generated_by_macro(client: Client, index: Index) -> Result<(), Error> {
412534
setup_test_index(&client, &index).await?;

src/errors.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub enum Error {
1111
/// Also check out: <https://github.com/meilisearch/Meilisearch/blob/main/meilisearch-error/src/lib.rs>
1212
#[error(transparent)]
1313
Meilisearch(#[from] MeilisearchError),
14+
#[error(transparent)]
15+
MeilisearchCommunication(#[from] MeilisearchCommunicationError),
1416
/// There is no Meilisearch server listening on the [specified host]
1517
/// (../client/struct.Client.html#method.new).
1618
#[error("The Meilisearch server can't be reached.")]
@@ -65,6 +67,30 @@ pub enum Error {
6567
InvalidUuid4Version,
6668
}
6769

70+
#[derive(Debug, Clone, Deserialize, Error)]
71+
#[serde(rename_all = "camelCase")]
72+
73+
pub struct MeilisearchCommunicationError {
74+
pub status_code: u16,
75+
pub message: Option<String>,
76+
pub url: String,
77+
}
78+
79+
impl std::fmt::Display for MeilisearchCommunicationError {
80+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81+
write!(
82+
f,
83+
"MeilisearchCommunicationError: The server responded with a {}.",
84+
self.status_code
85+
)?;
86+
if let Some(message) = &self.message {
87+
write!(f, " {}", message)?;
88+
}
89+
write!(f, "\nurl: {}", self.url)?;
90+
Ok(())
91+
}
92+
}
93+
6894
#[derive(Debug, Clone, Deserialize, Error)]
6995
#[serde(rename_all = "camelCase")]
7096
#[error("Meilisearch {}: {}: {}. {}", .error_type, .error_code, .error_message, .error_link)]
@@ -162,6 +188,8 @@ pub enum ErrorCode {
162188
InvalidIndexOffset,
163189
InvalidIndexLimit,
164190
InvalidIndexPrimaryKey,
191+
InvalidDocumentFilter,
192+
MissingDocumentFilter,
165193
InvalidDocumentFields,
166194
InvalidDocumentLimit,
167195
InvalidDocumentOffset,
@@ -234,6 +262,8 @@ pub enum ErrorCode {
234262
Unknown,
235263
}
236264

265+
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";
266+
237267
impl std::fmt::Display for ErrorCode {
238268
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
239269
write!(
@@ -311,6 +341,28 @@ mod test {
311341

312342
assert_eq!(error.to_string(), ("Meilisearch internal: index_creation_failed: The cool error message.. https://the best link eveer"));
313343

344+
let error: MeilisearchCommunicationError = MeilisearchCommunicationError {
345+
status_code: 404,
346+
message: Some("Hint: something.".to_string()),
347+
url: "http://localhost:7700/something".to_string(),
348+
};
349+
350+
assert_eq!(
351+
error.to_string(),
352+
("MeilisearchCommunicationError: The server responded with a 404. Hint: something.\nurl: http://localhost:7700/something")
353+
);
354+
355+
let error: MeilisearchCommunicationError = MeilisearchCommunicationError {
356+
status_code: 404,
357+
message: None,
358+
url: "http://localhost:7700/something".to_string(),
359+
};
360+
361+
assert_eq!(
362+
error.to_string(),
363+
("MeilisearchCommunicationError: The server responded with a 404.\nurl: http://localhost:7700/something")
364+
);
365+
314366
let error = Error::UnreachableServer;
315367
assert_eq!(
316368
error.to_string(),

src/indexes.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
client::Client,
33
documents::{DocumentQuery, DocumentsQuery, DocumentsResults},
4-
errors::Error,
4+
errors::{Error, MeilisearchCommunicationError, MeilisearchError, MEILISEARCH_VERSION_HINT},
55
request::*,
66
search::*,
77
task_info::TaskInfo,
@@ -466,6 +466,39 @@ impl Index {
466466
&self,
467467
documents_query: &DocumentsQuery<'_>,
468468
) -> Result<DocumentsResults<T>, Error> {
469+
if documents_query.filter.is_some() {
470+
let url = format!("{}/indexes/{}/documents/fetch", self.client.host, self.uid);
471+
return request::<(), &DocumentsQuery, DocumentsResults<T>>(
472+
&url,
473+
self.client.get_api_key(),
474+
Method::Post {
475+
body: documents_query,
476+
query: (),
477+
},
478+
200,
479+
)
480+
.await
481+
.map_err(|err| match err {
482+
Error::MeilisearchCommunication(error) => {
483+
Error::MeilisearchCommunication(MeilisearchCommunicationError {
484+
status_code: error.status_code,
485+
url: error.url,
486+
message: Some(format!("{}.", MEILISEARCH_VERSION_HINT)),
487+
})
488+
}
489+
Error::Meilisearch(error) => Error::Meilisearch(MeilisearchError {
490+
error_code: error.error_code,
491+
error_link: error.error_link,
492+
error_type: error.error_type,
493+
error_message: format!(
494+
"{}\n{}.",
495+
error.error_message, MEILISEARCH_VERSION_HINT
496+
),
497+
}),
498+
_ => err,
499+
});
500+
}
501+
469502
let url = format!("{}/indexes/{}/documents", self.client.host, self.uid);
470503
request::<&DocumentsQuery, (), DocumentsResults<T>>(
471504
&url,

src/request.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::errors::{Error, MeilisearchError};
1+
use crate::errors::{Error, MeilisearchCommunicationError, MeilisearchError};
22
use log::{error, trace, warn};
33
use serde::{de::DeserializeOwned, Serialize};
44
use serde_json::{from_str, to_string};
@@ -116,7 +116,7 @@ pub(crate) async fn request<
116116
body = "null".to_string();
117117
}
118118

119-
parse_response(status, expected_status_code, body)
119+
parse_response(status, expected_status_code, body, url.to_string())
120120
}
121121

122122
#[cfg(not(target_arch = "wasm32"))]
@@ -214,7 +214,7 @@ pub(crate) async fn stream_request<
214214
body = "null".to_string();
215215
}
216216

217-
parse_response(status, expected_status_code, body)
217+
parse_response(status, expected_status_code, body, url.to_string())
218218
}
219219

220220
#[cfg(target_arch = "wasm32")]
@@ -318,9 +318,14 @@ pub(crate) async fn request<
318318

319319
if let Some(t) = text.as_string() {
320320
if t.is_empty() {
321-
parse_response(status, expected_status_code, String::from("null"))
321+
parse_response(
322+
status,
323+
expected_status_code,
324+
String::from("null"),
325+
url.to_string(),
326+
)
322327
} else {
323-
parse_response(status, expected_status_code, t)
328+
parse_response(status, expected_status_code, t, url.to_string())
324329
}
325330
} else {
326331
error!("Invalid response");
@@ -332,6 +337,7 @@ fn parse_response<Output: DeserializeOwned>(
332337
status_code: u16,
333338
expected_status_code: u16,
334339
body: String,
340+
url: String,
335341
) -> Result<Output, Error> {
336342
if status_code == expected_status_code {
337343
match from_str::<Output>(&body) {
@@ -345,16 +351,26 @@ fn parse_response<Output: DeserializeOwned>(
345351
}
346352
};
347353
}
348-
// TODO: create issue where it is clear what the HTTP error is
349-
// ParseError(Error("invalid type: null, expected struct MeilisearchError", line: 1, column: 4))
350354

351355
warn!(
352356
"Expected response code {}, got {}",
353357
expected_status_code, status_code
354358
);
359+
355360
match from_str::<MeilisearchError>(&body) {
356361
Ok(e) => Err(Error::from(e)),
357-
Err(e) => Err(Error::ParseError(e)),
362+
Err(e) => {
363+
if status_code >= 400 {
364+
return Err(Error::MeilisearchCommunication(
365+
MeilisearchCommunicationError {
366+
status_code,
367+
message: None,
368+
url,
369+
},
370+
));
371+
}
372+
Err(Error::ParseError(e))
373+
}
358374
}
359375
}
360376

0 commit comments

Comments
 (0)