1use crate::{
2 client::Client,
3 errors::{Error, MeilisearchError},
4 indexes::Index,
5 request::HttpClient,
6 DefaultHttpClient,
7};
8use either::Either;
9use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer};
10use serde_json::{Map, Value};
11use std::collections::HashMap;
12
13#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
14pub struct MatchRange {
15 pub start: usize,
16 pub length: usize,
17
18 pub indices: Option<Vec<usize>>,
28}
29
30#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
31#[serde(transparent)]
32pub struct Filter<'a> {
33 #[serde(with = "either::serde_untagged")]
34 inner: Either<&'a str, Vec<&'a str>>,
35}
36
37impl<'a> Filter<'a> {
38 #[must_use]
39 pub fn new(inner: Either<&'a str, Vec<&'a str>>) -> Filter<'a> {
40 Filter { inner }
41 }
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub enum MatchingStrategies {
46 #[serde(rename = "all")]
47 ALL,
48 #[serde(rename = "last")]
49 LAST,
50 #[serde(rename = "frequency")]
51 FREQUENCY,
52}
53
54#[derive(Serialize, Deserialize, Debug, Clone)]
58pub struct SearchResult<T> {
59 #[serde(flatten)]
61 pub result: T,
62
63 #[serde(rename = "_formatted", skip_serializing_if = "Option::is_none")]
65 pub formatted_result: Option<Map<String, Value>>,
66
67 #[serde(rename = "_matchesPosition", skip_serializing_if = "Option::is_none")]
69 pub matches_position: Option<HashMap<String, Vec<MatchRange>>>,
70
71 #[serde(rename = "_rankingScore", skip_serializing_if = "Option::is_none")]
73 pub ranking_score: Option<f64>,
74
75 #[serde(
77 rename = "_rankingScoreDetails",
78 skip_serializing_if = "Option::is_none"
79 )]
80 pub ranking_score_details: Option<Map<String, Value>>,
81
82 #[serde(rename = "_federation", skip_serializing_if = "Option::is_none")]
84 pub federation: Option<FederationHitInfo>,
85}
86
87#[derive(Serialize, Deserialize, Debug, Clone)]
88#[serde(rename_all = "camelCase")]
89pub struct FacetStats {
90 pub min: f64,
91 pub max: f64,
92}
93
94#[derive(Serialize, Deserialize, Debug, Clone)]
95#[serde(rename_all = "camelCase")]
96pub struct SearchResults<T> {
98 pub hits: Vec<SearchResult<T>>,
100 pub offset: Option<usize>,
102 pub limit: Option<usize>,
104 pub estimated_total_hits: Option<usize>,
106 pub page: Option<usize>,
108 pub hits_per_page: Option<usize>,
110 pub total_hits: Option<usize>,
112 pub total_pages: Option<usize>,
114 pub facet_distribution: Option<HashMap<String, HashMap<String, usize>>>,
116 pub facet_stats: Option<HashMap<String, FacetStats>>,
118 pub exhaustive_facet_count: Option<bool>,
121 pub processing_time_ms: usize,
123 pub query: String,
125 pub index_uid: Option<String>,
127 #[serde(
130 rename = "queryVector",
131 alias = "query_vector",
132 alias = "queryEmbedding",
133 alias = "query_embedding",
134 alias = "vector",
135 skip_serializing_if = "Option::is_none"
136 )]
137 pub query_vector: Option<Vec<f32>>,
138 pub performance_details: Option<Value>,
141}
142
143fn serialize_attributes_to_crop_with_wildcard<S: Serializer>(
144 data: &Option<Selectors<&[AttributeToCrop]>>,
145 s: S,
146) -> Result<S::Ok, S::Error> {
147 match data {
148 Some(Selectors::All) => ["*"].serialize(s),
149 Some(Selectors::Some(data)) => {
150 let results = data
151 .iter()
152 .map(|(name, value)| {
153 let mut result = (*name).to_string();
154 if let Some(value) = value {
155 result.push(':');
156 result.push_str(value.to_string().as_str());
157 }
158 result
159 })
160 .collect::<Vec<_>>();
161 results.serialize(s)
162 }
163 None => s.serialize_none(),
164 }
165}
166
167#[derive(Debug, Clone)]
171pub enum Selectors<T> {
172 Some(T),
174 All,
176}
177
178impl<T: Serialize> Serialize for Selectors<T> {
179 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
180 match self {
181 Selectors::Some(data) => data.serialize(s),
182 Selectors::All => ["*"].serialize(s),
183 }
184 }
185}
186
187#[derive(Debug, Serialize, Clone)]
189#[serde(rename_all = "camelCase")]
190pub struct HybridSearch<'a> {
191 pub embedder: &'a str,
193 pub semantic_ratio: f32,
197}
198
199type AttributeToCrop<'a> = (&'a str, Option<usize>);
200
201#[derive(Debug, Serialize, Clone)]
260#[serde(rename_all = "camelCase")]
261pub struct SearchQuery<'a, Http: HttpClient> {
262 #[serde(skip_serializing)]
263 index: &'a Index<Http>,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 #[serde(rename = "q")]
267 pub query: Option<&'a str>,
268 #[serde(skip_serializing_if = "Option::is_none")]
275 pub offset: Option<usize>,
276 #[serde(skip_serializing_if = "Option::is_none")]
285 pub limit: Option<usize>,
286 #[serde(skip_serializing_if = "Option::is_none")]
292 pub page: Option<usize>,
293 #[serde(skip_serializing_if = "Option::is_none")]
297 pub hits_per_page: Option<usize>,
298 #[serde(skip_serializing_if = "Option::is_none")]
302 pub filter: Option<Filter<'a>>,
303 #[serde(skip_serializing_if = "Option::is_none")]
309 pub facets: Option<Selectors<&'a [&'a str]>>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub sort: Option<&'a [&'a str]>,
313 #[serde(skip_serializing_if = "Option::is_none")]
319 pub attributes_to_search_on: Option<&'a [&'a str]>,
320 #[serde(skip_serializing_if = "Option::is_none")]
326 pub attributes_to_retrieve: Option<Selectors<&'a [&'a str]>>,
327 #[serde(skip_serializing_if = "Option::is_none")]
333 #[serde(serialize_with = "serialize_attributes_to_crop_with_wildcard")]
334 pub attributes_to_crop: Option<Selectors<&'a [AttributeToCrop<'a>]>>,
335 #[serde(skip_serializing_if = "Option::is_none")]
341 pub crop_length: Option<usize>,
342 #[serde(skip_serializing_if = "Option::is_none")]
348 pub crop_marker: Option<&'a str>,
349 #[serde(skip_serializing_if = "Option::is_none")]
353 pub attributes_to_highlight: Option<Selectors<&'a [&'a str]>>,
354 #[serde(skip_serializing_if = "Option::is_none")]
360 pub highlight_pre_tag: Option<&'a str>,
361 #[serde(skip_serializing_if = "Option::is_none")]
367 pub highlight_post_tag: Option<&'a str>,
368 #[serde(skip_serializing_if = "Option::is_none")]
372 pub show_matches_position: Option<bool>,
373
374 #[serde(skip_serializing_if = "Option::is_none")]
378 pub show_ranking_score: Option<bool>,
379
380 #[serde(skip_serializing_if = "Option::is_none")]
384 pub show_ranking_score_details: Option<bool>,
385
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub matching_strategy: Option<MatchingStrategies>,
389
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub distinct: Option<&'a str>,
393
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub ranking_score_threshold: Option<f64>,
397
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub locales: Option<&'a [&'a str]>,
401
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub(crate) index_uid: Option<&'a str>,
404
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub hybrid: Option<HybridSearch<'a>>,
408
409 #[serde(skip_serializing_if = "Option::is_none")]
411 pub vector: Option<&'a [f32]>,
412
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub retrieve_vectors: Option<bool>,
416
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub media: Option<Value>,
420
421 #[serde(skip_serializing_if = "Option::is_none")]
426 pub exhaustive_facet_count: Option<bool>,
427
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub(crate) federation_options: Option<QueryFederationOptions>,
430
431 #[serde(skip_serializing_if = "Option::is_none")]
435 pub show_performance_details: Option<bool>,
436}
437
438#[derive(Debug, Serialize, Clone)]
439#[serde(rename_all = "camelCase")]
440pub struct QueryFederationOptions {
441 #[serde(skip_serializing_if = "Option::is_none")]
443 pub weight: Option<f32>,
444 #[serde(skip_serializing_if = "Option::is_none")]
446 pub remote: Option<String>,
447}
448
449#[allow(missing_docs)]
450impl<'a, Http: HttpClient> SearchQuery<'a, Http> {
451 #[must_use]
452 pub fn new(index: &'a Index<Http>) -> SearchQuery<'a, Http> {
453 SearchQuery {
454 index,
455 query: None,
456 offset: None,
457 limit: None,
458 page: None,
459 hits_per_page: None,
460 filter: None,
461 sort: None,
462 facets: None,
463 attributes_to_search_on: None,
464 attributes_to_retrieve: None,
465 attributes_to_crop: None,
466 crop_length: None,
467 crop_marker: None,
468 attributes_to_highlight: None,
469 highlight_pre_tag: None,
470 highlight_post_tag: None,
471 show_matches_position: None,
472 show_ranking_score: None,
473 show_ranking_score_details: None,
474 matching_strategy: None,
475 index_uid: None,
476 hybrid: None,
477 vector: None,
478 retrieve_vectors: None,
479 media: None,
480 exhaustive_facet_count: None,
481 distinct: None,
482 ranking_score_threshold: None,
483 locales: None,
484 federation_options: None,
485 show_performance_details: None,
486 }
487 }
488
489 pub fn with_query<'b>(&'b mut self, query: &'a str) -> &'b mut SearchQuery<'a, Http> {
490 self.query = Some(query);
491 self
492 }
493
494 pub fn with_offset<'b>(&'b mut self, offset: usize) -> &'b mut SearchQuery<'a, Http> {
495 self.offset = Some(offset);
496 self
497 }
498
499 pub fn with_limit<'b>(&'b mut self, limit: usize) -> &'b mut SearchQuery<'a, Http> {
500 self.limit = Some(limit);
501 self
502 }
503
504 pub fn with_page<'b>(&'b mut self, page: usize) -> &'b mut SearchQuery<'a, Http> {
532 self.page = Some(page);
533 self
534 }
535
536 pub fn with_hits_per_page<'b>(
564 &'b mut self,
565 hits_per_page: usize,
566 ) -> &'b mut SearchQuery<'a, Http> {
567 self.hits_per_page = Some(hits_per_page);
568 self
569 }
570
571 pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut SearchQuery<'a, Http> {
572 self.filter = Some(Filter::new(Either::Left(filter)));
573 self
574 }
575
576 pub fn with_array_filter<'b>(
577 &'b mut self,
578 filter: Vec<&'a str>,
579 ) -> &'b mut SearchQuery<'a, Http> {
580 self.filter = Some(Filter::new(Either::Right(filter)));
581 self
582 }
583
584 pub fn with_retrieve_vectors<'b>(
586 &'b mut self,
587 retrieve_vectors: bool,
588 ) -> &'b mut SearchQuery<'a, Http> {
589 self.retrieve_vectors = Some(retrieve_vectors);
590 self
591 }
592
593 pub fn with_facets<'b>(
594 &'b mut self,
595 facets: Selectors<&'a [&'a str]>,
596 ) -> &'b mut SearchQuery<'a, Http> {
597 self.facets = Some(facets);
598 self
599 }
600
601 pub fn with_sort<'b>(&'b mut self, sort: &'a [&'a str]) -> &'b mut SearchQuery<'a, Http> {
602 self.sort = Some(sort);
603 self
604 }
605
606 pub fn with_attributes_to_search_on<'b>(
607 &'b mut self,
608 attributes_to_search_on: &'a [&'a str],
609 ) -> &'b mut SearchQuery<'a, Http> {
610 self.attributes_to_search_on = Some(attributes_to_search_on);
611 self
612 }
613
614 pub fn with_attributes_to_retrieve<'b>(
615 &'b mut self,
616 attributes_to_retrieve: Selectors<&'a [&'a str]>,
617 ) -> &'b mut SearchQuery<'a, Http> {
618 self.attributes_to_retrieve = Some(attributes_to_retrieve);
619 self
620 }
621
622 pub fn with_attributes_to_crop<'b>(
623 &'b mut self,
624 attributes_to_crop: Selectors<&'a [(&'a str, Option<usize>)]>,
625 ) -> &'b mut SearchQuery<'a, Http> {
626 self.attributes_to_crop = Some(attributes_to_crop);
627 self
628 }
629
630 pub fn with_crop_length<'b>(&'b mut self, crop_length: usize) -> &'b mut SearchQuery<'a, Http> {
631 self.crop_length = Some(crop_length);
632 self
633 }
634
635 pub fn with_crop_marker<'b>(
636 &'b mut self,
637 crop_marker: &'a str,
638 ) -> &'b mut SearchQuery<'a, Http> {
639 self.crop_marker = Some(crop_marker);
640 self
641 }
642
643 pub fn with_attributes_to_highlight<'b>(
644 &'b mut self,
645 attributes_to_highlight: Selectors<&'a [&'a str]>,
646 ) -> &'b mut SearchQuery<'a, Http> {
647 self.attributes_to_highlight = Some(attributes_to_highlight);
648 self
649 }
650
651 pub fn with_highlight_pre_tag<'b>(
652 &'b mut self,
653 highlight_pre_tag: &'a str,
654 ) -> &'b mut SearchQuery<'a, Http> {
655 self.highlight_pre_tag = Some(highlight_pre_tag);
656 self
657 }
658
659 pub fn with_highlight_post_tag<'b>(
660 &'b mut self,
661 highlight_post_tag: &'a str,
662 ) -> &'b mut SearchQuery<'a, Http> {
663 self.highlight_post_tag = Some(highlight_post_tag);
664 self
665 }
666
667 pub fn with_show_matches_position<'b>(
668 &'b mut self,
669 show_matches_position: bool,
670 ) -> &'b mut SearchQuery<'a, Http> {
671 self.show_matches_position = Some(show_matches_position);
672 self
673 }
674
675 pub fn with_show_ranking_score<'b>(
676 &'b mut self,
677 show_ranking_score: bool,
678 ) -> &'b mut SearchQuery<'a, Http> {
679 self.show_ranking_score = Some(show_ranking_score);
680 self
681 }
682
683 pub fn with_show_ranking_score_details<'b>(
684 &'b mut self,
685 show_ranking_score_details: bool,
686 ) -> &'b mut SearchQuery<'a, Http> {
687 self.show_ranking_score_details = Some(show_ranking_score_details);
688 self
689 }
690
691 pub fn with_matching_strategy<'b>(
692 &'b mut self,
693 matching_strategy: MatchingStrategies,
694 ) -> &'b mut SearchQuery<'a, Http> {
695 self.matching_strategy = Some(matching_strategy);
696 self
697 }
698
699 pub fn with_index_uid<'b>(&'b mut self) -> &'b mut SearchQuery<'a, Http> {
700 self.index_uid = Some(&self.index.uid);
701 self
702 }
703
704 pub fn with_hybrid<'b>(
706 &'b mut self,
707 embedder: &'a str,
708 semantic_ratio: f32,
709 ) -> &'b mut SearchQuery<'a, Http> {
710 self.hybrid = Some(HybridSearch {
711 embedder,
712 semantic_ratio,
713 });
714 self
715 }
716
717 pub fn with_vector<'b>(&'b mut self, vector: &'a [f32]) -> &'b mut SearchQuery<'a, Http> {
724 self.vector = Some(vector);
725 self
726 }
727
728 pub fn with_media<'b>(&'b mut self, media: Value) -> &'b mut SearchQuery<'a, Http> {
730 self.media = Some(media);
731 self
732 }
733
734 pub fn with_distinct<'b>(&'b mut self, distinct: &'a str) -> &'b mut SearchQuery<'a, Http> {
735 self.distinct = Some(distinct);
736 self
737 }
738
739 pub fn with_ranking_score_threshold<'b>(
740 &'b mut self,
741 ranking_score_threshold: f64,
742 ) -> &'b mut SearchQuery<'a, Http> {
743 self.ranking_score_threshold = Some(ranking_score_threshold);
744 self
745 }
746
747 pub fn with_locales<'b>(&'b mut self, locales: &'a [&'a str]) -> &'b mut SearchQuery<'a, Http> {
748 self.locales = Some(locales);
749 self
750 }
751
752 pub fn build(&mut self) -> SearchQuery<'a, Http> {
753 self.clone()
754 }
755
756 pub fn with_exhaustive_facet_count<'b>(
758 &'b mut self,
759 exhaustive: bool,
760 ) -> &'b mut SearchQuery<'a, Http> {
761 self.exhaustive_facet_count = Some(exhaustive);
762 self
763 }
764
765 pub fn with_show_performance_details<'b>(
767 &'b mut self,
768 show_performance_details: bool,
769 ) -> &'b mut SearchQuery<'a, Http> {
770 self.show_performance_details = Some(show_performance_details);
771 self
772 }
773
774 pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
776 &'a self,
777 ) -> Result<SearchResults<T>, Error> {
778 self.index.execute_query::<T>(self).await
779 }
780}
781
782#[derive(Debug, Serialize, Clone)]
783#[serde(rename_all = "camelCase")]
784pub struct MultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> {
785 #[serde(skip_serializing)]
786 client: &'a Client<Http>,
787 #[serde(bound(serialize = ""))]
792 pub queries: Vec<SearchQuery<'b, Http>>,
793}
794
795#[allow(missing_docs)]
796impl<'a, 'b, Http: HttpClient> MultiSearchQuery<'a, 'b, Http> {
797 #[must_use]
798 pub fn new(client: &'a Client<Http>) -> MultiSearchQuery<'a, 'b, Http> {
799 MultiSearchQuery {
800 client,
801 queries: Vec::new(),
802 }
803 }
804
805 pub fn with_search_query(
806 &mut self,
807 mut search_query: SearchQuery<'b, Http>,
808 ) -> &mut MultiSearchQuery<'a, 'b, Http> {
809 search_query.with_index_uid();
810 self.queries.push(search_query);
811 self
812 }
813
814 pub fn with_search_query_and_weight(
815 &mut self,
816 search_query: SearchQuery<'b, Http>,
817 weight: f32,
818 ) -> &mut MultiSearchQuery<'a, 'b, Http> {
819 self.with_search_query_and_options(
820 search_query,
821 QueryFederationOptions {
822 weight: Some(weight),
823 remote: None,
824 },
825 )
826 }
827
828 pub fn with_search_query_and_options(
829 &mut self,
830 mut search_query: SearchQuery<'b, Http>,
831 options: QueryFederationOptions,
832 ) -> &mut MultiSearchQuery<'a, 'b, Http> {
833 search_query.with_index_uid();
834 search_query.federation_options = Some(options);
835 self.queries.push(search_query);
836 self
837 }
838
839 pub fn with_federation(
841 self,
842 federation: FederationOptions,
843 ) -> FederatedMultiSearchQuery<'a, 'b, Http> {
844 FederatedMultiSearchQuery {
845 client: self.client,
846 queries: self.queries,
847 federation: Some(federation),
848 }
849 }
850
851 pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
853 &'a self,
854 ) -> Result<MultiSearchResponse<T>, Error> {
855 self.client.execute_multi_search_query::<T>(self).await
856 }
857}
858
859#[derive(Debug, Clone, Deserialize, Serialize)]
860pub struct MultiSearchResponse<T> {
861 pub results: Vec<SearchResults<T>>,
862}
863
864#[derive(Debug, Serialize, Clone)]
865#[serde(rename_all = "camelCase")]
866pub struct FederatedMultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> {
867 #[serde(skip_serializing)]
868 client: &'a Client<Http>,
869 #[serde(bound(serialize = ""))]
870 pub queries: Vec<SearchQuery<'b, Http>>,
871 #[serde(skip_serializing_if = "Option::is_none")]
872 pub federation: Option<FederationOptions>,
873}
874
875#[derive(Debug, Serialize, Clone, Default)]
876#[serde(rename_all = "camelCase")]
877pub struct MergeFacets {
878 #[serde(skip_serializing_if = "Option::is_none")]
879 pub max_values_per_facet: Option<usize>,
880}
881
882#[derive(Debug, Serialize, Clone, Default)]
885#[serde(rename_all = "camelCase")]
886pub struct FederationOptions {
887 #[serde(skip_serializing_if = "Option::is_none")]
889 pub offset: Option<usize>,
890
891 #[serde(skip_serializing_if = "Option::is_none")]
893 pub limit: Option<usize>,
894
895 #[serde(skip_serializing_if = "Option::is_none")]
897 pub facets_by_index: Option<HashMap<String, Vec<String>>>,
898
899 #[serde(skip_serializing_if = "Option::is_none")]
901 pub merge_facets: Option<MergeFacets>,
902
903 #[serde(skip_serializing_if = "Option::is_none")]
905 pub show_performance_details: Option<bool>,
906}
907
908impl<'a, Http: HttpClient> FederatedMultiSearchQuery<'a, '_, Http> {
909 pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
911 &'a self,
912 ) -> Result<FederatedMultiSearchResponse<T>, Error> {
913 self.client
914 .execute_federated_multi_search_query::<T>(self)
915 .await
916 }
917}
918
919#[derive(Debug, Clone, Default, Serialize, Deserialize)]
920pub struct ComputedFacets {
921 pub distribution: HashMap<String, HashMap<String, u64>>,
922 pub stats: HashMap<String, FacetStats>,
923}
924
925#[derive(Debug, Deserialize, Clone)]
927#[serde(rename_all = "camelCase")]
928pub struct FederatedMultiSearchResponse<T> {
929 pub hits: Vec<SearchResult<T>>,
931
932 pub offset: usize,
934
935 pub limit: usize,
937
938 pub estimated_total_hits: usize,
940
941 pub processing_time_ms: usize,
943
944 pub facets_by_index: Option<ComputedFacets>,
946
947 pub facet_distribution: Option<HashMap<String, HashMap<String, usize>>>,
949
950 pub facet_stats: Option<HashMap<String, FacetStats>>,
952
953 pub remote_errors: Option<HashMap<String, MeilisearchError>>,
955
956 pub performance_details: Option<Value>,
958}
959
960#[derive(Serialize, Deserialize, Debug, Clone)]
962#[serde(rename_all = "camelCase")]
963pub struct FederationHitInfo {
964 pub index_uid: String,
966
967 pub queries_position: usize,
969
970 pub remote: Option<String>,
972
973 pub weighted_ranking_score: f32,
975}
976
977#[derive(Debug, Serialize, Clone)]
1027#[serde(rename_all = "camelCase")]
1028pub struct FacetSearchQuery<'a, Http: HttpClient = DefaultHttpClient> {
1029 #[serde(skip_serializing)]
1030 index: &'a Index<Http>,
1031 pub facet_name: &'a str,
1033 #[serde(skip_serializing_if = "Option::is_none")]
1035 pub facet_query: Option<&'a str>,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1038 #[serde(rename = "q")]
1039 pub search_query: Option<&'a str>,
1040 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub filter: Option<Filter<'a>>,
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub matching_strategy: Option<MatchingStrategies>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub attributes_to_search_on: Option<&'a [&'a str]>,
1051 #[serde(skip_serializing_if = "Option::is_none")]
1053 pub exhaustive_facet_count: Option<bool>,
1054}
1055
1056#[allow(missing_docs)]
1057impl<'a, Http: HttpClient> FacetSearchQuery<'a, Http> {
1058 pub fn new(index: &'a Index<Http>, facet_name: &'a str) -> FacetSearchQuery<'a, Http> {
1059 FacetSearchQuery {
1060 index,
1061 facet_name,
1062 facet_query: None,
1063 search_query: None,
1064 filter: None,
1065 matching_strategy: None,
1066 attributes_to_search_on: None,
1067 exhaustive_facet_count: None,
1068 }
1069 }
1070
1071 pub fn with_facet_query<'b>(
1072 &'b mut self,
1073 facet_query: &'a str,
1074 ) -> &'b mut FacetSearchQuery<'a, Http> {
1075 self.facet_query = Some(facet_query);
1076 self
1077 }
1078
1079 pub fn with_search_query<'b>(
1080 &'b mut self,
1081 search_query: &'a str,
1082 ) -> &'b mut FacetSearchQuery<'a, Http> {
1083 self.search_query = Some(search_query);
1084 self
1085 }
1086
1087 pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut FacetSearchQuery<'a, Http> {
1088 self.filter = Some(Filter::new(Either::Left(filter)));
1089 self
1090 }
1091
1092 pub fn with_array_filter<'b>(
1093 &'b mut self,
1094 filter: Vec<&'a str>,
1095 ) -> &'b mut FacetSearchQuery<'a, Http> {
1096 self.filter = Some(Filter::new(Either::Right(filter)));
1097 self
1098 }
1099
1100 pub fn with_matching_strategy<'b>(
1101 &'b mut self,
1102 matching_strategy: MatchingStrategies,
1103 ) -> &'b mut FacetSearchQuery<'a, Http> {
1104 self.matching_strategy = Some(matching_strategy);
1105 self
1106 }
1107
1108 pub fn with_attributes_to_search_on<'b>(
1109 &'b mut self,
1110 attributes_to_search_on: &'a [&'a str],
1111 ) -> &'b mut FacetSearchQuery<'a, Http> {
1112 self.attributes_to_search_on = Some(attributes_to_search_on);
1113 self
1114 }
1115
1116 pub fn with_exhaustive_facet_count<'b>(
1117 &'b mut self,
1118 exhaustive_facet_count: bool,
1119 ) -> &'b mut FacetSearchQuery<'a, Http> {
1120 self.exhaustive_facet_count = Some(exhaustive_facet_count);
1121 self
1122 }
1123
1124 pub fn build(&mut self) -> FacetSearchQuery<'a, Http> {
1125 self.clone()
1126 }
1127
1128 pub async fn execute(&'a self) -> Result<FacetSearchResponse, Error> {
1129 self.index.execute_facet_query(self).await
1130 }
1131}
1132
1133#[derive(Debug, Deserialize)]
1134#[serde(rename_all = "camelCase")]
1135pub struct FacetHit {
1136 pub value: String,
1137 pub count: usize,
1138}
1139
1140#[derive(Debug, Deserialize)]
1141#[serde(rename_all = "camelCase")]
1142pub struct FacetSearchResponse {
1143 pub facet_hits: Vec<FacetHit>,
1144 pub facet_query: Option<String>,
1145 pub processing_time_ms: usize,
1146}
1147
1148#[cfg(test)]
1149pub(crate) mod tests {
1150 use crate::errors::{ErrorCode, MeilisearchError};
1151 use crate::{
1152 client::*,
1153 key::{Action, KeyBuilder},
1154 search::*,
1155 settings::EmbedderSource,
1156 };
1157 use big_s::S;
1158 use meilisearch_test_macro::meilisearch_test;
1159 use serde::{Deserialize, Serialize};
1160 use serde_json::{json, Map, Value};
1161
1162 #[test]
1163 fn search_query_serializes_media_parameter() {
1164 let client = Client::new("http://localhost:7700", Some("masterKey")).unwrap();
1165 let index = client.index("media_query");
1166 let mut query = SearchQuery::new(&index);
1167
1168 query.with_query("example").with_media(json!({
1169 "FIELD_A": "VALUE_A",
1170 "FIELD_B": {
1171 "FIELD_C": "VALUE_B",
1172 "FIELD_D": "VALUE_C"
1173 }
1174 }));
1175
1176 let serialized = serde_json::to_value(&query.build()).unwrap();
1177
1178 assert_eq!(
1179 serialized.get("media"),
1180 Some(&json!({
1181 "FIELD_A": "VALUE_A",
1182 "FIELD_B": {
1183 "FIELD_C": "VALUE_B",
1184 "FIELD_D": "VALUE_C"
1185 }
1186 }))
1187 );
1188 }
1189
1190 #[derive(Debug, Serialize, Deserialize, PartialEq)]
1191 pub struct Nested {
1192 child: String,
1193 }
1194
1195 #[derive(Debug, Serialize, Deserialize, PartialEq)]
1196 pub struct Document {
1197 pub id: usize,
1198 pub value: String,
1199 pub kind: String,
1200 pub number: i32,
1201 pub nested: Nested,
1202 #[serde(skip_serializing_if = "Option::is_none", default)]
1203 pub _vectors: Option<Vectors>,
1204 }
1205
1206 #[derive(Debug, Serialize, Deserialize, PartialEq)]
1207 struct Vector {
1208 embeddings: SingleOrMultipleVectors,
1209 regenerate: bool,
1210 }
1211
1212 #[derive(Serialize, Deserialize, Debug, PartialEq)]
1213 #[serde(untagged)]
1214 enum SingleOrMultipleVectors {
1215 Single(Vec<f32>),
1216 Multiple(Vec<Vec<f32>>),
1217 }
1218
1219 #[derive(Debug, Serialize, Deserialize, PartialEq)]
1220 pub struct Vectors(HashMap<String, Vector>);
1221
1222 impl<T: Into<Vec<f32>>> From<T> for Vectors {
1223 fn from(value: T) -> Self {
1224 let vec: Vec<f32> = value.into();
1225 Vectors(HashMap::from([(
1226 S("default"),
1227 Vector {
1228 embeddings: SingleOrMultipleVectors::Multiple(Vec::from([vec])),
1229 regenerate: false,
1230 },
1231 )]))
1232 }
1233 }
1234
1235 impl PartialEq<Map<String, Value>> for Document {
1236 #[allow(clippy::cmp_owned)]
1237 fn eq(&self, rhs: &Map<String, Value>) -> bool {
1238 self.id.to_string() == rhs["id"]
1239 && self.value == rhs["value"]
1240 && self.kind == rhs["kind"]
1241 }
1242 }
1243
1244 fn vectorize(is_harry_potter: bool, id: usize) -> Vec<f32> {
1245 let mut vector: Vec<f32> = vec![0.; 11];
1246 vector[0] = if is_harry_potter { 1. } else { 0. };
1247 vector[id + 1] = 1.;
1248 vector
1249 }
1250
1251 pub(crate) async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
1252 let t0 = index.add_documents(&[
1253 Document { id: 0, kind: "text".into(), number: 0, value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), nested: Nested { child: S("first") }, _vectors: Some(Vectors::from(vectorize(false, 0))) },
1254 Document { id: 1, kind: "text".into(), number: 10, value: S("dolor sit amet, consectetur adipiscing elit"), nested: Nested { child: S("second") }, _vectors: Some(Vectors::from(vectorize(false, 1))) },
1255 Document { id: 2, kind: "title".into(), number: 20, value: S("The Social Network"), nested: Nested { child: S("third") }, _vectors: Some(Vectors::from(vectorize(false, 2))) },
1256 Document { id: 3, kind: "title".into(), number: 30, value: S("Harry Potter and the Sorcerer's Stone"), nested: Nested { child: S("fourth") }, _vectors: Some(Vectors::from(vectorize(true, 3))) },
1257 Document { id: 4, kind: "title".into(), number: 40, value: S("Harry Potter and the Chamber of Secrets"), nested: Nested { child: S("fift") }, _vectors: Some(Vectors::from(vectorize(true, 4))) },
1258 Document { id: 5, kind: "title".into(), number: 50, value: S("Harry Potter and the Prisoner of Azkaban"), nested: Nested { child: S("sixth") }, _vectors: Some(Vectors::from(vectorize(true, 5))) },
1259 Document { id: 6, kind: "title".into(), number: 60, value: S("Harry Potter and the Goblet of Fire"), nested: Nested { child: S("seventh") }, _vectors: Some(Vectors::from(vectorize(true, 6))) },
1260 Document { id: 7, kind: "title".into(), number: 70, value: S("Harry Potter and the Order of the Phoenix"), nested: Nested { child: S("eighth") }, _vectors: Some(Vectors::from(vectorize(true, 7))) },
1261 Document { id: 8, kind: "title".into(), number: 80, value: S("Harry Potter and the Half-Blood Prince"), nested: Nested { child: S("ninth") }, _vectors: Some(Vectors::from(vectorize(true, 8))) },
1262 Document { id: 9, kind: "title".into(), number: 90, value: S("Harry Potter and the Deathly Hallows"), nested: Nested { child: S("tenth") }, _vectors: Some(Vectors::from(vectorize(true, 9))) },
1263 ], None).await?;
1264 let t1 = index
1265 .set_filterable_attributes(["kind", "value", "number"])
1266 .await?;
1267 let t2 = index.set_sortable_attributes(["title"]).await?;
1268
1269 t2.wait_for_completion(client, None, None).await?;
1270 t1.wait_for_completion(client, None, None).await?;
1271 t0.wait_for_completion(client, None, None).await?;
1272
1273 Ok(())
1274 }
1275
1276 #[derive(Debug, Serialize, Deserialize, PartialEq)]
1277 struct VideoDocument {
1278 id: usize,
1279 title: String,
1280 description: Option<String>,
1281 duration: u32,
1282 }
1283
1284 async fn setup_test_video_index(client: &Client, index: &Index) -> Result<(), Error> {
1285 let t0 = index
1286 .add_documents(
1287 &[
1288 VideoDocument {
1289 id: 0,
1290 title: S("Spring"),
1291 description: Some(S("A Blender Open movie")),
1292 duration: 123,
1293 },
1294 VideoDocument {
1295 id: 1,
1296 title: S("Wing It!"),
1297 description: None,
1298 duration: 234,
1299 },
1300 VideoDocument {
1301 id: 2,
1302 title: S("Coffee Run"),
1303 description: Some(S("Directed by Hjalti Hjalmarsson")),
1304 duration: 345,
1305 },
1306 VideoDocument {
1307 id: 3,
1308 title: S("Harry Potter and the Deathly Hallows"),
1309 description: None,
1310 duration: 7654,
1311 },
1312 ],
1313 None,
1314 )
1315 .await?;
1316 let t1 = index.set_filterable_attributes(["duration"]).await?;
1317 let t2 = index.set_sortable_attributes(["title"]).await?;
1318
1319 t2.wait_for_completion(client, None, None).await?;
1320 t1.wait_for_completion(client, None, None).await?;
1321 t0.wait_for_completion(client, None, None).await?;
1322 Ok(())
1323 }
1324
1325 pub(crate) async fn setup_embedder(client: &Client, index: &Index) -> Result<(), Error> {
1326 use crate::settings::Embedder;
1327 let embedder_setting = Embedder {
1328 source: EmbedderSource::UserProvided,
1329 dimensions: Some(11),
1330 ..Embedder::default()
1331 };
1332 index
1333 .set_settings(&crate::settings::Settings {
1334 embedders: Some(HashMap::from([("default".to_string(), embedder_setting)])),
1335 ..crate::settings::Settings::default()
1336 })
1337 .await?
1338 .wait_for_completion(client, None, None)
1339 .await?;
1340 Ok(())
1341 }
1342
1343 #[meilisearch_test]
1344 async fn test_multi_search(client: Client, index: Index) -> Result<(), Error> {
1345 setup_test_index(&client, &index).await?;
1346 let search_query_1 = SearchQuery::new(&index)
1347 .with_query("Sorcerer's Stone")
1348 .build();
1349 let search_query_2 = SearchQuery::new(&index)
1350 .with_query("Chamber of Secrets")
1351 .build();
1352
1353 let response = client
1354 .multi_search()
1355 .with_search_query(search_query_1)
1356 .with_search_query(search_query_2)
1357 .execute::<Document>()
1358 .await
1359 .unwrap();
1360
1361 assert_eq!(response.results.len(), 2);
1362 Ok(())
1363 }
1364
1365 #[meilisearch_test]
1366 async fn test_federated_multi_search(
1367 client: Client,
1368 test_index: Index,
1369 video_index: Index,
1370 ) -> Result<(), Error> {
1371 setup_test_index(&client, &test_index).await?;
1372 setup_test_video_index(&client, &video_index).await?;
1373
1374 let query_test_index = SearchQuery::new(&test_index).with_query("death").build();
1375 let query_video_index = SearchQuery::new(&video_index).with_query("death").build();
1376
1377 #[derive(Debug, Serialize, Deserialize, PartialEq)]
1378 #[serde(untagged)]
1379 enum AnyDocument {
1380 Document(Document),
1381 VideoDocument(VideoDocument),
1382 }
1383
1384 let mut multi_query = client.multi_search();
1386 multi_query.with_search_query_and_weight(query_test_index.clone(), 999.0);
1387 multi_query.with_search_query(query_video_index.clone());
1388 let response = multi_query
1389 .with_federation(FederationOptions::default())
1390 .execute::<AnyDocument>()
1391 .await?;
1392 assert_eq!(response.hits.len(), 2);
1393 assert_eq!(
1394 response.hits[0].result,
1395 AnyDocument::Document(Document {
1396 id: 9,
1397 kind: "title".into(),
1398 number: 90,
1399 value: S("Harry Potter and the Deathly Hallows"),
1400 nested: Nested { child: S("tenth") },
1401 _vectors: None,
1402 })
1403 );
1404 assert_eq!(
1405 response.hits[1].result,
1406 AnyDocument::VideoDocument(VideoDocument {
1407 id: 3,
1408 title: S("Harry Potter and the Deathly Hallows"),
1409 description: None,
1410 duration: 7654,
1411 })
1412 );
1413
1414 let mut multi_query = client.multi_search();
1416 multi_query.with_search_query(query_test_index.clone());
1417 multi_query.with_search_query_and_weight(query_video_index.clone(), 999.0);
1418 let response = multi_query
1419 .with_federation(FederationOptions::default())
1420 .execute::<AnyDocument>()
1421 .await?;
1422 assert_eq!(response.hits.len(), 2);
1423 assert_eq!(
1424 response.hits[0].result,
1425 AnyDocument::VideoDocument(VideoDocument {
1426 id: 3,
1427 title: S("Harry Potter and the Deathly Hallows"),
1428 description: None,
1429 duration: 7654,
1430 })
1431 );
1432 assert_eq!(
1433 response.hits[1].result,
1434 AnyDocument::Document(Document {
1435 id: 9,
1436 kind: "title".into(),
1437 number: 90,
1438 value: S("Harry Potter and the Deathly Hallows"),
1439 nested: Nested { child: S("tenth") },
1440 _vectors: None,
1441 })
1442 );
1443
1444 let mut multi_query = client.multi_search();
1446 multi_query.with_search_query(query_test_index.clone());
1447 multi_query.with_search_query(query_video_index.clone());
1448 let response = multi_query
1449 .with_federation(FederationOptions {
1450 limit: Some(1),
1451 ..Default::default()
1452 })
1453 .execute::<AnyDocument>()
1454 .await?;
1455
1456 assert_eq!(response.hits.len(), 1);
1457
1458 Ok(())
1459 }
1460
1461 #[meilisearch_test]
1462 async fn test_query_builder(_client: Client, index: Index) -> Result<(), Error> {
1463 let mut query = SearchQuery::new(&index);
1464 query.with_query("space").with_offset(42).with_limit(21);
1465
1466 let res = query.execute::<Document>().await.unwrap();
1467
1468 assert_eq!(res.query, S("space"));
1469 assert_eq!(res.limit, Some(21));
1470 assert_eq!(res.offset, Some(42));
1471 assert_eq!(res.estimated_total_hits, Some(0));
1472 Ok(())
1473 }
1474
1475 #[meilisearch_test]
1476 async fn test_query_numbered_pagination(client: Client, index: Index) -> Result<(), Error> {
1477 setup_test_index(&client, &index).await?;
1478
1479 let mut query = SearchQuery::new(&index);
1480 query.with_query("").with_page(2).with_hits_per_page(2);
1481
1482 let res = query.execute::<Document>().await.unwrap();
1483
1484 assert_eq!(res.page, Some(2));
1485 assert_eq!(res.hits_per_page, Some(2));
1486 assert_eq!(res.total_hits, Some(10));
1487 assert_eq!(res.total_pages, Some(5));
1488 Ok(())
1489 }
1490
1491 #[meilisearch_test]
1492 async fn test_query_string(client: Client, index: Index) -> Result<(), Error> {
1493 setup_test_index(&client, &index).await?;
1494
1495 let results: SearchResults<Document> = index.search().with_query("dolor").execute().await?;
1496 assert_eq!(results.hits.len(), 2);
1497 Ok(())
1498 }
1499
1500 #[meilisearch_test]
1501 async fn test_query_string_on_nested_field(client: Client, index: Index) -> Result<(), Error> {
1502 setup_test_index(&client, &index).await?;
1503
1504 let results: SearchResults<Document> =
1505 index.search().with_query("second").execute().await?;
1506
1507 assert_eq!(
1508 &Document {
1509 id: 1,
1510 value: S("dolor sit amet, consectetur adipiscing elit"),
1511 kind: S("text"),
1512 number: 10,
1513 nested: Nested { child: S("second") },
1514 _vectors: None,
1515 },
1516 &results.hits[0].result
1517 );
1518
1519 Ok(())
1520 }
1521
1522 #[meilisearch_test]
1523 async fn test_query_limit(client: Client, index: Index) -> Result<(), Error> {
1524 setup_test_index(&client, &index).await?;
1525
1526 let results: SearchResults<Document> = index.search().with_limit(5).execute().await?;
1527 assert_eq!(results.hits.len(), 5);
1528 Ok(())
1529 }
1530
1531 #[meilisearch_test]
1532 async fn test_query_page(client: Client, index: Index) -> Result<(), Error> {
1533 setup_test_index(&client, &index).await?;
1534
1535 let results: SearchResults<Document> = index.search().with_page(2).execute().await?;
1536 assert_eq!(results.page, Some(2));
1537 assert_eq!(results.hits_per_page, Some(20));
1538 Ok(())
1539 }
1540
1541 #[meilisearch_test]
1542 async fn test_query_hits_per_page(client: Client, index: Index) -> Result<(), Error> {
1543 setup_test_index(&client, &index).await?;
1544
1545 let results: SearchResults<Document> =
1546 index.search().with_hits_per_page(2).execute().await?;
1547 assert_eq!(results.page, Some(1));
1548 assert_eq!(results.hits_per_page, Some(2));
1549 Ok(())
1550 }
1551
1552 #[meilisearch_test]
1553 async fn test_query_offset(client: Client, index: Index) -> Result<(), Error> {
1554 setup_test_index(&client, &index).await?;
1555
1556 let results: SearchResults<Document> = index.search().with_offset(6).execute().await?;
1557 assert_eq!(results.hits.len(), 4);
1558 Ok(())
1559 }
1560
1561 #[meilisearch_test]
1562 async fn test_query_filter(client: Client, index: Index) -> Result<(), Error> {
1563 setup_test_index(&client, &index).await?;
1564
1565 let results: SearchResults<Document> = index
1566 .search()
1567 .with_filter("value = \"The Social Network\"")
1568 .execute()
1569 .await?;
1570 assert_eq!(results.hits.len(), 1);
1571
1572 let results: SearchResults<Document> = index
1573 .search()
1574 .with_filter("NOT value = \"The Social Network\"")
1575 .execute()
1576 .await?;
1577 assert_eq!(results.hits.len(), 9);
1578 Ok(())
1579 }
1580
1581 #[meilisearch_test]
1582 async fn test_query_filter_with_array(client: Client, index: Index) -> Result<(), Error> {
1583 setup_test_index(&client, &index).await?;
1584
1585 let results: SearchResults<Document> = index
1586 .search()
1587 .with_array_filter(vec![
1588 "value = \"The Social Network\"",
1589 "value = \"The Social Network\"",
1590 ])
1591 .execute()
1592 .await?;
1593 assert_eq!(results.hits.len(), 1);
1594
1595 Ok(())
1596 }
1597
1598 #[meilisearch_test]
1599 async fn test_query_facet_distribution(client: Client, index: Index) -> Result<(), Error> {
1600 setup_test_index(&client, &index).await?;
1601
1602 let mut query = SearchQuery::new(&index);
1603 query.with_facets(Selectors::All);
1604 let results: SearchResults<Document> = index.execute_query(&query).await?;
1605 assert_eq!(
1606 results
1607 .facet_distribution
1608 .unwrap()
1609 .get("kind")
1610 .unwrap()
1611 .get("title")
1612 .unwrap(),
1613 &8
1614 );
1615
1616 let mut query = SearchQuery::new(&index);
1617 query.with_facets(Selectors::Some(&["kind"]));
1618 let results: SearchResults<Document> = index.execute_query(&query).await?;
1619 assert_eq!(
1620 results
1621 .facet_distribution
1622 .clone()
1623 .unwrap()
1624 .get("kind")
1625 .unwrap()
1626 .get("title")
1627 .unwrap(),
1628 &8
1629 );
1630 assert_eq!(
1631 results
1632 .facet_distribution
1633 .unwrap()
1634 .get("kind")
1635 .unwrap()
1636 .get("text")
1637 .unwrap(),
1638 &2
1639 );
1640 Ok(())
1641 }
1642
1643 #[meilisearch_test]
1644 async fn test_query_attributes_to_retrieve(client: Client, index: Index) -> Result<(), Error> {
1645 setup_test_index(&client, &index).await?;
1646
1647 let results: SearchResults<Document> = index
1648 .search()
1649 .with_attributes_to_retrieve(Selectors::All)
1650 .execute()
1651 .await?;
1652 assert_eq!(results.hits.len(), 10);
1653
1654 let mut query = SearchQuery::new(&index);
1655 query.with_attributes_to_retrieve(Selectors::Some(&["kind", "id"])); assert!(index.execute_query::<Document>(&query).await.is_err()); Ok(())
1658 }
1659
1660 #[meilisearch_test]
1661 async fn test_query_sort(client: Client, index: Index) -> Result<(), Error> {
1662 setup_test_index(&client, &index).await?;
1663
1664 let mut query = SearchQuery::new(&index);
1665 query.with_query("harry potter");
1666 query.with_sort(&["title:desc"]);
1667 let results: SearchResults<Document> = index.execute_query(&query).await?;
1668 assert_eq!(results.hits.len(), 7);
1669 Ok(())
1670 }
1671
1672 #[meilisearch_test]
1673 async fn test_query_attributes_to_crop(client: Client, index: Index) -> Result<(), Error> {
1674 setup_test_index(&client, &index).await?;
1675
1676 let mut query = SearchQuery::new(&index);
1677 query.with_query("lorem ipsum");
1678 query.with_attributes_to_crop(Selectors::All);
1679 let results: SearchResults<Document> = index.execute_query(&query).await?;
1680 assert_eq!(
1681 &Document {
1682 id: 0,
1683 value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do…"),
1684 kind: S("text"),
1685 number: 0,
1686 nested: Nested { child: S("first") },
1687 _vectors: None,
1688 },
1689 results.hits[0].formatted_result.as_ref().unwrap()
1690 );
1691
1692 let mut query = SearchQuery::new(&index);
1693 query.with_query("lorem ipsum");
1694 query.with_attributes_to_crop(Selectors::Some(&[("value", Some(5)), ("kind", None)]));
1695 let results: SearchResults<Document> = index.execute_query(&query).await?;
1696 assert_eq!(
1697 &Document {
1698 id: 0,
1699 value: S("Lorem ipsum dolor sit amet…"),
1700 kind: S("text"),
1701 number: 0,
1702 nested: Nested { child: S("first") },
1703 _vectors: None,
1704 },
1705 results.hits[0].formatted_result.as_ref().unwrap()
1706 );
1707 Ok(())
1708 }
1709
1710 #[meilisearch_test]
1711 async fn test_query_crop_length(client: Client, index: Index) -> Result<(), Error> {
1712 setup_test_index(&client, &index).await?;
1713
1714 let mut query = SearchQuery::new(&index);
1715 query.with_query("lorem ipsum");
1716 query.with_attributes_to_crop(Selectors::All);
1717 query.with_crop_length(200);
1718 let results: SearchResults<Document> = index.execute_query(&query).await?;
1719 assert_eq!(&Document {
1720 id: 0,
1721 value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
1722 kind: S("text"),
1723 number: 0,
1724 nested: Nested { child: S("first") },
1725 _vectors: None,
1726 },
1727 results.hits[0].formatted_result.as_ref().unwrap());
1728
1729 let mut query = SearchQuery::new(&index);
1730 query.with_query("lorem ipsum");
1731 query.with_attributes_to_crop(Selectors::All);
1732 query.with_crop_length(5);
1733 let results: SearchResults<Document> = index.execute_query(&query).await?;
1734 assert_eq!(
1735 &Document {
1736 id: 0,
1737 value: S("Lorem ipsum dolor sit amet…"),
1738 kind: S("text"),
1739 number: 0,
1740 nested: Nested { child: S("first") },
1741 _vectors: None,
1742 },
1743 results.hits[0].formatted_result.as_ref().unwrap()
1744 );
1745 Ok(())
1746 }
1747
1748 #[meilisearch_test]
1749 async fn test_query_customized_crop_marker(client: Client, index: Index) -> Result<(), Error> {
1750 setup_test_index(&client, &index).await?;
1751
1752 let mut query = SearchQuery::new(&index);
1753 query.with_query("sed do eiusmod");
1754 query.with_attributes_to_crop(Selectors::All);
1755 query.with_crop_length(6);
1756 query.with_crop_marker("(ꈍᴗꈍ)");
1757
1758 let results: SearchResults<Document> = index.execute_query(&query).await?;
1759
1760 assert_eq!(
1761 &Document {
1762 id: 0,
1763 value: S("(ꈍᴗꈍ)sed do eiusmod tempor incididunt ut(ꈍᴗꈍ)"),
1764 kind: S("text"),
1765 number: 0,
1766 nested: Nested { child: S("first") },
1767 _vectors: None,
1768 },
1769 results.hits[0].formatted_result.as_ref().unwrap()
1770 );
1771 Ok(())
1772 }
1773
1774 #[meilisearch_test]
1775 async fn test_query_customized_highlight_pre_tag(
1776 client: Client,
1777 index: Index,
1778 ) -> Result<(), Error> {
1779 setup_test_index(&client, &index).await?;
1780
1781 let mut query = SearchQuery::new(&index);
1782 query.with_query("Social");
1783 query.with_attributes_to_highlight(Selectors::All);
1784 query.with_highlight_pre_tag("(⊃。•́‿•̀。)⊃ ");
1785 query.with_highlight_post_tag(" ⊂(´• ω •`⊂)");
1786
1787 let results: SearchResults<Document> = index.execute_query(&query).await?;
1788 assert_eq!(
1789 &Document {
1790 id: 2,
1791 value: S("The (⊃。•́‿•̀。)⊃ Social ⊂(´• ω •`⊂) Network"),
1792 kind: S("title"),
1793 number: 20,
1794 nested: Nested { child: S("third") },
1795 _vectors: None,
1796 },
1797 results.hits[0].formatted_result.as_ref().unwrap()
1798 );
1799
1800 Ok(())
1801 }
1802
1803 #[meilisearch_test]
1804 async fn test_query_attributes_to_highlight(client: Client, index: Index) -> Result<(), Error> {
1805 setup_test_index(&client, &index).await?;
1806
1807 let mut query = SearchQuery::new(&index);
1808 query.with_query("dolor text");
1809 query.with_attributes_to_highlight(Selectors::All);
1810 let results: SearchResults<Document> = index.execute_query(&query).await?;
1811 assert_eq!(
1812 &Document {
1813 id: 1,
1814 value: S("<em>dolor</em> sit amet, consectetur adipiscing elit"),
1815 kind: S("<em>text</em>"),
1816 number: 10,
1817 nested: Nested { child: S("second") },
1818 _vectors: None,
1819 },
1820 results.hits[0].formatted_result.as_ref().unwrap(),
1821 );
1822
1823 let mut query = SearchQuery::new(&index);
1824 query.with_query("dolor text");
1825 query.with_attributes_to_highlight(Selectors::Some(&["value"]));
1826 let results: SearchResults<Document> = index.execute_query(&query).await?;
1827 assert_eq!(
1828 &Document {
1829 id: 1,
1830 value: S("<em>dolor</em> sit amet, consectetur adipiscing elit"),
1831 kind: S("text"),
1832 number: 10,
1833 nested: Nested { child: S("second") },
1834 _vectors: None,
1835 },
1836 results.hits[0].formatted_result.as_ref().unwrap()
1837 );
1838 Ok(())
1839 }
1840
1841 #[meilisearch_test]
1842 async fn test_query_show_matches_position(client: Client, index: Index) -> Result<(), Error> {
1843 setup_test_index(&client, &index).await?;
1844
1845 let mut query = SearchQuery::new(&index);
1846 query.with_query("dolor text");
1847 query.with_show_matches_position(true);
1848 let results: SearchResults<Document> = index.execute_query(&query).await?;
1849 assert_eq!(results.hits[0].matches_position.as_ref().unwrap().len(), 2);
1850 assert_eq!(
1851 results.hits[0]
1852 .matches_position
1853 .as_ref()
1854 .unwrap()
1855 .get("value")
1856 .unwrap(),
1857 &vec![MatchRange {
1858 start: 0,
1859 length: 5,
1860 indices: None,
1861 }]
1862 );
1863 Ok(())
1864 }
1865
1866 #[meilisearch_test]
1867 async fn test_query_show_ranking_score(client: Client, index: Index) -> Result<(), Error> {
1868 setup_test_index(&client, &index).await?;
1869
1870 let mut query = SearchQuery::new(&index);
1871 query.with_query("dolor text");
1872 query.with_show_ranking_score(true);
1873 let results: SearchResults<Document> = index.execute_query(&query).await?;
1874 assert!(results.hits[0].ranking_score.is_some());
1875 Ok(())
1876 }
1877
1878 #[meilisearch_test]
1879 async fn test_query_show_ranking_score_details(
1880 client: Client,
1881 index: Index,
1882 ) -> Result<(), Error> {
1883 setup_test_index(&client, &index).await?;
1884
1885 let mut query = SearchQuery::new(&index);
1886 query.with_query("dolor text");
1887 query.with_show_ranking_score_details(true);
1888 let results: SearchResults<Document> = index.execute_query(&query).await.unwrap();
1889 assert!(results.hits[0].ranking_score_details.is_some());
1890 Ok(())
1891 }
1892
1893 #[meilisearch_test]
1894 async fn test_query_show_ranking_score_threshold(
1895 client: Client,
1896 index: Index,
1897 ) -> Result<(), Error> {
1898 setup_test_index(&client, &index).await?;
1899
1900 let mut query = SearchQuery::new(&index);
1901 query.with_query("dolor text");
1902 query.with_ranking_score_threshold(1.0);
1903 let results: SearchResults<Document> = index.execute_query(&query).await.unwrap();
1904 assert!(results.hits.is_empty());
1905 Ok(())
1906 }
1907
1908 #[meilisearch_test]
1909 async fn test_query_locales(client: Client, index: Index) -> Result<(), Error> {
1910 setup_test_index(&client, &index).await?;
1911
1912 let mut query = SearchQuery::new(&index);
1913 query.with_query("Harry Styles");
1914 query.with_locales(&["eng"]);
1915 let results: SearchResults<Document> = index.execute_query(&query).await.unwrap();
1916 assert_eq!(results.hits.len(), 7);
1917 Ok(())
1918 }
1919
1920 #[meilisearch_test]
1921 async fn test_phrase_search(client: Client, index: Index) -> Result<(), Error> {
1922 setup_test_index(&client, &index).await?;
1923
1924 let mut query = SearchQuery::new(&index);
1925 query.with_query("harry \"of Fire\"");
1926 let results: SearchResults<Document> = index.execute_query(&query).await?;
1927
1928 assert_eq!(results.hits.len(), 1);
1929 Ok(())
1930 }
1931
1932 #[meilisearch_test]
1933 async fn test_matching_strategy_all(client: Client, index: Index) -> Result<(), Error> {
1934 setup_test_index(&client, &index).await?;
1935
1936 let results = SearchQuery::new(&index)
1937 .with_query("Harry Styles")
1938 .with_matching_strategy(MatchingStrategies::ALL)
1939 .execute::<Document>()
1940 .await
1941 .unwrap();
1942
1943 assert_eq!(results.hits.len(), 0);
1944 Ok(())
1945 }
1946
1947 #[meilisearch_test]
1948 async fn test_matching_strategy_last(client: Client, index: Index) -> Result<(), Error> {
1949 setup_test_index(&client, &index).await?;
1950
1951 let results = SearchQuery::new(&index)
1952 .with_query("Harry Styles")
1953 .with_matching_strategy(MatchingStrategies::LAST)
1954 .execute::<Document>()
1955 .await
1956 .unwrap();
1957
1958 assert_eq!(results.hits.len(), 7);
1959 Ok(())
1960 }
1961
1962 #[meilisearch_test]
1963 async fn test_matching_strategy_frequency(client: Client, index: Index) -> Result<(), Error> {
1964 setup_test_index(&client, &index).await?;
1965
1966 let results = SearchQuery::new(&index)
1967 .with_query("Harry Styles")
1968 .with_matching_strategy(MatchingStrategies::FREQUENCY)
1969 .execute::<Document>()
1970 .await
1971 .unwrap();
1972
1973 assert_eq!(results.hits.len(), 7);
1974 Ok(())
1975 }
1976
1977 #[meilisearch_test]
1978 async fn test_distinct(client: Client, index: Index) -> Result<(), Error> {
1979 setup_test_index(&client, &index).await?;
1980
1981 let results = SearchQuery::new(&index)
1982 .with_distinct("kind")
1983 .execute::<Document>()
1984 .await
1985 .unwrap();
1986
1987 assert_eq!(results.hits.len(), 2);
1988 Ok(())
1989 }
1990
1991 #[meilisearch_test]
1992 async fn test_generate_tenant_token_from_client(
1993 client: Client,
1994 index: Index,
1995 ) -> Result<(), Error> {
1996 setup_test_index(&client, &index).await?;
1997
1998 let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1999 let key = KeyBuilder::new()
2000 .with_action(Action::All)
2001 .with_index("*")
2002 .execute(&client)
2003 .await
2004 .unwrap();
2005 let allowed_client = Client::new(meilisearch_url, Some(key.key)).unwrap();
2006
2007 let search_rules = vec![
2008 json!({ "*": {}}),
2009 json!({ "*": Value::Null }),
2010 json!(["*"]),
2011 json!({ "*": { "filter": "kind = text" } }),
2012 json!([index.uid.to_string()]),
2013 ];
2014
2015 for rules in search_rules {
2016 let token = allowed_client
2017 .generate_tenant_token(key.uid.clone(), rules, None, None)
2018 .expect("Cannot generate tenant token.");
2019
2020 let new_client = Client::new(meilisearch_url, Some(token.clone())).unwrap();
2021
2022 let result: SearchResults<Document> = new_client
2023 .index(index.uid.to_string())
2024 .search()
2025 .execute()
2026 .await?;
2027
2028 assert!(!result.hits.is_empty());
2029 }
2030
2031 Ok(())
2032 }
2033
2034 #[meilisearch_test]
2035 async fn test_facet_search_base(client: Client, index: Index) -> Result<(), Error> {
2036 setup_test_index(&client, &index).await?;
2037 let res = index.facet_search("kind").execute().await?;
2038 assert_eq!(res.facet_hits.len(), 2);
2039 Ok(())
2040 }
2041
2042 #[meilisearch_test]
2043 async fn test_facet_search_with_exhaustive_facet_count(
2044 client: Client,
2045 index: Index,
2046 ) -> Result<(), Error> {
2047 setup_test_index(&client, &index).await?;
2048 let res = index
2049 .facet_search("kind")
2050 .with_exhaustive_facet_count(true)
2051 .execute()
2052 .await?;
2053 assert_eq!(res.facet_hits.len(), 2);
2054 Ok(())
2055 }
2056
2057 #[meilisearch_test]
2058 async fn test_search_with_exhaustive_facet_count(
2059 client: Client,
2060 index: Index,
2061 ) -> Result<(), Error> {
2062 setup_test_index(&client, &index).await?;
2063
2064 let mut query = SearchQuery::new(&index);
2067 query
2068 .with_facets(Selectors::Some(&["kind"]))
2069 .with_exhaustive_facet_count(true);
2070
2071 let res = index.execute_query::<Document>(&query).await;
2072 match res {
2073 Ok(results) => {
2074 assert!(results.exhaustive_facet_count.is_some());
2075 Ok(())
2076 }
2077 Err(error)
2078 if matches!(
2079 error,
2080 Error::Meilisearch(MeilisearchError {
2081 error_code: ErrorCode::BadRequest,
2082 ..
2083 })
2084 ) =>
2085 {
2086 Ok(())
2088 }
2089 Err(e) => Err(e),
2090 }
2091 }
2092
2093 #[test]
2094 fn test_search_query_serialization_exhaustive_facet_count() {
2095 let client = Client::new(
2097 option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"),
2098 Some(option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey")),
2099 )
2100 .unwrap();
2101 let index = client.index("dummy");
2102
2103 let mut query = SearchQuery::new(&index);
2104 query
2105 .with_facets(Selectors::Some(&["kind"]))
2106 .with_exhaustive_facet_count(true);
2107
2108 let v = serde_json::to_value(&query).unwrap();
2109 assert_eq!(
2110 v.get("exhaustiveFacetCount").and_then(|b| b.as_bool()),
2111 Some(true)
2112 );
2113 }
2114
2115 #[meilisearch_test]
2116 async fn test_facet_search_with_facet_query(client: Client, index: Index) -> Result<(), Error> {
2117 setup_test_index(&client, &index).await?;
2118 let res = index
2119 .facet_search("kind")
2120 .with_facet_query("title")
2121 .execute()
2122 .await?;
2123 assert_eq!(res.facet_hits.len(), 1);
2124 assert_eq!(res.facet_hits[0].value, "title");
2125 assert_eq!(res.facet_hits[0].count, 8);
2126 Ok(())
2127 }
2128
2129 #[meilisearch_test]
2130 async fn test_facet_search_with_attributes_to_search_on(
2131 client: Client,
2132 index: Index,
2133 ) -> Result<(), Error> {
2134 setup_test_index(&client, &index).await?;
2135 let res = index
2136 .facet_search("kind")
2137 .with_search_query("title")
2138 .with_attributes_to_search_on(&["value"])
2139 .execute()
2140 .await?;
2141 println!("{:?}", res);
2142 assert_eq!(res.facet_hits.len(), 0);
2143
2144 let res = index
2145 .facet_search("kind")
2146 .with_search_query("title")
2147 .with_attributes_to_search_on(&["kind"])
2148 .execute()
2149 .await?;
2150 assert_eq!(res.facet_hits.len(), 1);
2151 Ok(())
2152 }
2153
2154 #[meilisearch_test]
2155 async fn test_with_vectors(client: Client, index: Index) -> Result<(), Error> {
2156 setup_embedder(&client, &index).await?;
2157 setup_test_index(&client, &index).await?;
2158
2159 let results: SearchResults<Document> = index
2160 .search()
2161 .with_query("lorem ipsum")
2162 .with_retrieve_vectors(true)
2163 .execute()
2164 .await?;
2165 assert_eq!(results.hits.len(), 1);
2166 let expected = Some(Vectors::from(vectorize(false, 0)));
2167 assert_eq!(results.hits[0].result._vectors, expected);
2168
2169 let results: SearchResults<Document> = index
2170 .search()
2171 .with_query("lorem ipsum")
2172 .with_retrieve_vectors(false)
2173 .execute()
2174 .await?;
2175 assert_eq!(results.hits.len(), 1);
2176 assert_eq!(results.hits[0].result._vectors, None);
2177 Ok(())
2178 }
2179
2180 #[meilisearch_test]
2181 async fn test_query_vector_in_response(client: Client, index: Index) -> Result<(), Error> {
2182 setup_embedder(&client, &index).await?;
2183 setup_test_index(&client, &index).await?;
2184
2185 let mut query = SearchQuery::new(&index);
2186 let qv = vectorize(false, 0);
2187 query
2188 .with_hybrid("default", 1.0)
2189 .with_vector(&qv)
2190 .with_retrieve_vectors(true);
2191
2192 let results: SearchResults<Document> = index.execute_query(&query).await?;
2193
2194 if std::env::var("MSDK_DEBUG_RAW_SEARCH").ok().as_deref() == Some("1")
2195 && results.query_vector.is_none()
2196 {
2197 use crate::request::Method;
2198 let url = format!("{}/indexes/{}/search", index.client.get_host(), index.uid);
2199 let raw: serde_json::Value = index
2200 .client
2201 .http_client
2202 .request::<(), &SearchQuery<_>, serde_json::Value>(
2203 &url,
2204 Method::Post {
2205 body: &query,
2206 query: (),
2207 },
2208 200,
2209 )
2210 .await
2211 .unwrap();
2212 eprintln!("DEBUG raw search response: {}", raw);
2213 }
2214
2215 assert!(results.query_vector.is_some());
2216 assert_eq!(results.query_vector.as_ref().unwrap().len(), 11);
2217 Ok(())
2218 }
2219
2220 #[meilisearch_test]
2221 async fn test_hybrid(client: Client, index: Index) -> Result<(), Error> {
2222 setup_embedder(&client, &index).await?;
2223 setup_test_index(&client, &index).await?;
2224
2225 let results: SearchResults<Document> = index
2228 .search()
2229 .with_hybrid("default", 1.0)
2230 .with_vector(&vectorize(true, 0))
2231 .execute()
2232 .await?;
2233 let ids = results
2234 .hits
2235 .iter()
2236 .map(|hit| hit.result.id)
2237 .collect::<Vec<_>>();
2238 assert_eq!(ids, vec![0, 3, 4, 5, 6, 7, 8, 9, 1, 2]);
2239
2240 Ok(())
2241 }
2242
2243 #[meilisearch_test]
2244 async fn test_facet_search_with_search_query(
2245 client: Client,
2246 index: Index,
2247 ) -> Result<(), Error> {
2248 setup_test_index(&client, &index).await?;
2249 let res = index
2250 .facet_search("kind")
2251 .with_search_query("Harry Potter")
2252 .execute()
2253 .await?;
2254 assert_eq!(res.facet_hits.len(), 1);
2255 assert_eq!(res.facet_hits[0].value, "title");
2256 assert_eq!(res.facet_hits[0].count, 7);
2257 Ok(())
2258 }
2259
2260 #[meilisearch_test]
2261 async fn test_facet_search_with_filter(client: Client, index: Index) -> Result<(), Error> {
2262 setup_test_index(&client, &index).await?;
2263 let res = index
2264 .facet_search("kind")
2265 .with_filter("value = \"The Social Network\"")
2266 .execute()
2267 .await?;
2268 assert_eq!(res.facet_hits.len(), 1);
2269 assert_eq!(res.facet_hits[0].value, "title");
2270 assert_eq!(res.facet_hits[0].count, 1);
2271
2272 let res = index
2273 .facet_search("kind")
2274 .with_filter("NOT value = \"The Social Network\"")
2275 .execute()
2276 .await?;
2277 assert_eq!(res.facet_hits.len(), 2);
2278 Ok(())
2279 }
2280
2281 #[meilisearch_test]
2282 async fn test_facet_search_with_array_filter(
2283 client: Client,
2284 index: Index,
2285 ) -> Result<(), Error> {
2286 setup_test_index(&client, &index).await?;
2287 let res = index
2288 .facet_search("kind")
2289 .with_array_filter(vec![
2290 "value = \"The Social Network\"",
2291 "value = \"The Social Network\"",
2292 ])
2293 .execute()
2294 .await?;
2295 assert_eq!(res.facet_hits.len(), 1);
2296 assert_eq!(res.facet_hits[0].value, "title");
2297 assert_eq!(res.facet_hits[0].count, 1);
2298 Ok(())
2299 }
2300
2301 #[meilisearch_test]
2302 async fn test_facet_search_with_matching_strategy_all(
2303 client: Client,
2304 index: Index,
2305 ) -> Result<(), Error> {
2306 setup_test_index(&client, &index).await?;
2307 let res = index
2308 .facet_search("kind")
2309 .with_search_query("Harry Styles")
2310 .with_matching_strategy(MatchingStrategies::ALL)
2311 .execute()
2312 .await?;
2313 assert_eq!(res.facet_hits.len(), 0);
2314 Ok(())
2315 }
2316
2317 #[meilisearch_test]
2318 async fn test_facet_search_with_matching_strategy_last(
2319 client: Client,
2320 index: Index,
2321 ) -> Result<(), Error> {
2322 setup_test_index(&client, &index).await?;
2323 let res = index
2324 .facet_search("kind")
2325 .with_search_query("Harry Styles")
2326 .with_matching_strategy(MatchingStrategies::LAST)
2327 .execute()
2328 .await?;
2329 assert_eq!(res.facet_hits.len(), 1);
2330 assert_eq!(res.facet_hits[0].value, "title");
2331 assert_eq!(res.facet_hits[0].count, 7);
2332 Ok(())
2333 }
2334
2335 #[meilisearch_test]
2336 async fn test_search_with_show_performance_details(
2337 client: Client,
2338 index: Index,
2339 ) -> Result<(), Error> {
2340 setup_test_index(&client, &index).await?;
2341
2342 let res = index
2343 .search()
2344 .with_show_performance_details(true)
2345 .with_query("Lorem")
2346 .execute::<Value>()
2347 .await?;
2348
2349 assert!(res.performance_details.is_some());
2350
2351 Ok(())
2352 }
2353
2354 #[meilisearch_test]
2355 async fn test_multi_search_with_show_performance_details(
2356 client: Client,
2357 index: Index,
2358 ) -> Result<(), Error> {
2359 setup_test_index(&client, &index).await?;
2360 let search_query_1 = SearchQuery::new(&index)
2361 .with_query("Sorcerer's Stone")
2362 .with_show_performance_details(true)
2363 .build();
2364 let search_query_2 = SearchQuery::new(&index)
2365 .with_query("Chamber of Secrets")
2366 .build();
2367
2368 let response = client
2369 .multi_search()
2370 .with_search_query(search_query_1)
2371 .with_search_query(search_query_2)
2372 .execute::<Document>()
2373 .await
2374 .unwrap();
2375
2376 assert!(response.results[0].performance_details.is_some());
2377 Ok(())
2378 }
2379
2380 #[meilisearch_test]
2381 async fn test_federated_multi_search_with_show_performance_details(
2382 client: Client,
2383 test_index: Index,
2384 ) -> Result<(), Error> {
2385 setup_test_index(&client, &test_index).await?;
2386
2387 let response = client
2388 .multi_search()
2389 .with_federation(FederationOptions {
2390 show_performance_details: Some(true),
2391 ..Default::default()
2392 })
2393 .execute::<Value>()
2394 .await?;
2395
2396 assert!(response.performance_details.is_some());
2397
2398 Ok(())
2399 }
2400}