meilisearch_sdk/
documents.rs

1use async_trait::async_trait;
2use serde::{de::DeserializeOwned, Deserialize, Serialize};
3
4/// Derive the [`IndexConfig`] trait.
5///
6/// ## Field attribute
7/// Use the `#[index_config(..)]` field attribute to generate the correct settings
8/// for each field. The available parameters are:
9/// - `primary_key` (can only be used once)
10/// - `distinct` (can only be used once)
11/// - `searchable`
12/// - `displayed`
13/// - `filterable`
14/// - `sortable`
15///
16/// ## Index name
17/// The name of the index will be the name of the struct converted to snake case.
18///
19/// ## Sample usage:
20/// ```
21/// use serde::{Serialize, Deserialize};
22/// use meilisearch_sdk::documents::IndexConfig;
23/// use meilisearch_sdk::settings::Settings;
24/// use meilisearch_sdk::indexes::Index;
25/// use meilisearch_sdk::client::Client;
26///
27/// #[derive(Serialize, Deserialize, IndexConfig)]
28/// struct Movie {
29///     #[index_config(primary_key)]
30///     movie_id: u64,
31///     #[index_config(displayed, searchable)]
32///     title: String,
33///     #[index_config(displayed)]
34///     description: String,
35///     #[index_config(filterable, sortable, displayed)]
36///     release_date: String,
37///     #[index_config(filterable, displayed)]
38///     genres: Vec<String>,
39/// }
40///
41/// async fn usage(client: Client) {
42///     // Default settings with the distinct, searchable, displayed, filterable, and sortable fields set correctly.
43///     let settings: Settings = Movie::generate_settings();
44///     // Index created with the name `movie` and the primary key set to `movie_id`
45///     let index: Index = Movie::generate_index(&client).await.unwrap();
46/// }
47/// ```
48pub use meilisearch_index_setting_macro::IndexConfig;
49
50use crate::client::Client;
51use crate::request::HttpClient;
52use crate::settings::Settings;
53use crate::task_info::TaskInfo;
54use crate::tasks::Task;
55use crate::{errors::Error, indexes::Index};
56
57#[async_trait(?Send)]
58pub trait IndexConfig {
59    const INDEX_STR: &'static str;
60
61    #[must_use]
62    fn index<Http: HttpClient>(client: &Client<Http>) -> Index<Http> {
63        client.index(Self::INDEX_STR)
64    }
65    fn generate_settings() -> Settings;
66    async fn generate_index<Http: HttpClient>(client: &Client<Http>) -> Result<Index<Http>, Task>;
67}
68
69#[derive(Debug, Clone, Deserialize)]
70pub struct DocumentsResults<T> {
71    pub results: Vec<T>,
72    pub limit: u32,
73    pub offset: u32,
74    pub total: u32,
75}
76
77#[derive(Debug, Clone, Serialize)]
78pub struct DocumentQuery<'a, Http: HttpClient> {
79    #[serde(skip_serializing)]
80    pub index: &'a Index<Http>,
81
82    /// The fields that should appear in the documents. By default, all of the fields are present.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub fields: Option<Vec<&'a str>>,
85}
86
87impl<'a, Http: HttpClient> DocumentQuery<'a, Http> {
88    #[must_use]
89    pub fn new(index: &Index<Http>) -> DocumentQuery<'_, Http> {
90        DocumentQuery {
91            index,
92            fields: None,
93        }
94    }
95
96    /// Specify the fields to return in the document.
97    ///
98    /// # Example
99    ///
100    /// ```
101    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
102    /// #
103    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
104    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
105    /// #
106    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
107    /// let index = client.index("document_query_with_fields");
108    /// let mut document_query = DocumentQuery::new(&index);
109    ///
110    /// document_query.with_fields(["title"]);
111    /// ```
112    pub fn with_fields(
113        &mut self,
114        fields: impl IntoIterator<Item = &'a str>,
115    ) -> &mut DocumentQuery<'a, Http> {
116        self.fields = Some(fields.into_iter().collect());
117        self
118    }
119
120    /// Execute the get document query.
121    ///
122    /// # Example
123    ///
124    /// ```
125    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
126    /// # use serde::{Deserialize, Serialize};
127    /// #
128    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
129    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
130    /// #
131    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
132    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
133    /// #[derive(Debug, Serialize, Deserialize, PartialEq)]
134    /// struct MyObject {
135    ///     id: String,
136    ///     kind: String,
137    /// }
138    ///
139    /// #[derive(Debug, Serialize, Deserialize, PartialEq)]
140    /// struct MyObjectReduced {
141    ///     id: String,
142    /// }
143    /// # let index = client.index("document_query_execute");
144    /// # index.add_or_replace(&[MyObject{id:"1".to_string(), kind:String::from("a kind")},MyObject{id:"2".to_string(), kind:String::from("some kind")}], None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
145    ///
146    /// let document = DocumentQuery::new(&index).with_fields(["id"])
147    ///     .execute::<MyObjectReduced>("1")
148    ///     .await
149    ///     .unwrap();
150    ///
151    /// assert_eq!(
152    ///     document,
153    ///     MyObjectReduced { id: "1".to_string() }
154    /// );
155    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
156    /// # });
157    pub async fn execute<T: DeserializeOwned + 'static + Send + Sync>(
158        &self,
159        document_id: &str,
160    ) -> Result<T, Error> {
161        self.index.get_document_with::<T>(document_id, self).await
162    }
163}
164
165#[derive(Debug, Clone, Serialize)]
166pub struct DocumentsQuery<'a, Http: HttpClient> {
167    #[serde(skip_serializing)]
168    pub index: &'a Index<Http>,
169
170    /// The number of documents to skip.
171    ///
172    /// If the value of the parameter `offset` is `n`, the `n` first documents will not be returned.
173    /// This is helpful for pagination.
174    ///
175    /// Example: If you want to skip the first document, set offset to `1`.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub offset: Option<usize>,
178
179    /// The maximum number of documents returned.
180    /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response.
181    /// This is helpful for pagination.
182    ///
183    /// Example: If you don't want to get more than two documents, set limit to `2`.
184    ///
185    /// **Default: `20`**
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub limit: Option<usize>,
188
189    /// The fields that should appear in the documents. By default, all of the fields are present.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub fields: Option<Vec<&'a str>>,
192
193    /// Attributes used to sort the returned documents.
194    ///
195    /// Available since v1.16 of Meilisearch.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub sort: Option<Vec<&'a str>>,
198
199    /// Filters to apply.
200    ///
201    /// Available since v1.2 of Meilisearch
202    /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub filter: Option<&'a str>,
205
206    /// Retrieve documents by their IDs.
207    ///
208    /// When `ids` is provided, the SDK will call the `/documents/fetch` endpoint with a POST request.
209    ///
210    /// Note: IDs are represented as strings to keep consistency with [`Index::get_document`]. If your IDs
211    /// are numeric, pass them as strings (e.g., `"1"`, `"2"`).
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub ids: Option<Vec<&'a str>>,
214}
215
216impl<'a, Http: HttpClient> DocumentsQuery<'a, Http> {
217    #[must_use]
218    pub fn new(index: &Index<Http>) -> DocumentsQuery<'_, Http> {
219        DocumentsQuery {
220            index,
221            offset: None,
222            limit: None,
223            fields: None,
224            sort: None,
225            filter: None,
226            ids: None,
227        }
228    }
229
230    /// Specify the offset.
231    ///
232    /// # Example
233    ///
234    /// ```
235    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
236    /// #
237    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
238    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
239    /// #
240    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
241    /// let index = client.index("my_index");
242    ///
243    /// let mut documents_query = DocumentsQuery::new(&index).with_offset(1);
244    /// ```
245    pub fn with_offset(&mut self, offset: usize) -> &mut DocumentsQuery<'a, Http> {
246        self.offset = Some(offset);
247        self
248    }
249
250    /// Specify the limit.
251    ///
252    /// # Example
253    ///
254    /// ```
255    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
256    /// #
257    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
258    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
259    /// #
260    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
261    /// let index = client.index("my_index");
262    ///
263    /// let mut documents_query = DocumentsQuery::new(&index);
264    ///
265    /// documents_query.with_limit(1);
266    /// ```
267    pub fn with_limit(&mut self, limit: usize) -> &mut DocumentsQuery<'a, Http> {
268        self.limit = Some(limit);
269        self
270    }
271
272    /// Specify the fields to return in the documents.
273    ///
274    /// # Example
275    ///
276    /// ```
277    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
278    /// #
279    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
280    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
281    /// #
282    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
283    /// let index = client.index("my_index");
284    ///
285    /// let mut documents_query = DocumentsQuery::new(&index);
286    ///
287    /// documents_query.with_fields(["title"]);
288    /// ```
289    pub fn with_fields(
290        &mut self,
291        fields: impl IntoIterator<Item = &'a str>,
292    ) -> &mut DocumentsQuery<'a, Http> {
293        self.fields = Some(fields.into_iter().collect());
294        self
295    }
296
297    /// Specify the sort order of the returned documents.
298    ///
299    /// # Example
300    ///
301    /// ```
302    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
303    /// #
304    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
305    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
306    /// #
307    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
308    /// let index = client.index("documents_with_sort");
309    ///
310    /// let mut documents_query = DocumentsQuery::new(&index);
311    ///
312    /// documents_query.with_sort(["release_date:desc"]);
313    /// ```
314    pub fn with_sort(
315        &mut self,
316        sort: impl IntoIterator<Item = &'a str>,
317    ) -> &mut DocumentsQuery<'a, Http> {
318        self.sort = Some(sort.into_iter().collect());
319        self
320    }
321
322    pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentsQuery<'a, Http> {
323        self.filter = Some(filter);
324        self
325    }
326
327    /// Specify a list of document IDs to retrieve.
328    ///
329    /// # Example
330    ///
331    /// ```
332    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
333    /// # use serde::{Deserialize, Serialize};
334    /// #
335    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
336    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
337    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
338    /// let index = client.index("get_documents_by_ids_example");
339    /// let mut query = DocumentsQuery::new(&index);
340    /// query.with_ids(["1", "2"]);
341    /// ```
342    pub fn with_ids(
343        &mut self,
344        ids: impl IntoIterator<Item = &'a str>,
345    ) -> &mut DocumentsQuery<'a, Http> {
346        self.ids = Some(ids.into_iter().collect());
347        self
348    }
349
350    /// Execute the get documents query.
351    ///
352    /// # Example
353    ///
354    /// ```
355    /// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
356    /// # use serde::{Deserialize, Serialize};
357    /// #
358    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
359    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
360    /// #
361    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
362    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
363    /// # let index = client.create_index("documents_query_execute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
364    /// #[derive(Debug, Serialize, Deserialize, PartialEq)]
365    /// struct MyObject {
366    ///     id: Option<usize>,
367    ///     kind: String,
368    /// }
369    /// let index = client.index("documents_query_execute");
370    ///
371    /// let document = DocumentsQuery::new(&index)
372    ///     .with_offset(1)
373    ///     .execute::<MyObject>()
374    ///     .await
375    ///     .unwrap();
376    ///
377    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
378    /// # });
379    /// ```
380    pub async fn execute<T: DeserializeOwned + 'static + Send + Sync>(
381        &self,
382    ) -> Result<DocumentsResults<T>, Error> {
383        self.index.get_documents_with::<T>(self).await
384    }
385}
386
387#[derive(Debug, Clone, Serialize)]
388pub struct DocumentDeletionQuery<'a, Http: HttpClient> {
389    #[serde(skip_serializing)]
390    pub index: &'a Index<Http>,
391
392    /// Filters to apply.
393    ///
394    /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax.
395    pub filter: Option<&'a str>,
396}
397
398impl<'a, Http: HttpClient> DocumentDeletionQuery<'a, Http> {
399    #[must_use]
400    pub fn new(index: &Index<Http>) -> DocumentDeletionQuery<'_, Http> {
401        DocumentDeletionQuery {
402            index,
403            filter: None,
404        }
405    }
406
407    pub fn with_filter<'b>(
408        &'b mut self,
409        filter: &'a str,
410    ) -> &'b mut DocumentDeletionQuery<'a, Http> {
411        self.filter = Some(filter);
412        self
413    }
414
415    pub async fn execute<T: DeserializeOwned + 'static>(&self) -> Result<TaskInfo, Error> {
416        self.index.delete_documents_with(self).await
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use crate::{client::Client, errors::*, indexes::*};
424    use meilisearch_test_macro::meilisearch_test;
425    use serde::{Deserialize, Serialize};
426
427    #[derive(Debug, Serialize, Deserialize, PartialEq)]
428    struct MyObject {
429        id: Option<usize>,
430        kind: String,
431    }
432
433    #[allow(unused)]
434    #[derive(IndexConfig)]
435    struct MovieClips {
436        #[index_config(primary_key)]
437        movie_id: u64,
438        #[index_config(distinct)]
439        owner: String,
440        #[index_config(displayed, searchable)]
441        title: String,
442        #[index_config(displayed)]
443        description: String,
444        #[index_config(filterable, sortable, displayed)]
445        release_date: String,
446        #[index_config(filterable, displayed)]
447        genres: Vec<String>,
448    }
449
450    #[allow(unused)]
451    #[derive(IndexConfig)]
452    struct VideoClips {
453        video_id: u64,
454    }
455
456    async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
457        let t0 = index
458            .add_documents(
459                &[
460                    MyObject {
461                        id: Some(0),
462                        kind: "text".into(),
463                    },
464                    MyObject {
465                        id: Some(1),
466                        kind: "text".into(),
467                    },
468                    MyObject {
469                        id: Some(2),
470                        kind: "title".into(),
471                    },
472                    MyObject {
473                        id: Some(3),
474                        kind: "title".into(),
475                    },
476                ],
477                None,
478            )
479            .await?;
480
481        t0.wait_for_completion(client, None, None).await?;
482
483        Ok(())
484    }
485
486    #[meilisearch_test]
487    async fn test_get_documents_with_execute(client: Client, index: Index) -> Result<(), Error> {
488        setup_test_index(&client, &index).await?;
489        let documents = DocumentsQuery::new(&index)
490            .with_limit(1)
491            .with_offset(1)
492            .with_fields(["kind"])
493            .execute::<MyObject>()
494            .await
495            .unwrap();
496
497        assert_eq!(documents.limit, 1);
498        assert_eq!(documents.offset, 1);
499        assert_eq!(documents.results.len(), 1);
500
501        Ok(())
502    }
503
504    #[meilisearch_test]
505    async fn test_get_documents_by_ids(client: Client, index: Index) -> Result<(), Error> {
506        setup_test_index(&client, &index).await?;
507
508        let documents = DocumentsQuery::new(&index)
509            .with_ids(["1", "3"]) // retrieve by IDs
510            .execute::<MyObject>()
511            .await?;
512
513        assert_eq!(documents.results.len(), 2);
514        Ok(())
515    }
516
517    #[meilisearch_test]
518    async fn test_delete_documents_with(client: Client, index: Index) -> Result<(), Error> {
519        setup_test_index(&client, &index).await?;
520        index
521            .set_filterable_attributes(["id"])
522            .await?
523            .wait_for_completion(&client, None, None)
524            .await?;
525
526        let mut query = DocumentDeletionQuery::new(&index);
527        query.with_filter("id = 1");
528        index
529            .delete_documents_with(&query)
530            .await?
531            .wait_for_completion(&client, None, None)
532            .await?;
533        let document_result = index.get_document::<MyObject>("1").await;
534
535        match document_result {
536            Ok(_) => panic!("The test was expecting no documents to be returned but got one."),
537            Err(e) => match e {
538                Error::Meilisearch(err) => {
539                    assert_eq!(err.error_code, ErrorCode::DocumentNotFound);
540                }
541                _ => panic!("The error was expected to be a Meilisearch error, but it was not."),
542            },
543        }
544
545        Ok(())
546    }
547
548    #[meilisearch_test]
549    async fn test_delete_documents_with_filter_not_filterable(
550        client: Client,
551        index: Index,
552    ) -> Result<(), Error> {
553        setup_test_index(&client, &index).await?;
554
555        let mut query = DocumentDeletionQuery::new(&index);
556        query.with_filter("id = 1");
557        let error = index
558            .delete_documents_with(&query)
559            .await?
560            .wait_for_completion(&client, None, None)
561            .await?;
562
563        let error = error.unwrap_failure();
564
565        assert!(matches!(
566            error,
567            MeilisearchError {
568                error_code: ErrorCode::InvalidDocumentFilter,
569                error_type: ErrorType::InvalidRequest,
570                ..
571            }
572        ));
573
574        Ok(())
575    }
576
577    #[meilisearch_test]
578    async fn test_get_documents_with_only_one_param(
579        client: Client,
580        index: Index,
581    ) -> Result<(), Error> {
582        setup_test_index(&client, &index).await?;
583        // let documents = index.get_documents(None, None, None).await.unwrap();
584        let documents = DocumentsQuery::new(&index)
585            .with_limit(1)
586            .execute::<MyObject>()
587            .await
588            .unwrap();
589
590        assert_eq!(documents.limit, 1);
591        assert_eq!(documents.offset, 0);
592        assert_eq!(documents.results.len(), 1);
593
594        Ok(())
595    }
596
597    #[meilisearch_test]
598    async fn test_get_documents_with_filter(client: Client, index: Index) -> Result<(), Error> {
599        setup_test_index(&client, &index).await?;
600
601        index
602            .set_filterable_attributes(["id"])
603            .await
604            .unwrap()
605            .wait_for_completion(&client, None, None)
606            .await
607            .unwrap();
608
609        let documents = DocumentsQuery::new(&index)
610            .with_filter("id = 1")
611            .execute::<MyObject>()
612            .await?;
613
614        assert_eq!(documents.results.len(), 1);
615
616        Ok(())
617    }
618
619    #[meilisearch_test]
620    async fn test_get_documents_with_sort(client: Client, index: Index) -> Result<(), Error> {
621        setup_test_index(&client, &index).await?;
622
623        index
624            .set_sortable_attributes(["id"])
625            .await?
626            .wait_for_completion(&client, None, None)
627            .await?;
628
629        let documents = DocumentsQuery::new(&index)
630            .with_sort(["id:desc"])
631            .execute::<MyObject>()
632            .await?;
633
634        assert_eq!(
635            documents.results.first().and_then(|document| document.id),
636            Some(3)
637        );
638        assert_eq!(
639            documents.results.last().and_then(|document| document.id),
640            Some(0)
641        );
642
643        Ok(())
644    }
645
646    #[meilisearch_test]
647    async fn test_get_documents_with_error_hint() -> Result<(), Error> {
648        let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
649        let client = Client::new(format!("{meilisearch_url}/hello"), Some("masterKey")).unwrap();
650        let index = client.index("test_get_documents_with_filter_wrong_ms_version");
651
652        let documents = DocumentsQuery::new(&index)
653            .with_filter("id = 1")
654            .execute::<MyObject>()
655            .await;
656
657        let error = documents.unwrap_err();
658
659        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());
660        let url = format!(
661            "{meilisearch_url}/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch"
662        );
663        let status_code = 404;
664        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");
665
666        match &error {
667            Error::MeilisearchCommunication(error) => {
668                assert_eq!(error.status_code, status_code);
669                assert_eq!(error.message, message);
670                assert_eq!(error.url, url);
671            }
672            _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."),
673        };
674        assert_eq!(format!("{error}"), displayed_error);
675
676        Ok(())
677    }
678
679    #[meilisearch_test]
680    async fn test_get_documents_with_error_hint_meilisearch_api_error(
681        index: Index,
682        client: Client,
683    ) -> Result<(), Error> {
684        setup_test_index(&client, &index).await?;
685
686        let error = DocumentsQuery::new(&index)
687            .with_filter("id = 1")
688            .execute::<MyObject>()
689            .await
690            .unwrap_err();
691
692        let message = "Attribute `id` is not filterable. This index does not have configured filterable attributes.
6931:3 id = 1
694Hint: 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();
695        let displayed_error = "Meilisearch invalid_request: invalid_document_filter: Attribute `id` is not filterable. This index does not have configured filterable attributes.
6961:3 id = 1
697Hint: 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";
698
699        match &error {
700            Error::Meilisearch(error) => {
701                assert_eq!(error.error_message, message);
702            }
703            _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."),
704        };
705        assert_eq!(format!("{error}"), displayed_error);
706
707        Ok(())
708    }
709
710    #[meilisearch_test]
711    async fn test_get_documents_with_invalid_filter(
712        client: Client,
713        index: Index,
714    ) -> Result<(), Error> {
715        setup_test_index(&client, &index).await?;
716
717        // Does not work because `id` is not filterable
718        let error = DocumentsQuery::new(&index)
719            .with_filter("id = 1")
720            .execute::<MyObject>()
721            .await
722            .unwrap_err();
723
724        assert!(matches!(
725            error,
726            Error::Meilisearch(MeilisearchError {
727                error_code: ErrorCode::InvalidDocumentFilter,
728                error_type: ErrorType::InvalidRequest,
729                ..
730            })
731        ));
732
733        Ok(())
734    }
735
736    #[meilisearch_test]
737    async fn test_settings_generated_by_macro(client: Client, index: Index) -> Result<(), Error> {
738        setup_test_index(&client, &index).await?;
739
740        let movie_settings: Settings = MovieClips::generate_settings();
741        let video_settings: Settings = VideoClips::generate_settings();
742
743        assert_eq!(movie_settings.searchable_attributes.unwrap(), ["title"]);
744        assert!(video_settings.searchable_attributes.unwrap().is_empty());
745
746        assert_eq!(
747            movie_settings.displayed_attributes.unwrap(),
748            ["title", "description", "release_date", "genres"]
749        );
750        assert!(video_settings.displayed_attributes.unwrap().is_empty());
751
752        assert_eq!(
753            movie_settings.filterable_attributes.unwrap(),
754            ["release_date", "genres"]
755        );
756        assert!(video_settings.filterable_attributes.unwrap().is_empty());
757
758        assert_eq!(
759            movie_settings.sortable_attributes.unwrap(),
760            ["release_date"]
761        );
762        assert!(video_settings.sortable_attributes.unwrap().is_empty());
763
764        Ok(())
765    }
766
767    #[meilisearch_test]
768    async fn test_generate_index(client: Client) -> Result<(), Error> {
769        let index: Index = MovieClips::generate_index(&client).await.unwrap();
770
771        assert_eq!(index.uid, "movie_clips");
772
773        index
774            .delete()
775            .await?
776            .wait_for_completion(&client, None, None)
777            .await?;
778
779        Ok(())
780    }
781    #[derive(Serialize, Deserialize, IndexConfig)]
782    struct Movie {
783        #[index_config(primary_key)]
784        movie_id: u64,
785        #[index_config(displayed, searchable)]
786        title: String,
787        #[index_config(displayed)]
788        description: String,
789        #[index_config(filterable, sortable, displayed)]
790        release_date: String,
791        #[index_config(filterable, displayed)]
792        genres: Vec<String>,
793    }
794}