use async_trait::async_trait;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
pub use meilisearch_index_setting_macro::IndexConfig;
use crate::client::Client;
use crate::request::HttpClient;
use crate::settings::Settings;
use crate::task_info::TaskInfo;
use crate::tasks::Task;
use crate::{errors::Error, indexes::Index};
#[async_trait(?Send)]
pub trait IndexConfig {
const INDEX_STR: &'static str;
#[must_use]
fn index<Http: HttpClient>(client: &Client<Http>) -> Index<Http> {
client.index(Self::INDEX_STR)
}
fn generate_settings() -> Settings;
async fn generate_index<Http: HttpClient>(client: &Client<Http>) -> Result<Index<Http>, Task>;
}
#[derive(Debug, Clone, Deserialize)]
pub struct DocumentsResults<T> {
pub results: Vec<T>,
pub limit: u32,
pub offset: u32,
pub total: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct DocumentQuery<'a, Http: HttpClient> {
#[serde(skip_serializing)]
pub index: &'a Index<Http>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<&'a str>>,
}
impl<'a, Http: HttpClient> DocumentQuery<'a, Http> {
#[must_use]
pub fn new(index: &Index<Http>) -> DocumentQuery<'_, Http> {
DocumentQuery {
index,
fields: None,
}
}
pub fn with_fields(
&mut self,
fields: impl IntoIterator<Item = &'a str>,
) -> &mut DocumentQuery<'a, Http> {
self.fields = Some(fields.into_iter().collect());
self
}
pub async fn execute<T: DeserializeOwned + 'static + Send + Sync>(
&self,
document_id: &str,
) -> Result<T, Error> {
self.index.get_document_with::<T>(document_id, self).await
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DocumentsQuery<'a, Http: HttpClient> {
#[serde(skip_serializing)]
pub index: &'a Index<Http>,
#[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 fields: Option<Vec<&'a str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<Vec<&'a str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ids: Option<Vec<&'a str>>,
}
impl<'a, Http: HttpClient> DocumentsQuery<'a, Http> {
#[must_use]
pub fn new(index: &Index<Http>) -> DocumentsQuery<'_, Http> {
DocumentsQuery {
index,
offset: None,
limit: None,
fields: None,
sort: None,
filter: None,
ids: None,
}
}
pub fn with_offset(&mut self, offset: usize) -> &mut DocumentsQuery<'a, Http> {
self.offset = Some(offset);
self
}
pub fn with_limit(&mut self, limit: usize) -> &mut DocumentsQuery<'a, Http> {
self.limit = Some(limit);
self
}
pub fn with_fields(
&mut self,
fields: impl IntoIterator<Item = &'a str>,
) -> &mut DocumentsQuery<'a, Http> {
self.fields = Some(fields.into_iter().collect());
self
}
pub fn with_sort(
&mut self,
sort: impl IntoIterator<Item = &'a str>,
) -> &mut DocumentsQuery<'a, Http> {
self.sort = Some(sort.into_iter().collect());
self
}
pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentsQuery<'a, Http> {
self.filter = Some(filter);
self
}
pub fn with_ids(
&mut self,
ids: impl IntoIterator<Item = &'a str>,
) -> &mut DocumentsQuery<'a, Http> {
self.ids = Some(ids.into_iter().collect());
self
}
pub async fn execute<T: DeserializeOwned + 'static + Send + Sync>(
&self,
) -> Result<DocumentsResults<T>, Error> {
self.index.get_documents_with::<T>(self).await
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DocumentDeletionQuery<'a, Http: HttpClient> {
#[serde(skip_serializing)]
pub index: &'a Index<Http>,
pub filter: Option<&'a str>,
}
impl<'a, Http: HttpClient> DocumentDeletionQuery<'a, Http> {
#[must_use]
pub fn new(index: &Index<Http>) -> DocumentDeletionQuery<'_, Http> {
DocumentDeletionQuery {
index,
filter: None,
}
}
pub fn with_filter<'b>(
&'b mut self,
filter: &'a str,
) -> &'b mut DocumentDeletionQuery<'a, Http> {
self.filter = Some(filter);
self
}
pub async fn execute<T: DeserializeOwned + 'static>(&self) -> Result<TaskInfo, Error> {
self.index.delete_documents_with(self).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{client::Client, errors::*, indexes::*};
use meilisearch_test_macro::meilisearch_test;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct MyObject {
id: Option<usize>,
kind: String,
}
#[allow(unused)]
#[derive(IndexConfig)]
struct MovieClips {
#[index_config(primary_key)]
movie_id: u64,
#[index_config(distinct)]
owner: String,
#[index_config(displayed, searchable)]
title: String,
#[index_config(displayed)]
description: String,
#[index_config(filterable, sortable, displayed)]
release_date: String,
#[index_config(filterable, displayed)]
genres: Vec<String>,
}
#[allow(unused)]
#[derive(IndexConfig)]
struct VideoClips {
video_id: u64,
}
async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
let t0 = index
.add_documents(
&[
MyObject {
id: Some(0),
kind: "text".into(),
},
MyObject {
id: Some(1),
kind: "text".into(),
},
MyObject {
id: Some(2),
kind: "title".into(),
},
MyObject {
id: Some(3),
kind: "title".into(),
},
],
None,
)
.await?;
t0.wait_for_completion(client, None, None).await?;
Ok(())
}
#[meilisearch_test]
async fn test_get_documents_with_execute(client: Client, index: Index) -> Result<(), Error> {
setup_test_index(&client, &index).await?;
let documents = DocumentsQuery::new(&index)
.with_limit(1)
.with_offset(1)
.with_fields(["kind"])
.execute::<MyObject>()
.await
.unwrap();
assert_eq!(documents.limit, 1);
assert_eq!(documents.offset, 1);
assert_eq!(documents.results.len(), 1);
Ok(())
}
#[meilisearch_test]
async fn test_get_documents_by_ids(client: Client, index: Index) -> Result<(), Error> {
setup_test_index(&client, &index).await?;
let documents = DocumentsQuery::new(&index)
.with_ids(["1", "3"]) .execute::<MyObject>()
.await?;
assert_eq!(documents.results.len(), 2);
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?
.wait_for_completion(&client, None, None)
.await?;
let mut query = DocumentDeletionQuery::new(&index);
query.with_filter("id = 1");
index
.delete_documents_with(&query)
.await?
.wait_for_completion(&client, None, None)
.await?;
let document_result = index.get_document::<MyObject>("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_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,
index: Index,
) -> Result<(), Error> {
setup_test_index(&client, &index).await?;
let documents = DocumentsQuery::new(&index)
.with_limit(1)
.execute::<MyObject>()
.await
.unwrap();
assert_eq!(documents.limit, 1);
assert_eq!(documents.offset, 0);
assert_eq!(documents.results.len(), 1);
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::<MyObject>()
.await?;
assert_eq!(documents.results.len(), 1);
Ok(())
}
#[meilisearch_test]
async fn test_get_documents_with_sort(client: Client, index: Index) -> Result<(), Error> {
setup_test_index(&client, &index).await?;
index
.set_sortable_attributes(["id"])
.await?
.wait_for_completion(&client, None, None)
.await?;
let documents = DocumentsQuery::new(&index)
.with_sort(["id:desc"])
.execute::<MyObject>()
.await?;
assert_eq!(
documents.results.first().and_then(|document| document.id),
Some(3)
);
assert_eq!(
documents.results.last().and_then(|document| document.id),
Some(0)
);
Ok(())
}
#[meilisearch_test]
async fn test_get_documents_with_error_hint() -> Result<(), Error> {
let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
let client = Client::new(format!("{meilisearch_url}/hello"), Some("masterKey")).unwrap();
let index = client.index("test_get_documents_with_filter_wrong_ms_version");
let documents = DocumentsQuery::new(&index)
.with_filter("id = 1")
.execute::<MyObject>()
.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 = format!(
"{meilisearch_url}/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch"
);
let status_code = 404;
let displayed_error = format!("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: {meilisearch_url}/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::<MyObject>()
.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?;
let error = DocumentsQuery::new(&index)
.with_filter("id = 1")
.execute::<MyObject>()
.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?;
let movie_settings: Settings = MovieClips::generate_settings();
let video_settings: Settings = VideoClips::generate_settings();
assert_eq!(movie_settings.searchable_attributes.unwrap(), ["title"]);
assert!(video_settings.searchable_attributes.unwrap().is_empty());
assert_eq!(
movie_settings.displayed_attributes.unwrap(),
["title", "description", "release_date", "genres"]
);
assert!(video_settings.displayed_attributes.unwrap().is_empty());
use crate::settings::FilterableAttribute;
assert_eq!(
movie_settings.filterable_attributes.unwrap(),
vec![
FilterableAttribute::Attribute("release_date".to_string()),
FilterableAttribute::Attribute("genres".to_string()),
]
);
assert!(video_settings.filterable_attributes.unwrap().is_empty());
assert_eq!(
movie_settings.sortable_attributes.unwrap(),
["release_date"]
);
assert!(video_settings.sortable_attributes.unwrap().is_empty());
Ok(())
}
#[meilisearch_test]
async fn test_generate_index(client: Client) -> Result<(), Error> {
let index: Index = MovieClips::generate_index(&client).await.unwrap();
assert_eq!(index.uid, "movie_clips");
index
.delete()
.await?
.wait_for_completion(&client, None, None)
.await?;
Ok(())
}
#[derive(Serialize, Deserialize, IndexConfig)]
struct Movie {
#[index_config(primary_key)]
movie_id: u64,
#[index_config(displayed, searchable)]
title: String,
#[index_config(displayed)]
description: String,
#[index_config(filterable, sortable, displayed)]
release_date: String,
#[index_config(filterable, displayed)]
genres: Vec<String>,
}
}