1use async_trait::async_trait;
2use serde::{de::DeserializeOwned, Deserialize, Serialize};
3
4pub 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 #[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 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 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 #[serde(skip_serializing_if = "Option::is_none")]
177 pub offset: Option<usize>,
178
179 #[serde(skip_serializing_if = "Option::is_none")]
187 pub limit: Option<usize>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub fields: Option<Vec<&'a str>>,
192
193 #[serde(skip_serializing_if = "Option::is_none")]
197 pub sort: Option<Vec<&'a str>>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
204 pub filter: Option<&'a str>,
205
206 #[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 pub fn with_offset(&mut self, offset: usize) -> &mut DocumentsQuery<'a, Http> {
246 self.offset = Some(offset);
247 self
248 }
249
250 pub fn with_limit(&mut self, limit: usize) -> &mut DocumentsQuery<'a, Http> {
268 self.limit = Some(limit);
269 self
270 }
271
272 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 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 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 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 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"]) .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 = 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 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}