meilisearch_sdk/
search.rs

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    /// If the match is somewhere inside a (potentially nested) array, this
19    /// field is set to the index/indices of the matched element(s).
20    ///
21    /// In the simple case, if the field has the value `["foo", "bar"]`, then
22    /// searching for `ba` will return `indices: Some([1])`. If the value
23    /// contains multiple nested arrays, the first index describes the most
24    /// top-level array, and descending from there. For example, if the value is
25    /// `[{ x: "cat" }, "bear", { y: ["dog", "fox"] }]`, searching for `dog`
26    /// will return `indices: Some([2, 0])`.
27    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/// A single result.
55///
56/// Contains the complete object, optionally the formatted object, and optionally an object that contains information about the matches.
57#[derive(Serialize, Deserialize, Debug, Clone)]
58pub struct SearchResult<T> {
59    /// The full result.
60    #[serde(flatten)]
61    pub result: T,
62
63    /// The formatted result.
64    #[serde(rename = "_formatted", skip_serializing_if = "Option::is_none")]
65    pub formatted_result: Option<Map<String, Value>>,
66
67    /// The object that contains information about the matches.
68    #[serde(rename = "_matchesPosition", skip_serializing_if = "Option::is_none")]
69    pub matches_position: Option<HashMap<String, Vec<MatchRange>>>,
70
71    /// The relevancy score of the match.
72    #[serde(rename = "_rankingScore", skip_serializing_if = "Option::is_none")]
73    pub ranking_score: Option<f64>,
74
75    /// A detailed global ranking score field
76    #[serde(
77        rename = "_rankingScoreDetails",
78        skip_serializing_if = "Option::is_none"
79    )]
80    pub ranking_score_details: Option<Map<String, Value>>,
81
82    /// Only returned for federated multi search.
83    #[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")]
96/// A struct containing search results and other information about the search.
97pub struct SearchResults<T> {
98    /// Results of the query.
99    pub hits: Vec<SearchResult<T>>,
100    /// Number of documents skipped.
101    pub offset: Option<usize>,
102    /// Number of results returned.
103    pub limit: Option<usize>,
104    /// Estimated total number of matches.
105    pub estimated_total_hits: Option<usize>,
106    /// Current page number
107    pub page: Option<usize>,
108    /// Maximum number of hits in a page.
109    pub hits_per_page: Option<usize>,
110    /// Exhaustive number of matches.
111    pub total_hits: Option<usize>,
112    /// Exhaustive number of pages.
113    pub total_pages: Option<usize>,
114    /// Distribution of the given facets.
115    pub facet_distribution: Option<HashMap<String, HashMap<String, usize>>>,
116    /// facet stats of the numerical facets requested in the `facet` search parameter.
117    pub facet_stats: Option<HashMap<String, FacetStats>>,
118    /// Indicates whether facet counts are exhaustive (exact) rather than estimated.
119    /// Present when the `exhaustiveFacetCount` search parameter is used.
120    pub exhaustive_facet_count: Option<bool>,
121    /// Processing time of the query.
122    pub processing_time_ms: usize,
123    /// Query originating the response.
124    pub query: String,
125    /// Index uid on which the search was made.
126    pub index_uid: Option<String>,
127    /// The query vector returned when `retrieveVectors` is enabled.
128    /// Accept multiple possible field names to be forward/backward compatible with server variations.
129    #[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}
139
140fn serialize_attributes_to_crop_with_wildcard<S: Serializer>(
141    data: &Option<Selectors<&[AttributeToCrop]>>,
142    s: S,
143) -> Result<S::Ok, S::Error> {
144    match data {
145        Some(Selectors::All) => ["*"].serialize(s),
146        Some(Selectors::Some(data)) => {
147            let results = data
148                .iter()
149                .map(|(name, value)| {
150                    let mut result = (*name).to_string();
151                    if let Some(value) = value {
152                        result.push(':');
153                        result.push_str(value.to_string().as_str());
154                    }
155                    result
156                })
157                .collect::<Vec<_>>();
158            results.serialize(s)
159        }
160        None => s.serialize_none(),
161    }
162}
163
164/// Some list fields in a `SearchQuery` can be set to a wildcard value.
165///
166/// This structure allows you to choose between the wildcard value and an exhaustive list of selectors.
167#[derive(Debug, Clone)]
168pub enum Selectors<T> {
169    /// A list of selectors.
170    Some(T),
171    /// The wildcard.
172    All,
173}
174
175impl<T: Serialize> Serialize for Selectors<T> {
176    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
177        match self {
178            Selectors::Some(data) => data.serialize(s),
179            Selectors::All => ["*"].serialize(s),
180        }
181    }
182}
183
184/// Configures Meilisearch to return search results based on a query’s meaning and context
185#[derive(Debug, Serialize, Clone)]
186#[serde(rename_all = "camelCase")]
187pub struct HybridSearch<'a> {
188    /// Indicates one of the embedders configured for the queried index
189    pub embedder: &'a str,
190    /// number between `0` and `1`:
191    /// - `0.0` indicates full keyword search
192    /// - `1.0` indicates full semantic search
193    pub semantic_ratio: f32,
194}
195
196type AttributeToCrop<'a> = (&'a str, Option<usize>);
197
198/// A struct representing a query.
199///
200/// You can add search parameters using the builder syntax.
201///
202/// See [this page](https://www.meilisearch.com/docs/reference/api/search#query-q) for the official list and description of all parameters.
203///
204/// # Examples
205///
206/// ```
207/// # use serde::{Serialize, Deserialize};
208/// # use meilisearch_sdk::{client::Client, search::*, indexes::Index};
209/// #
210/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
211/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
212/// #
213/// #[derive(Serialize, Deserialize, Debug)]
214/// struct Movie {
215///     name: String,
216///     description: String,
217/// }
218/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
219/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
220/// # let index = client
221/// #  .create_index("search_query_builder", None)
222/// #  .await
223/// #  .unwrap()
224/// #  .wait_for_completion(&client, None, None)
225/// #  .await.unwrap()
226/// #  .try_make_index(&client)
227/// #  .unwrap();
228///
229/// let mut res = SearchQuery::new(&index)
230///     .with_query("space")
231///     .with_offset(42)
232///     .with_limit(21)
233///     .execute::<Movie>()
234///     .await
235///     .unwrap();
236///
237/// assert_eq!(res.limit, Some(21));
238/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
239/// # });
240/// ```
241///
242/// ```
243/// # use meilisearch_sdk::{client::Client, search::*, indexes::Index};
244/// #
245/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
246/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
247/// #
248/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
249/// # let index = client.index("search_query_builder_build");
250/// let query = index.search()
251///     .with_query("space")
252///     .with_offset(42)
253///     .with_limit(21)
254///     .build(); // you can also execute() instead of build()
255/// ```
256#[derive(Debug, Serialize, Clone)]
257#[serde(rename_all = "camelCase")]
258pub struct SearchQuery<'a, Http: HttpClient> {
259    #[serde(skip_serializing)]
260    index: &'a Index<Http>,
261    /// The text that will be searched for among the documents.
262    #[serde(skip_serializing_if = "Option::is_none")]
263    #[serde(rename = "q")]
264    pub query: Option<&'a str>,
265    /// The number of documents to skip.
266    ///
267    /// If the value of the parameter `offset` is `n`, the `n` first documents (ordered by relevance) will not be returned.
268    /// This is helpful for pagination.
269    ///
270    /// Example: If you want to skip the first document, set offset to `1`.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub offset: Option<usize>,
273    /// The maximum number of documents returned.
274    ///
275    /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response.
276    /// This is helpful for pagination.
277    ///
278    /// Example: If you don't want to get more than two documents, set limit to `2`.
279    ///
280    /// **Default: `20`**
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub limit: Option<usize>,
283    /// The page number on which you paginate.
284    ///
285    /// Pagination starts at 1. If page is 0, no results are returned.
286    ///
287    /// **Default: None unless `hits_per_page` is defined, in which case page is `1`**
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub page: Option<usize>,
290    /// The maximum number of results in a page. A page can contain less results than the number of hits_per_page.
291    ///
292    /// **Default: None unless `page` is defined, in which case `20`**
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub hits_per_page: Option<usize>,
295    /// Filter applied to documents.
296    ///
297    /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub filter: Option<Filter<'a>>,
300    /// Facets for which to retrieve the matching count.
301    ///
302    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
303    ///
304    /// **Default: all attributes found in the documents.**
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub facets: Option<Selectors<&'a [&'a str]>>,
307    /// Attributes to sort.
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub sort: Option<&'a [&'a str]>,
310    /// Attributes to perform the search on.
311    ///
312    /// Specify the subset of searchableAttributes for a search without modifying Meilisearch’s index settings.
313    ///
314    /// **Default: all searchable attributes found in the documents.**
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub attributes_to_search_on: Option<&'a [&'a str]>,
317    /// Attributes to display in the returned documents.
318    ///
319    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
320    ///
321    /// **Default: all attributes found in the documents.**
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub attributes_to_retrieve: Option<Selectors<&'a [&'a str]>>,
324    /// Attributes whose values have to be cropped.
325    ///
326    /// Attributes are composed by the attribute name and an optional `usize` that overwrites the `crop_length` parameter.
327    ///
328    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
329    #[serde(skip_serializing_if = "Option::is_none")]
330    #[serde(serialize_with = "serialize_attributes_to_crop_with_wildcard")]
331    pub attributes_to_crop: Option<Selectors<&'a [AttributeToCrop<'a>]>>,
332    /// Maximum number of words including the matched query term(s) contained in the returned cropped value(s).
333    ///
334    /// See [attributes_to_crop](#structfield.attributes_to_crop).
335    ///
336    /// **Default: `10`**
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub crop_length: Option<usize>,
339    /// Marker at the start and the end of a cropped value.
340    ///
341    /// ex: `...middle of a crop...`
342    ///
343    /// **Default: `...`**
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub crop_marker: Option<&'a str>,
346    /// Attributes whose values will contain **highlighted matching terms**.
347    ///
348    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub attributes_to_highlight: Option<Selectors<&'a [&'a str]>>,
351    /// Tag in front of a highlighted term.
352    ///
353    /// ex: `<mytag>hello world`
354    ///
355    /// **Default: `<em>`**
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub highlight_pre_tag: Option<&'a str>,
358    /// Tag after a highlighted term.
359    ///
360    /// ex: `hello world</ mytag>`
361    ///
362    /// **Default: `</em>`**
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub highlight_post_tag: Option<&'a str>,
365    /// Defines whether an object that contains information about the matches should be returned or not.
366    ///
367    /// **Default: `false`**
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub show_matches_position: Option<bool>,
370
371    /// Defines whether to show the relevancy score of the match.
372    ///
373    /// **Default: `false`**
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub show_ranking_score: Option<bool>,
376
377    ///Adds a detailed global ranking score field to each document.
378    ///
379    /// **Default: `false`**
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub show_ranking_score_details: Option<bool>,
382
383    /// Defines the strategy on how to handle queries containing multiple words.
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub matching_strategy: Option<MatchingStrategies>,
386
387    ///Defines one attribute in the filterableAttributes list as a distinct attribute.
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub distinct: Option<&'a str>,
390
391    ///Excludes results below the specified ranking score.
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub ranking_score_threshold: Option<f64>,
394
395    /// Defines the language of the search query.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub locales: Option<&'a [&'a str]>,
398
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub(crate) index_uid: Option<&'a str>,
401
402    /// Configures Meilisearch to return search results based on a query’s meaning and context.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub hybrid: Option<HybridSearch<'a>>,
405
406    /// Use a custom vector to perform a search query.
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub vector: Option<&'a [f32]>,
409
410    /// Defines whether document embeddings are returned with search results.
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub retrieve_vectors: Option<bool>,
413
414    /// Provides multimodal data for search queries.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub media: Option<Value>,
417
418    /// Request exhaustive facet counts up to the limit defined by `maxTotalHits`.
419    ///
420    /// When set to `true`, Meilisearch computes exact facet counts instead of approximate ones.
421    /// Default is `false`.
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub exhaustive_facet_count: Option<bool>,
424
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub(crate) federation_options: Option<QueryFederationOptions>,
427}
428
429#[derive(Debug, Serialize, Clone)]
430#[serde(rename_all = "camelCase")]
431pub struct QueryFederationOptions {
432    /// Weight multiplier for this query when merging federated results
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub weight: Option<f32>,
435    /// Remote instance name to target when sharding; corresponds to a key in network.remotes
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub remote: Option<String>,
438}
439
440#[allow(missing_docs)]
441impl<'a, Http: HttpClient> SearchQuery<'a, Http> {
442    #[must_use]
443    pub fn new(index: &'a Index<Http>) -> SearchQuery<'a, Http> {
444        SearchQuery {
445            index,
446            query: None,
447            offset: None,
448            limit: None,
449            page: None,
450            hits_per_page: None,
451            filter: None,
452            sort: None,
453            facets: None,
454            attributes_to_search_on: None,
455            attributes_to_retrieve: None,
456            attributes_to_crop: None,
457            crop_length: None,
458            crop_marker: None,
459            attributes_to_highlight: None,
460            highlight_pre_tag: None,
461            highlight_post_tag: None,
462            show_matches_position: None,
463            show_ranking_score: None,
464            show_ranking_score_details: None,
465            matching_strategy: None,
466            index_uid: None,
467            hybrid: None,
468            vector: None,
469            retrieve_vectors: None,
470            media: None,
471            exhaustive_facet_count: None,
472            distinct: None,
473            ranking_score_threshold: None,
474            locales: None,
475            federation_options: None,
476        }
477    }
478
479    pub fn with_query<'b>(&'b mut self, query: &'a str) -> &'b mut SearchQuery<'a, Http> {
480        self.query = Some(query);
481        self
482    }
483
484    pub fn with_offset<'b>(&'b mut self, offset: usize) -> &'b mut SearchQuery<'a, Http> {
485        self.offset = Some(offset);
486        self
487    }
488
489    pub fn with_limit<'b>(&'b mut self, limit: usize) -> &'b mut SearchQuery<'a, Http> {
490        self.limit = Some(limit);
491        self
492    }
493
494    /// Add the page number on which to paginate.
495    ///
496    /// # Example
497    ///
498    /// ```
499    /// # use serde::{Serialize, Deserialize};
500    /// # use meilisearch_sdk::{client::*, indexes::*, search::*};
501    /// #
502    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
503    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
504    /// #
505    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
506    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
507    /// # #[derive(Serialize, Deserialize, Debug)]
508    /// # struct Movie {
509    /// #     name: String,
510    /// #     description: String,
511    /// # }
512    /// # client.create_index("search_with_page", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
513    /// let mut index = client.index("search_with_page");
514    ///
515    /// let mut query = SearchQuery::new(&index);
516    /// query.with_query("").with_page(2);
517    /// let res = query.execute::<Movie>().await.unwrap();
518    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
519    /// # });
520    /// ```
521    pub fn with_page<'b>(&'b mut self, page: usize) -> &'b mut SearchQuery<'a, Http> {
522        self.page = Some(page);
523        self
524    }
525
526    /// Add the maximum number of results per page.
527    ///
528    /// # Example
529    ///
530    /// ```
531    /// # use serde::{Serialize, Deserialize};
532    /// # use meilisearch_sdk::{client::*, indexes::*, search::*};
533    /// #
534    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
535    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
536    /// #
537    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
538    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
539    /// # #[derive(Serialize, Deserialize, Debug)]
540    /// # struct Movie {
541    /// #     name: String,
542    /// #     description: String,
543    /// # }
544    /// # client.create_index("search_with_hits_per_page", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
545    /// let mut index = client.index("search_with_hits_per_page");
546    ///
547    /// let mut query = SearchQuery::new(&index);
548    /// query.with_query("").with_hits_per_page(2);
549    /// let res = query.execute::<Movie>().await.unwrap();
550    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
551    /// # });
552    /// ```
553    pub fn with_hits_per_page<'b>(
554        &'b mut self,
555        hits_per_page: usize,
556    ) -> &'b mut SearchQuery<'a, Http> {
557        self.hits_per_page = Some(hits_per_page);
558        self
559    }
560
561    pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut SearchQuery<'a, Http> {
562        self.filter = Some(Filter::new(Either::Left(filter)));
563        self
564    }
565
566    pub fn with_array_filter<'b>(
567        &'b mut self,
568        filter: Vec<&'a str>,
569    ) -> &'b mut SearchQuery<'a, Http> {
570        self.filter = Some(Filter::new(Either::Right(filter)));
571        self
572    }
573
574    /// Defines whether document embeddings are returned with search results.
575    pub fn with_retrieve_vectors<'b>(
576        &'b mut self,
577        retrieve_vectors: bool,
578    ) -> &'b mut SearchQuery<'a, Http> {
579        self.retrieve_vectors = Some(retrieve_vectors);
580        self
581    }
582
583    pub fn with_facets<'b>(
584        &'b mut self,
585        facets: Selectors<&'a [&'a str]>,
586    ) -> &'b mut SearchQuery<'a, Http> {
587        self.facets = Some(facets);
588        self
589    }
590
591    pub fn with_sort<'b>(&'b mut self, sort: &'a [&'a str]) -> &'b mut SearchQuery<'a, Http> {
592        self.sort = Some(sort);
593        self
594    }
595
596    pub fn with_attributes_to_search_on<'b>(
597        &'b mut self,
598        attributes_to_search_on: &'a [&'a str],
599    ) -> &'b mut SearchQuery<'a, Http> {
600        self.attributes_to_search_on = Some(attributes_to_search_on);
601        self
602    }
603
604    pub fn with_attributes_to_retrieve<'b>(
605        &'b mut self,
606        attributes_to_retrieve: Selectors<&'a [&'a str]>,
607    ) -> &'b mut SearchQuery<'a, Http> {
608        self.attributes_to_retrieve = Some(attributes_to_retrieve);
609        self
610    }
611
612    pub fn with_attributes_to_crop<'b>(
613        &'b mut self,
614        attributes_to_crop: Selectors<&'a [(&'a str, Option<usize>)]>,
615    ) -> &'b mut SearchQuery<'a, Http> {
616        self.attributes_to_crop = Some(attributes_to_crop);
617        self
618    }
619
620    pub fn with_crop_length<'b>(&'b mut self, crop_length: usize) -> &'b mut SearchQuery<'a, Http> {
621        self.crop_length = Some(crop_length);
622        self
623    }
624
625    pub fn with_crop_marker<'b>(
626        &'b mut self,
627        crop_marker: &'a str,
628    ) -> &'b mut SearchQuery<'a, Http> {
629        self.crop_marker = Some(crop_marker);
630        self
631    }
632
633    pub fn with_attributes_to_highlight<'b>(
634        &'b mut self,
635        attributes_to_highlight: Selectors<&'a [&'a str]>,
636    ) -> &'b mut SearchQuery<'a, Http> {
637        self.attributes_to_highlight = Some(attributes_to_highlight);
638        self
639    }
640
641    pub fn with_highlight_pre_tag<'b>(
642        &'b mut self,
643        highlight_pre_tag: &'a str,
644    ) -> &'b mut SearchQuery<'a, Http> {
645        self.highlight_pre_tag = Some(highlight_pre_tag);
646        self
647    }
648
649    pub fn with_highlight_post_tag<'b>(
650        &'b mut self,
651        highlight_post_tag: &'a str,
652    ) -> &'b mut SearchQuery<'a, Http> {
653        self.highlight_post_tag = Some(highlight_post_tag);
654        self
655    }
656
657    pub fn with_show_matches_position<'b>(
658        &'b mut self,
659        show_matches_position: bool,
660    ) -> &'b mut SearchQuery<'a, Http> {
661        self.show_matches_position = Some(show_matches_position);
662        self
663    }
664
665    pub fn with_show_ranking_score<'b>(
666        &'b mut self,
667        show_ranking_score: bool,
668    ) -> &'b mut SearchQuery<'a, Http> {
669        self.show_ranking_score = Some(show_ranking_score);
670        self
671    }
672
673    pub fn with_show_ranking_score_details<'b>(
674        &'b mut self,
675        show_ranking_score_details: bool,
676    ) -> &'b mut SearchQuery<'a, Http> {
677        self.show_ranking_score_details = Some(show_ranking_score_details);
678        self
679    }
680
681    pub fn with_matching_strategy<'b>(
682        &'b mut self,
683        matching_strategy: MatchingStrategies,
684    ) -> &'b mut SearchQuery<'a, Http> {
685        self.matching_strategy = Some(matching_strategy);
686        self
687    }
688
689    pub fn with_index_uid<'b>(&'b mut self) -> &'b mut SearchQuery<'a, Http> {
690        self.index_uid = Some(&self.index.uid);
691        self
692    }
693
694    /// Configures Meilisearch to return search results based on a query’s meaning and context
695    pub fn with_hybrid<'b>(
696        &'b mut self,
697        embedder: &'a str,
698        semantic_ratio: f32,
699    ) -> &'b mut SearchQuery<'a, Http> {
700        self.hybrid = Some(HybridSearch {
701            embedder,
702            semantic_ratio,
703        });
704        self
705    }
706
707    /// Use a custom vector to perform a search query
708    ///
709    /// `vector` is mandatory when performing searches with `userProvided` embedders.
710    /// You may also use `vector` to override an embedder’s automatic vector generation.
711    ///
712    /// `vector` dimensions must match the dimensions of the embedder.
713    pub fn with_vector<'b>(&'b mut self, vector: &'a [f32]) -> &'b mut SearchQuery<'a, Http> {
714        self.vector = Some(vector);
715        self
716    }
717
718    /// Attach media fragments to the search query.
719    pub fn with_media<'b>(&'b mut self, media: Value) -> &'b mut SearchQuery<'a, Http> {
720        self.media = Some(media);
721        self
722    }
723
724    pub fn with_distinct<'b>(&'b mut self, distinct: &'a str) -> &'b mut SearchQuery<'a, Http> {
725        self.distinct = Some(distinct);
726        self
727    }
728
729    pub fn with_ranking_score_threshold<'b>(
730        &'b mut self,
731        ranking_score_threshold: f64,
732    ) -> &'b mut SearchQuery<'a, Http> {
733        self.ranking_score_threshold = Some(ranking_score_threshold);
734        self
735    }
736
737    pub fn with_locales<'b>(&'b mut self, locales: &'a [&'a str]) -> &'b mut SearchQuery<'a, Http> {
738        self.locales = Some(locales);
739        self
740    }
741
742    pub fn build(&mut self) -> SearchQuery<'a, Http> {
743        self.clone()
744    }
745
746    /// Request exhaustive facet count in the response.
747    pub fn with_exhaustive_facet_count<'b>(
748        &'b mut self,
749        exhaustive: bool,
750    ) -> &'b mut SearchQuery<'a, Http> {
751        self.exhaustive_facet_count = Some(exhaustive);
752        self
753    }
754
755    /// Execute the query and fetch the results.
756    pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
757        &'a self,
758    ) -> Result<SearchResults<T>, Error> {
759        self.index.execute_query::<T>(self).await
760    }
761}
762
763#[derive(Debug, Serialize, Clone)]
764#[serde(rename_all = "camelCase")]
765pub struct MultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> {
766    #[serde(skip_serializing)]
767    client: &'a Client<Http>,
768    // The weird `serialize = ""` is actually useful: without it, serde adds the
769    // bound `Http: Serialize` to the `Serialize` impl block, but that's not
770    // necessary. `SearchQuery` always implements `Serialize` (regardless of
771    // type parameter), so no bound is fine.
772    #[serde(bound(serialize = ""))]
773    pub queries: Vec<SearchQuery<'b, Http>>,
774}
775
776#[allow(missing_docs)]
777impl<'a, 'b, Http: HttpClient> MultiSearchQuery<'a, 'b, Http> {
778    #[must_use]
779    pub fn new(client: &'a Client<Http>) -> MultiSearchQuery<'a, 'b, Http> {
780        MultiSearchQuery {
781            client,
782            queries: Vec::new(),
783        }
784    }
785
786    pub fn with_search_query(
787        &mut self,
788        mut search_query: SearchQuery<'b, Http>,
789    ) -> &mut MultiSearchQuery<'a, 'b, Http> {
790        search_query.with_index_uid();
791        self.queries.push(search_query);
792        self
793    }
794
795    pub fn with_search_query_and_weight(
796        &mut self,
797        search_query: SearchQuery<'b, Http>,
798        weight: f32,
799    ) -> &mut MultiSearchQuery<'a, 'b, Http> {
800        self.with_search_query_and_options(
801            search_query,
802            QueryFederationOptions {
803                weight: Some(weight),
804                remote: None,
805            },
806        )
807    }
808
809    pub fn with_search_query_and_options(
810        &mut self,
811        mut search_query: SearchQuery<'b, Http>,
812        options: QueryFederationOptions,
813    ) -> &mut MultiSearchQuery<'a, 'b, Http> {
814        search_query.with_index_uid();
815        search_query.federation_options = Some(options);
816        self.queries.push(search_query);
817        self
818    }
819
820    /// Adds the `federation` parameter, turning the search into a federated search.
821    pub fn with_federation(
822        self,
823        federation: FederationOptions,
824    ) -> FederatedMultiSearchQuery<'a, 'b, Http> {
825        FederatedMultiSearchQuery {
826            client: self.client,
827            queries: self.queries,
828            federation: Some(federation),
829        }
830    }
831
832    /// Execute the query and fetch the results.
833    pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
834        &'a self,
835    ) -> Result<MultiSearchResponse<T>, Error> {
836        self.client.execute_multi_search_query::<T>(self).await
837    }
838}
839
840#[derive(Debug, Clone, Deserialize, Serialize)]
841pub struct MultiSearchResponse<T> {
842    pub results: Vec<SearchResults<T>>,
843}
844
845#[derive(Debug, Serialize, Clone)]
846#[serde(rename_all = "camelCase")]
847pub struct FederatedMultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> {
848    #[serde(skip_serializing)]
849    client: &'a Client<Http>,
850    #[serde(bound(serialize = ""))]
851    pub queries: Vec<SearchQuery<'b, Http>>,
852    #[serde(skip_serializing_if = "Option::is_none")]
853    pub federation: Option<FederationOptions>,
854}
855
856#[derive(Debug, Serialize, Clone, Default)]
857#[serde(rename_all = "camelCase")]
858pub struct MergeFacets {
859    #[serde(skip_serializing_if = "Option::is_none")]
860    pub max_values_per_facet: Option<usize>,
861}
862
863/// The `federation` field of the multi search API.
864/// See [the docs](https://www.meilisearch.com/docs/reference/api/multi_search#federation).
865#[derive(Debug, Serialize, Clone, Default)]
866#[serde(rename_all = "camelCase")]
867pub struct FederationOptions {
868    /// Number of documents to skip
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub offset: Option<usize>,
871
872    /// Maximum number of documents returned
873    #[serde(skip_serializing_if = "Option::is_none")]
874    pub limit: Option<usize>,
875
876    /// Display facet information for the specified indexes
877    #[serde(skip_serializing_if = "Option::is_none")]
878    pub facets_by_index: Option<HashMap<String, Vec<String>>>,
879
880    /// Request to merge the facets to enforce a maximum number of values per facet.
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub merge_facets: Option<MergeFacets>,
883}
884
885impl<'a, Http: HttpClient> FederatedMultiSearchQuery<'a, '_, Http> {
886    /// Execute the query and fetch the results.
887    pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
888        &'a self,
889    ) -> Result<FederatedMultiSearchResponse<T>, Error> {
890        self.client
891            .execute_federated_multi_search_query::<T>(self)
892            .await
893    }
894}
895
896#[derive(Debug, Clone, Default, Serialize, Deserialize)]
897pub struct ComputedFacets {
898    pub distribution: HashMap<String, HashMap<String, u64>>,
899    pub stats: HashMap<String, FacetStats>,
900}
901
902/// Returned by federated multi search.
903#[derive(Debug, Deserialize, Clone)]
904#[serde(rename_all = "camelCase")]
905pub struct FederatedMultiSearchResponse<T> {
906    /// Merged results of the query.
907    pub hits: Vec<SearchResult<T>>,
908
909    /// Number of documents skipped.
910    pub offset: usize,
911
912    /// Number of results returned.
913    pub limit: usize,
914
915    /// Estimated total number of matches.
916    pub estimated_total_hits: usize,
917
918    /// Processing time of the query.
919    pub processing_time_ms: usize,
920
921    /// [Data for facets present in the search results](https://www.meilisearch.com/docs/reference/api/multi_search#facetsbyindex)
922    pub facets_by_index: Option<ComputedFacets>,
923
924    /// [Distribution of the given facets](https://www.meilisearch.com/docs/reference/api/multi_search#mergefacets)
925    pub facet_distribution: Option<HashMap<String, HashMap<String, usize>>>,
926
927    /// [The numeric `min` and `max` values per facet](https://www.meilisearch.com/docs/reference/api/multi_search#mergefacets)
928    pub facet_stats: Option<HashMap<String, FacetStats>>,
929
930    /// Indicates which remote requests failed and why
931    pub remote_errors: Option<HashMap<String, MeilisearchError>>,
932}
933
934/// Returned for each hit in `_federation` when doing federated multi search.
935#[derive(Serialize, Deserialize, Debug, Clone)]
936#[serde(rename_all = "camelCase")]
937pub struct FederationHitInfo {
938    /// Index of origin for this document
939    pub index_uid: String,
940
941    /// Array index number of the query in the request’s queries array
942    pub queries_position: usize,
943
944    /// Remote instance of origin for this document
945    pub remote: Option<String>,
946
947    /// The product of the _rankingScore of the hit and the weight of the query of origin.
948    pub weighted_ranking_score: f32,
949}
950
951/// A struct representing a facet-search query.
952///
953/// You can add search parameters using the builder syntax.
954///
955/// See [this page](https://www.meilisearch.com/docs/reference/api/facet_search) for the official list and description of all parameters.
956///
957/// # Examples
958///
959/// ```
960/// # use serde::{Serialize, Deserialize};
961/// # use meilisearch_sdk::{client::*, indexes::*, search::*};
962/// #
963/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
964/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
965/// #
966/// #[derive(Serialize)]
967/// struct Movie {
968///     name: String,
969///     genre: String,
970/// }
971/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
972/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
973/// let movies = client.index("execute_query3");
974///
975/// // add some documents
976/// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), genre:String::from("scifi")},Movie{name:String::from("Inception"), genre:String::from("drama")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
977/// # movies.set_filterable_attributes(["genre"]).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
978///
979/// let query = FacetSearchQuery::new(&movies, "genre").with_facet_query("scifi").build();
980/// let res = movies.execute_facet_query(&query).await.unwrap();
981///
982/// assert!(res.facet_hits.len() > 0);
983/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
984/// # });
985/// ```
986///
987/// ```
988/// # use meilisearch_sdk::{client::*, indexes::*, search::*};
989/// #
990/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
991/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
992/// #
993/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
994/// # let index = client.index("facet_search_query_builder_build");
995/// let query = index.facet_search("kind")
996///     .with_facet_query("space")
997///     .build(); // you can also execute() instead of build()
998/// ```
999
1000#[derive(Debug, Serialize, Clone)]
1001#[serde(rename_all = "camelCase")]
1002pub struct FacetSearchQuery<'a, Http: HttpClient = DefaultHttpClient> {
1003    #[serde(skip_serializing)]
1004    index: &'a Index<Http>,
1005    /// The facet name to search values on.
1006    pub facet_name: &'a str,
1007    /// The search query for the facet values.
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    pub facet_query: Option<&'a str>,
1010    /// The text that will be searched for among the documents.
1011    #[serde(skip_serializing_if = "Option::is_none")]
1012    #[serde(rename = "q")]
1013    pub search_query: Option<&'a str>,
1014    /// Filter applied to documents.
1015    ///
1016    /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
1017    #[serde(skip_serializing_if = "Option::is_none")]
1018    pub filter: Option<Filter<'a>>,
1019    /// Defines the strategy on how to handle search queries containing multiple words.
1020    #[serde(skip_serializing_if = "Option::is_none")]
1021    pub matching_strategy: Option<MatchingStrategies>,
1022    /// Restrict search to the specified attributes
1023    #[serde(skip_serializing_if = "Option::is_none")]
1024    pub attributes_to_search_on: Option<&'a [&'a str]>,
1025    /// Return an exhaustive count of facets, up to the limit defined by maxTotalHits. Default is false.
1026    #[serde(skip_serializing_if = "Option::is_none")]
1027    pub exhaustive_facet_count: Option<bool>,
1028}
1029
1030#[allow(missing_docs)]
1031impl<'a, Http: HttpClient> FacetSearchQuery<'a, Http> {
1032    pub fn new(index: &'a Index<Http>, facet_name: &'a str) -> FacetSearchQuery<'a, Http> {
1033        FacetSearchQuery {
1034            index,
1035            facet_name,
1036            facet_query: None,
1037            search_query: None,
1038            filter: None,
1039            matching_strategy: None,
1040            attributes_to_search_on: None,
1041            exhaustive_facet_count: None,
1042        }
1043    }
1044
1045    pub fn with_facet_query<'b>(
1046        &'b mut self,
1047        facet_query: &'a str,
1048    ) -> &'b mut FacetSearchQuery<'a, Http> {
1049        self.facet_query = Some(facet_query);
1050        self
1051    }
1052
1053    pub fn with_search_query<'b>(
1054        &'b mut self,
1055        search_query: &'a str,
1056    ) -> &'b mut FacetSearchQuery<'a, Http> {
1057        self.search_query = Some(search_query);
1058        self
1059    }
1060
1061    pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut FacetSearchQuery<'a, Http> {
1062        self.filter = Some(Filter::new(Either::Left(filter)));
1063        self
1064    }
1065
1066    pub fn with_array_filter<'b>(
1067        &'b mut self,
1068        filter: Vec<&'a str>,
1069    ) -> &'b mut FacetSearchQuery<'a, Http> {
1070        self.filter = Some(Filter::new(Either::Right(filter)));
1071        self
1072    }
1073
1074    pub fn with_matching_strategy<'b>(
1075        &'b mut self,
1076        matching_strategy: MatchingStrategies,
1077    ) -> &'b mut FacetSearchQuery<'a, Http> {
1078        self.matching_strategy = Some(matching_strategy);
1079        self
1080    }
1081
1082    pub fn with_attributes_to_search_on<'b>(
1083        &'b mut self,
1084        attributes_to_search_on: &'a [&'a str],
1085    ) -> &'b mut FacetSearchQuery<'a, Http> {
1086        self.attributes_to_search_on = Some(attributes_to_search_on);
1087        self
1088    }
1089
1090    pub fn with_exhaustive_facet_count<'b>(
1091        &'b mut self,
1092        exhaustive_facet_count: bool,
1093    ) -> &'b mut FacetSearchQuery<'a, Http> {
1094        self.exhaustive_facet_count = Some(exhaustive_facet_count);
1095        self
1096    }
1097
1098    pub fn build(&mut self) -> FacetSearchQuery<'a, Http> {
1099        self.clone()
1100    }
1101
1102    pub async fn execute(&'a self) -> Result<FacetSearchResponse, Error> {
1103        self.index.execute_facet_query(self).await
1104    }
1105}
1106
1107#[derive(Debug, Deserialize)]
1108#[serde(rename_all = "camelCase")]
1109pub struct FacetHit {
1110    pub value: String,
1111    pub count: usize,
1112}
1113
1114#[derive(Debug, Deserialize)]
1115#[serde(rename_all = "camelCase")]
1116pub struct FacetSearchResponse {
1117    pub facet_hits: Vec<FacetHit>,
1118    pub facet_query: Option<String>,
1119    pub processing_time_ms: usize,
1120}
1121
1122#[cfg(test)]
1123pub(crate) mod tests {
1124    use crate::errors::{ErrorCode, MeilisearchError};
1125    use crate::{
1126        client::*,
1127        key::{Action, KeyBuilder},
1128        search::*,
1129        settings::EmbedderSource,
1130    };
1131    use big_s::S;
1132    use meilisearch_test_macro::meilisearch_test;
1133    use serde::{Deserialize, Serialize};
1134    use serde_json::{json, Map, Value};
1135
1136    #[test]
1137    fn search_query_serializes_media_parameter() {
1138        let client = Client::new("http://localhost:7700", Some("masterKey")).unwrap();
1139        let index = client.index("media_query");
1140        let mut query = SearchQuery::new(&index);
1141
1142        query.with_query("example").with_media(json!({
1143            "FIELD_A": "VALUE_A",
1144            "FIELD_B": {
1145                "FIELD_C": "VALUE_B",
1146                "FIELD_D": "VALUE_C"
1147            }
1148        }));
1149
1150        let serialized = serde_json::to_value(&query.build()).unwrap();
1151
1152        assert_eq!(
1153            serialized.get("media"),
1154            Some(&json!({
1155                "FIELD_A": "VALUE_A",
1156                "FIELD_B": {
1157                    "FIELD_C": "VALUE_B",
1158                    "FIELD_D": "VALUE_C"
1159                }
1160            }))
1161        );
1162    }
1163
1164    #[derive(Debug, Serialize, Deserialize, PartialEq)]
1165    pub struct Nested {
1166        child: String,
1167    }
1168
1169    #[derive(Debug, Serialize, Deserialize, PartialEq)]
1170    pub struct Document {
1171        pub id: usize,
1172        pub value: String,
1173        pub kind: String,
1174        pub number: i32,
1175        pub nested: Nested,
1176        #[serde(skip_serializing_if = "Option::is_none", default)]
1177        pub _vectors: Option<Vectors>,
1178    }
1179
1180    #[derive(Debug, Serialize, Deserialize, PartialEq)]
1181    struct Vector {
1182        embeddings: SingleOrMultipleVectors,
1183        regenerate: bool,
1184    }
1185
1186    #[derive(Serialize, Deserialize, Debug, PartialEq)]
1187    #[serde(untagged)]
1188    enum SingleOrMultipleVectors {
1189        Single(Vec<f32>),
1190        Multiple(Vec<Vec<f32>>),
1191    }
1192
1193    #[derive(Debug, Serialize, Deserialize, PartialEq)]
1194    pub struct Vectors(HashMap<String, Vector>);
1195
1196    impl<T: Into<Vec<f32>>> From<T> for Vectors {
1197        fn from(value: T) -> Self {
1198            let vec: Vec<f32> = value.into();
1199            Vectors(HashMap::from([(
1200                S("default"),
1201                Vector {
1202                    embeddings: SingleOrMultipleVectors::Multiple(Vec::from([vec])),
1203                    regenerate: false,
1204                },
1205            )]))
1206        }
1207    }
1208
1209    impl PartialEq<Map<String, Value>> for Document {
1210        #[allow(clippy::cmp_owned)]
1211        fn eq(&self, rhs: &Map<String, Value>) -> bool {
1212            self.id.to_string() == rhs["id"]
1213                && self.value == rhs["value"]
1214                && self.kind == rhs["kind"]
1215        }
1216    }
1217
1218    fn vectorize(is_harry_potter: bool, id: usize) -> Vec<f32> {
1219        let mut vector: Vec<f32> = vec![0.; 11];
1220        vector[0] = if is_harry_potter { 1. } else { 0. };
1221        vector[id + 1] = 1.;
1222        vector
1223    }
1224
1225    pub(crate) async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
1226        let t0 = index.add_documents(&[
1227            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))) },
1228            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))) },
1229            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))) },
1230            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))) },
1231            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))) },
1232            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))) },
1233            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))) },
1234            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))) },
1235            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))) },
1236            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))) },
1237        ], None).await?;
1238        let t1 = index
1239            .set_filterable_attributes(["kind", "value", "number"])
1240            .await?;
1241        let t2 = index.set_sortable_attributes(["title"]).await?;
1242
1243        t2.wait_for_completion(client, None, None).await?;
1244        t1.wait_for_completion(client, None, None).await?;
1245        t0.wait_for_completion(client, None, None).await?;
1246
1247        Ok(())
1248    }
1249
1250    #[derive(Debug, Serialize, Deserialize, PartialEq)]
1251    struct VideoDocument {
1252        id: usize,
1253        title: String,
1254        description: Option<String>,
1255        duration: u32,
1256    }
1257
1258    async fn setup_test_video_index(client: &Client, index: &Index) -> Result<(), Error> {
1259        let t0 = index
1260            .add_documents(
1261                &[
1262                    VideoDocument {
1263                        id: 0,
1264                        title: S("Spring"),
1265                        description: Some(S("A Blender Open movie")),
1266                        duration: 123,
1267                    },
1268                    VideoDocument {
1269                        id: 1,
1270                        title: S("Wing It!"),
1271                        description: None,
1272                        duration: 234,
1273                    },
1274                    VideoDocument {
1275                        id: 2,
1276                        title: S("Coffee Run"),
1277                        description: Some(S("Directed by Hjalti Hjalmarsson")),
1278                        duration: 345,
1279                    },
1280                    VideoDocument {
1281                        id: 3,
1282                        title: S("Harry Potter and the Deathly Hallows"),
1283                        description: None,
1284                        duration: 7654,
1285                    },
1286                ],
1287                None,
1288            )
1289            .await?;
1290        let t1 = index.set_filterable_attributes(["duration"]).await?;
1291        let t2 = index.set_sortable_attributes(["title"]).await?;
1292
1293        t2.wait_for_completion(client, None, None).await?;
1294        t1.wait_for_completion(client, None, None).await?;
1295        t0.wait_for_completion(client, None, None).await?;
1296        Ok(())
1297    }
1298
1299    pub(crate) async fn setup_embedder(client: &Client, index: &Index) -> Result<(), Error> {
1300        use crate::settings::Embedder;
1301        let embedder_setting = Embedder {
1302            source: EmbedderSource::UserProvided,
1303            dimensions: Some(11),
1304            ..Embedder::default()
1305        };
1306        index
1307            .set_settings(&crate::settings::Settings {
1308                embedders: Some(HashMap::from([("default".to_string(), embedder_setting)])),
1309                ..crate::settings::Settings::default()
1310            })
1311            .await?
1312            .wait_for_completion(client, None, None)
1313            .await?;
1314        Ok(())
1315    }
1316
1317    #[meilisearch_test]
1318    async fn test_multi_search(client: Client, index: Index) -> Result<(), Error> {
1319        setup_test_index(&client, &index).await?;
1320        let search_query_1 = SearchQuery::new(&index)
1321            .with_query("Sorcerer's Stone")
1322            .build();
1323        let search_query_2 = SearchQuery::new(&index)
1324            .with_query("Chamber of Secrets")
1325            .build();
1326
1327        let response = client
1328            .multi_search()
1329            .with_search_query(search_query_1)
1330            .with_search_query(search_query_2)
1331            .execute::<Document>()
1332            .await
1333            .unwrap();
1334
1335        assert_eq!(response.results.len(), 2);
1336        Ok(())
1337    }
1338
1339    #[meilisearch_test]
1340    async fn test_federated_multi_search(
1341        client: Client,
1342        test_index: Index,
1343        video_index: Index,
1344    ) -> Result<(), Error> {
1345        setup_test_index(&client, &test_index).await?;
1346        setup_test_video_index(&client, &video_index).await?;
1347
1348        let query_test_index = SearchQuery::new(&test_index).with_query("death").build();
1349        let query_video_index = SearchQuery::new(&video_index).with_query("death").build();
1350
1351        #[derive(Debug, Serialize, Deserialize, PartialEq)]
1352        #[serde(untagged)]
1353        enum AnyDocument {
1354            Document(Document),
1355            VideoDocument(VideoDocument),
1356        }
1357
1358        // Search with big weight on the test index
1359        let mut multi_query = client.multi_search();
1360        multi_query.with_search_query_and_weight(query_test_index.clone(), 999.0);
1361        multi_query.with_search_query(query_video_index.clone());
1362        let response = multi_query
1363            .with_federation(FederationOptions::default())
1364            .execute::<AnyDocument>()
1365            .await?;
1366        assert_eq!(response.hits.len(), 2);
1367        assert_eq!(
1368            response.hits[0].result,
1369            AnyDocument::Document(Document {
1370                id: 9,
1371                kind: "title".into(),
1372                number: 90,
1373                value: S("Harry Potter and the Deathly Hallows"),
1374                nested: Nested { child: S("tenth") },
1375                _vectors: None,
1376            })
1377        );
1378        assert_eq!(
1379            response.hits[1].result,
1380            AnyDocument::VideoDocument(VideoDocument {
1381                id: 3,
1382                title: S("Harry Potter and the Deathly Hallows"),
1383                description: None,
1384                duration: 7654,
1385            })
1386        );
1387
1388        // Search with big weight on the video index
1389        let mut multi_query = client.multi_search();
1390        multi_query.with_search_query(query_test_index.clone());
1391        multi_query.with_search_query_and_weight(query_video_index.clone(), 999.0);
1392        let response = multi_query
1393            .with_federation(FederationOptions::default())
1394            .execute::<AnyDocument>()
1395            .await?;
1396        assert_eq!(response.hits.len(), 2);
1397        assert_eq!(
1398            response.hits[0].result,
1399            AnyDocument::VideoDocument(VideoDocument {
1400                id: 3,
1401                title: S("Harry Potter and the Deathly Hallows"),
1402                description: None,
1403                duration: 7654,
1404            })
1405        );
1406        assert_eq!(
1407            response.hits[1].result,
1408            AnyDocument::Document(Document {
1409                id: 9,
1410                kind: "title".into(),
1411                number: 90,
1412                value: S("Harry Potter and the Deathly Hallows"),
1413                nested: Nested { child: S("tenth") },
1414                _vectors: None,
1415            })
1416        );
1417
1418        // Make sure federation options are applied
1419        let mut multi_query = client.multi_search();
1420        multi_query.with_search_query(query_test_index.clone());
1421        multi_query.with_search_query(query_video_index.clone());
1422        let response = multi_query
1423            .with_federation(FederationOptions {
1424                limit: Some(1),
1425                ..Default::default()
1426            })
1427            .execute::<AnyDocument>()
1428            .await?;
1429
1430        assert_eq!(response.hits.len(), 1);
1431
1432        Ok(())
1433    }
1434
1435    #[meilisearch_test]
1436    async fn test_query_builder(_client: Client, index: Index) -> Result<(), Error> {
1437        let mut query = SearchQuery::new(&index);
1438        query.with_query("space").with_offset(42).with_limit(21);
1439
1440        let res = query.execute::<Document>().await.unwrap();
1441
1442        assert_eq!(res.query, S("space"));
1443        assert_eq!(res.limit, Some(21));
1444        assert_eq!(res.offset, Some(42));
1445        assert_eq!(res.estimated_total_hits, Some(0));
1446        Ok(())
1447    }
1448
1449    #[meilisearch_test]
1450    async fn test_query_numbered_pagination(client: Client, index: Index) -> Result<(), Error> {
1451        setup_test_index(&client, &index).await?;
1452
1453        let mut query = SearchQuery::new(&index);
1454        query.with_query("").with_page(2).with_hits_per_page(2);
1455
1456        let res = query.execute::<Document>().await.unwrap();
1457
1458        assert_eq!(res.page, Some(2));
1459        assert_eq!(res.hits_per_page, Some(2));
1460        assert_eq!(res.total_hits, Some(10));
1461        assert_eq!(res.total_pages, Some(5));
1462        Ok(())
1463    }
1464
1465    #[meilisearch_test]
1466    async fn test_query_string(client: Client, index: Index) -> Result<(), Error> {
1467        setup_test_index(&client, &index).await?;
1468
1469        let results: SearchResults<Document> = index.search().with_query("dolor").execute().await?;
1470        assert_eq!(results.hits.len(), 2);
1471        Ok(())
1472    }
1473
1474    #[meilisearch_test]
1475    async fn test_query_string_on_nested_field(client: Client, index: Index) -> Result<(), Error> {
1476        setup_test_index(&client, &index).await?;
1477
1478        let results: SearchResults<Document> =
1479            index.search().with_query("second").execute().await?;
1480
1481        assert_eq!(
1482            &Document {
1483                id: 1,
1484                value: S("dolor sit amet, consectetur adipiscing elit"),
1485                kind: S("text"),
1486                number: 10,
1487                nested: Nested { child: S("second") },
1488                _vectors: None,
1489            },
1490            &results.hits[0].result
1491        );
1492
1493        Ok(())
1494    }
1495
1496    #[meilisearch_test]
1497    async fn test_query_limit(client: Client, index: Index) -> Result<(), Error> {
1498        setup_test_index(&client, &index).await?;
1499
1500        let results: SearchResults<Document> = index.search().with_limit(5).execute().await?;
1501        assert_eq!(results.hits.len(), 5);
1502        Ok(())
1503    }
1504
1505    #[meilisearch_test]
1506    async fn test_query_page(client: Client, index: Index) -> Result<(), Error> {
1507        setup_test_index(&client, &index).await?;
1508
1509        let results: SearchResults<Document> = index.search().with_page(2).execute().await?;
1510        assert_eq!(results.page, Some(2));
1511        assert_eq!(results.hits_per_page, Some(20));
1512        Ok(())
1513    }
1514
1515    #[meilisearch_test]
1516    async fn test_query_hits_per_page(client: Client, index: Index) -> Result<(), Error> {
1517        setup_test_index(&client, &index).await?;
1518
1519        let results: SearchResults<Document> =
1520            index.search().with_hits_per_page(2).execute().await?;
1521        assert_eq!(results.page, Some(1));
1522        assert_eq!(results.hits_per_page, Some(2));
1523        Ok(())
1524    }
1525
1526    #[meilisearch_test]
1527    async fn test_query_offset(client: Client, index: Index) -> Result<(), Error> {
1528        setup_test_index(&client, &index).await?;
1529
1530        let results: SearchResults<Document> = index.search().with_offset(6).execute().await?;
1531        assert_eq!(results.hits.len(), 4);
1532        Ok(())
1533    }
1534
1535    #[meilisearch_test]
1536    async fn test_query_filter(client: Client, index: Index) -> Result<(), Error> {
1537        setup_test_index(&client, &index).await?;
1538
1539        let results: SearchResults<Document> = index
1540            .search()
1541            .with_filter("value = \"The Social Network\"")
1542            .execute()
1543            .await?;
1544        assert_eq!(results.hits.len(), 1);
1545
1546        let results: SearchResults<Document> = index
1547            .search()
1548            .with_filter("NOT value = \"The Social Network\"")
1549            .execute()
1550            .await?;
1551        assert_eq!(results.hits.len(), 9);
1552        Ok(())
1553    }
1554
1555    #[meilisearch_test]
1556    async fn test_query_filter_with_array(client: Client, index: Index) -> Result<(), Error> {
1557        setup_test_index(&client, &index).await?;
1558
1559        let results: SearchResults<Document> = index
1560            .search()
1561            .with_array_filter(vec![
1562                "value = \"The Social Network\"",
1563                "value = \"The Social Network\"",
1564            ])
1565            .execute()
1566            .await?;
1567        assert_eq!(results.hits.len(), 1);
1568
1569        Ok(())
1570    }
1571
1572    #[meilisearch_test]
1573    async fn test_query_facet_distribution(client: Client, index: Index) -> Result<(), Error> {
1574        setup_test_index(&client, &index).await?;
1575
1576        let mut query = SearchQuery::new(&index);
1577        query.with_facets(Selectors::All);
1578        let results: SearchResults<Document> = index.execute_query(&query).await?;
1579        assert_eq!(
1580            results
1581                .facet_distribution
1582                .unwrap()
1583                .get("kind")
1584                .unwrap()
1585                .get("title")
1586                .unwrap(),
1587            &8
1588        );
1589
1590        let mut query = SearchQuery::new(&index);
1591        query.with_facets(Selectors::Some(&["kind"]));
1592        let results: SearchResults<Document> = index.execute_query(&query).await?;
1593        assert_eq!(
1594            results
1595                .facet_distribution
1596                .clone()
1597                .unwrap()
1598                .get("kind")
1599                .unwrap()
1600                .get("title")
1601                .unwrap(),
1602            &8
1603        );
1604        assert_eq!(
1605            results
1606                .facet_distribution
1607                .unwrap()
1608                .get("kind")
1609                .unwrap()
1610                .get("text")
1611                .unwrap(),
1612            &2
1613        );
1614        Ok(())
1615    }
1616
1617    #[meilisearch_test]
1618    async fn test_query_attributes_to_retrieve(client: Client, index: Index) -> Result<(), Error> {
1619        setup_test_index(&client, &index).await?;
1620
1621        let results: SearchResults<Document> = index
1622            .search()
1623            .with_attributes_to_retrieve(Selectors::All)
1624            .execute()
1625            .await?;
1626        assert_eq!(results.hits.len(), 10);
1627
1628        let mut query = SearchQuery::new(&index);
1629        query.with_attributes_to_retrieve(Selectors::Some(&["kind", "id"])); // omit the "value" field
1630        assert!(index.execute_query::<Document>(&query).await.is_err()); // error: missing "value" field
1631        Ok(())
1632    }
1633
1634    #[meilisearch_test]
1635    async fn test_query_sort(client: Client, index: Index) -> Result<(), Error> {
1636        setup_test_index(&client, &index).await?;
1637
1638        let mut query = SearchQuery::new(&index);
1639        query.with_query("harry potter");
1640        query.with_sort(&["title:desc"]);
1641        let results: SearchResults<Document> = index.execute_query(&query).await?;
1642        assert_eq!(results.hits.len(), 7);
1643        Ok(())
1644    }
1645
1646    #[meilisearch_test]
1647    async fn test_query_attributes_to_crop(client: Client, index: Index) -> Result<(), Error> {
1648        setup_test_index(&client, &index).await?;
1649
1650        let mut query = SearchQuery::new(&index);
1651        query.with_query("lorem ipsum");
1652        query.with_attributes_to_crop(Selectors::All);
1653        let results: SearchResults<Document> = index.execute_query(&query).await?;
1654        assert_eq!(
1655            &Document {
1656                id: 0,
1657                value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do…"),
1658                kind: S("text"),
1659                number: 0,
1660                nested: Nested { child: S("first") },
1661                _vectors: None,
1662            },
1663            results.hits[0].formatted_result.as_ref().unwrap()
1664        );
1665
1666        let mut query = SearchQuery::new(&index);
1667        query.with_query("lorem ipsum");
1668        query.with_attributes_to_crop(Selectors::Some(&[("value", Some(5)), ("kind", None)]));
1669        let results: SearchResults<Document> = index.execute_query(&query).await?;
1670        assert_eq!(
1671            &Document {
1672                id: 0,
1673                value: S("Lorem ipsum dolor sit amet…"),
1674                kind: S("text"),
1675                number: 0,
1676                nested: Nested { child: S("first") },
1677                _vectors: None,
1678            },
1679            results.hits[0].formatted_result.as_ref().unwrap()
1680        );
1681        Ok(())
1682    }
1683
1684    #[meilisearch_test]
1685    async fn test_query_crop_length(client: Client, index: Index) -> Result<(), Error> {
1686        setup_test_index(&client, &index).await?;
1687
1688        let mut query = SearchQuery::new(&index);
1689        query.with_query("lorem ipsum");
1690        query.with_attributes_to_crop(Selectors::All);
1691        query.with_crop_length(200);
1692        let results: SearchResults<Document> = index.execute_query(&query).await?;
1693        assert_eq!(&Document {
1694            id: 0,
1695            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."),
1696            kind: S("text"),
1697            number: 0,
1698            nested: Nested { child: S("first") },
1699            _vectors: None,
1700        },
1701        results.hits[0].formatted_result.as_ref().unwrap());
1702
1703        let mut query = SearchQuery::new(&index);
1704        query.with_query("lorem ipsum");
1705        query.with_attributes_to_crop(Selectors::All);
1706        query.with_crop_length(5);
1707        let results: SearchResults<Document> = index.execute_query(&query).await?;
1708        assert_eq!(
1709            &Document {
1710                id: 0,
1711                value: S("Lorem ipsum dolor sit amet…"),
1712                kind: S("text"),
1713                number: 0,
1714                nested: Nested { child: S("first") },
1715                _vectors: None,
1716            },
1717            results.hits[0].formatted_result.as_ref().unwrap()
1718        );
1719        Ok(())
1720    }
1721
1722    #[meilisearch_test]
1723    async fn test_query_customized_crop_marker(client: Client, index: Index) -> Result<(), Error> {
1724        setup_test_index(&client, &index).await?;
1725
1726        let mut query = SearchQuery::new(&index);
1727        query.with_query("sed do eiusmod");
1728        query.with_attributes_to_crop(Selectors::All);
1729        query.with_crop_length(6);
1730        query.with_crop_marker("(ꈍᴗꈍ)");
1731
1732        let results: SearchResults<Document> = index.execute_query(&query).await?;
1733
1734        assert_eq!(
1735            &Document {
1736                id: 0,
1737                value: S("(ꈍᴗꈍ)sed do eiusmod tempor incididunt ut(ꈍᴗꈍ)"),
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_highlight_pre_tag(
1750        client: Client,
1751        index: Index,
1752    ) -> Result<(), Error> {
1753        setup_test_index(&client, &index).await?;
1754
1755        let mut query = SearchQuery::new(&index);
1756        query.with_query("Social");
1757        query.with_attributes_to_highlight(Selectors::All);
1758        query.with_highlight_pre_tag("(⊃。•́‿•̀。)⊃ ");
1759        query.with_highlight_post_tag(" ⊂(´• ω •`⊂)");
1760
1761        let results: SearchResults<Document> = index.execute_query(&query).await?;
1762        assert_eq!(
1763            &Document {
1764                id: 2,
1765                value: S("The (⊃。•́‿•̀。)⊃ Social ⊂(´• ω •`⊂) Network"),
1766                kind: S("title"),
1767                number: 20,
1768                nested: Nested { child: S("third") },
1769                _vectors: None,
1770            },
1771            results.hits[0].formatted_result.as_ref().unwrap()
1772        );
1773
1774        Ok(())
1775    }
1776
1777    #[meilisearch_test]
1778    async fn test_query_attributes_to_highlight(client: Client, index: Index) -> Result<(), Error> {
1779        setup_test_index(&client, &index).await?;
1780
1781        let mut query = SearchQuery::new(&index);
1782        query.with_query("dolor text");
1783        query.with_attributes_to_highlight(Selectors::All);
1784        let results: SearchResults<Document> = index.execute_query(&query).await?;
1785        assert_eq!(
1786            &Document {
1787                id: 1,
1788                value: S("<em>dolor</em> sit amet, consectetur adipiscing elit"),
1789                kind: S("<em>text</em>"),
1790                number: 10,
1791                nested: Nested { child: S("second") },
1792                _vectors: None,
1793            },
1794            results.hits[0].formatted_result.as_ref().unwrap(),
1795        );
1796
1797        let mut query = SearchQuery::new(&index);
1798        query.with_query("dolor text");
1799        query.with_attributes_to_highlight(Selectors::Some(&["value"]));
1800        let results: SearchResults<Document> = index.execute_query(&query).await?;
1801        assert_eq!(
1802            &Document {
1803                id: 1,
1804                value: S("<em>dolor</em> sit amet, consectetur adipiscing elit"),
1805                kind: S("text"),
1806                number: 10,
1807                nested: Nested { child: S("second") },
1808                _vectors: None,
1809            },
1810            results.hits[0].formatted_result.as_ref().unwrap()
1811        );
1812        Ok(())
1813    }
1814
1815    #[meilisearch_test]
1816    async fn test_query_show_matches_position(client: Client, index: Index) -> Result<(), Error> {
1817        setup_test_index(&client, &index).await?;
1818
1819        let mut query = SearchQuery::new(&index);
1820        query.with_query("dolor text");
1821        query.with_show_matches_position(true);
1822        let results: SearchResults<Document> = index.execute_query(&query).await?;
1823        assert_eq!(results.hits[0].matches_position.as_ref().unwrap().len(), 2);
1824        assert_eq!(
1825            results.hits[0]
1826                .matches_position
1827                .as_ref()
1828                .unwrap()
1829                .get("value")
1830                .unwrap(),
1831            &vec![MatchRange {
1832                start: 0,
1833                length: 5,
1834                indices: None,
1835            }]
1836        );
1837        Ok(())
1838    }
1839
1840    #[meilisearch_test]
1841    async fn test_query_show_ranking_score(client: Client, index: Index) -> Result<(), Error> {
1842        setup_test_index(&client, &index).await?;
1843
1844        let mut query = SearchQuery::new(&index);
1845        query.with_query("dolor text");
1846        query.with_show_ranking_score(true);
1847        let results: SearchResults<Document> = index.execute_query(&query).await?;
1848        assert!(results.hits[0].ranking_score.is_some());
1849        Ok(())
1850    }
1851
1852    #[meilisearch_test]
1853    async fn test_query_show_ranking_score_details(
1854        client: Client,
1855        index: Index,
1856    ) -> Result<(), Error> {
1857        setup_test_index(&client, &index).await?;
1858
1859        let mut query = SearchQuery::new(&index);
1860        query.with_query("dolor text");
1861        query.with_show_ranking_score_details(true);
1862        let results: SearchResults<Document> = index.execute_query(&query).await.unwrap();
1863        assert!(results.hits[0].ranking_score_details.is_some());
1864        Ok(())
1865    }
1866
1867    #[meilisearch_test]
1868    async fn test_query_show_ranking_score_threshold(
1869        client: Client,
1870        index: Index,
1871    ) -> Result<(), Error> {
1872        setup_test_index(&client, &index).await?;
1873
1874        let mut query = SearchQuery::new(&index);
1875        query.with_query("dolor text");
1876        query.with_ranking_score_threshold(1.0);
1877        let results: SearchResults<Document> = index.execute_query(&query).await.unwrap();
1878        assert!(results.hits.is_empty());
1879        Ok(())
1880    }
1881
1882    #[meilisearch_test]
1883    async fn test_query_locales(client: Client, index: Index) -> Result<(), Error> {
1884        setup_test_index(&client, &index).await?;
1885
1886        let mut query = SearchQuery::new(&index);
1887        query.with_query("Harry Styles");
1888        query.with_locales(&["eng"]);
1889        let results: SearchResults<Document> = index.execute_query(&query).await.unwrap();
1890        assert_eq!(results.hits.len(), 7);
1891        Ok(())
1892    }
1893
1894    #[meilisearch_test]
1895    async fn test_phrase_search(client: Client, index: Index) -> Result<(), Error> {
1896        setup_test_index(&client, &index).await?;
1897
1898        let mut query = SearchQuery::new(&index);
1899        query.with_query("harry \"of Fire\"");
1900        let results: SearchResults<Document> = index.execute_query(&query).await?;
1901
1902        assert_eq!(results.hits.len(), 1);
1903        Ok(())
1904    }
1905
1906    #[meilisearch_test]
1907    async fn test_matching_strategy_all(client: Client, index: Index) -> Result<(), Error> {
1908        setup_test_index(&client, &index).await?;
1909
1910        let results = SearchQuery::new(&index)
1911            .with_query("Harry Styles")
1912            .with_matching_strategy(MatchingStrategies::ALL)
1913            .execute::<Document>()
1914            .await
1915            .unwrap();
1916
1917        assert_eq!(results.hits.len(), 0);
1918        Ok(())
1919    }
1920
1921    #[meilisearch_test]
1922    async fn test_matching_strategy_last(client: Client, index: Index) -> Result<(), Error> {
1923        setup_test_index(&client, &index).await?;
1924
1925        let results = SearchQuery::new(&index)
1926            .with_query("Harry Styles")
1927            .with_matching_strategy(MatchingStrategies::LAST)
1928            .execute::<Document>()
1929            .await
1930            .unwrap();
1931
1932        assert_eq!(results.hits.len(), 7);
1933        Ok(())
1934    }
1935
1936    #[meilisearch_test]
1937    async fn test_matching_strategy_frequency(client: Client, index: Index) -> Result<(), Error> {
1938        setup_test_index(&client, &index).await?;
1939
1940        let results = SearchQuery::new(&index)
1941            .with_query("Harry Styles")
1942            .with_matching_strategy(MatchingStrategies::FREQUENCY)
1943            .execute::<Document>()
1944            .await
1945            .unwrap();
1946
1947        assert_eq!(results.hits.len(), 7);
1948        Ok(())
1949    }
1950
1951    #[meilisearch_test]
1952    async fn test_distinct(client: Client, index: Index) -> Result<(), Error> {
1953        setup_test_index(&client, &index).await?;
1954
1955        let results = SearchQuery::new(&index)
1956            .with_distinct("kind")
1957            .execute::<Document>()
1958            .await
1959            .unwrap();
1960
1961        assert_eq!(results.hits.len(), 2);
1962        Ok(())
1963    }
1964
1965    #[meilisearch_test]
1966    async fn test_generate_tenant_token_from_client(
1967        client: Client,
1968        index: Index,
1969    ) -> Result<(), Error> {
1970        setup_test_index(&client, &index).await?;
1971
1972        let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1973        let key = KeyBuilder::new()
1974            .with_action(Action::All)
1975            .with_index("*")
1976            .execute(&client)
1977            .await
1978            .unwrap();
1979        let allowed_client = Client::new(meilisearch_url, Some(key.key)).unwrap();
1980
1981        let search_rules = vec![
1982            json!({ "*": {}}),
1983            json!({ "*": Value::Null }),
1984            json!(["*"]),
1985            json!({ "*": { "filter": "kind = text" } }),
1986            json!([index.uid.to_string()]),
1987        ];
1988
1989        for rules in search_rules {
1990            let token = allowed_client
1991                .generate_tenant_token(key.uid.clone(), rules, None, None)
1992                .expect("Cannot generate tenant token.");
1993
1994            let new_client = Client::new(meilisearch_url, Some(token.clone())).unwrap();
1995
1996            let result: SearchResults<Document> = new_client
1997                .index(index.uid.to_string())
1998                .search()
1999                .execute()
2000                .await?;
2001
2002            assert!(!result.hits.is_empty());
2003        }
2004
2005        Ok(())
2006    }
2007
2008    #[meilisearch_test]
2009    async fn test_facet_search_base(client: Client, index: Index) -> Result<(), Error> {
2010        setup_test_index(&client, &index).await?;
2011        let res = index.facet_search("kind").execute().await?;
2012        assert_eq!(res.facet_hits.len(), 2);
2013        Ok(())
2014    }
2015
2016    #[meilisearch_test]
2017    async fn test_facet_search_with_exhaustive_facet_count(
2018        client: Client,
2019        index: Index,
2020    ) -> Result<(), Error> {
2021        setup_test_index(&client, &index).await?;
2022        let res = index
2023            .facet_search("kind")
2024            .with_exhaustive_facet_count(true)
2025            .execute()
2026            .await?;
2027        assert_eq!(res.facet_hits.len(), 2);
2028        Ok(())
2029    }
2030
2031    #[meilisearch_test]
2032    async fn test_search_with_exhaustive_facet_count(
2033        client: Client,
2034        index: Index,
2035    ) -> Result<(), Error> {
2036        setup_test_index(&client, &index).await?;
2037
2038        // Request exhaustive facet counts for a specific facet and ensure the server
2039        // returns the exhaustive flag in the response.
2040        let mut query = SearchQuery::new(&index);
2041        query
2042            .with_facets(Selectors::Some(&["kind"]))
2043            .with_exhaustive_facet_count(true);
2044
2045        let res = index.execute_query::<Document>(&query).await;
2046        match res {
2047            Ok(results) => {
2048                assert!(results.exhaustive_facet_count.is_some());
2049                Ok(())
2050            }
2051            Err(error)
2052                if matches!(
2053                    error,
2054                    Error::Meilisearch(MeilisearchError {
2055                        error_code: ErrorCode::BadRequest,
2056                        ..
2057                    })
2058                ) =>
2059            {
2060                // Server doesn't support this field on /search yet; treat as a skip.
2061                Ok(())
2062            }
2063            Err(e) => Err(e),
2064        }
2065    }
2066
2067    #[test]
2068    fn test_search_query_serialization_exhaustive_facet_count() {
2069        // Build a query and ensure it serializes using the expected camelCase field name
2070        let client = Client::new(
2071            option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"),
2072            Some(option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey")),
2073        )
2074        .unwrap();
2075        let index = client.index("dummy");
2076
2077        let mut query = SearchQuery::new(&index);
2078        query
2079            .with_facets(Selectors::Some(&["kind"]))
2080            .with_exhaustive_facet_count(true);
2081
2082        let v = serde_json::to_value(&query).unwrap();
2083        assert_eq!(
2084            v.get("exhaustiveFacetCount").and_then(|b| b.as_bool()),
2085            Some(true)
2086        );
2087    }
2088
2089    #[meilisearch_test]
2090    async fn test_facet_search_with_facet_query(client: Client, index: Index) -> Result<(), Error> {
2091        setup_test_index(&client, &index).await?;
2092        let res = index
2093            .facet_search("kind")
2094            .with_facet_query("title")
2095            .execute()
2096            .await?;
2097        assert_eq!(res.facet_hits.len(), 1);
2098        assert_eq!(res.facet_hits[0].value, "title");
2099        assert_eq!(res.facet_hits[0].count, 8);
2100        Ok(())
2101    }
2102
2103    #[meilisearch_test]
2104    async fn test_facet_search_with_attributes_to_search_on(
2105        client: Client,
2106        index: Index,
2107    ) -> Result<(), Error> {
2108        setup_test_index(&client, &index).await?;
2109        let res = index
2110            .facet_search("kind")
2111            .with_search_query("title")
2112            .with_attributes_to_search_on(&["value"])
2113            .execute()
2114            .await?;
2115        println!("{:?}", res);
2116        assert_eq!(res.facet_hits.len(), 0);
2117
2118        let res = index
2119            .facet_search("kind")
2120            .with_search_query("title")
2121            .with_attributes_to_search_on(&["kind"])
2122            .execute()
2123            .await?;
2124        assert_eq!(res.facet_hits.len(), 1);
2125        Ok(())
2126    }
2127
2128    #[meilisearch_test]
2129    async fn test_with_vectors(client: Client, index: Index) -> Result<(), Error> {
2130        setup_embedder(&client, &index).await?;
2131        setup_test_index(&client, &index).await?;
2132
2133        let results: SearchResults<Document> = index
2134            .search()
2135            .with_query("lorem ipsum")
2136            .with_retrieve_vectors(true)
2137            .execute()
2138            .await?;
2139        assert_eq!(results.hits.len(), 1);
2140        let expected = Some(Vectors::from(vectorize(false, 0)));
2141        assert_eq!(results.hits[0].result._vectors, expected);
2142
2143        let results: SearchResults<Document> = index
2144            .search()
2145            .with_query("lorem ipsum")
2146            .with_retrieve_vectors(false)
2147            .execute()
2148            .await?;
2149        assert_eq!(results.hits.len(), 1);
2150        assert_eq!(results.hits[0].result._vectors, None);
2151        Ok(())
2152    }
2153
2154    #[meilisearch_test]
2155    async fn test_query_vector_in_response(client: Client, index: Index) -> Result<(), Error> {
2156        setup_embedder(&client, &index).await?;
2157        setup_test_index(&client, &index).await?;
2158
2159        let mut query = SearchQuery::new(&index);
2160        let qv = vectorize(false, 0);
2161        query
2162            .with_hybrid("default", 1.0)
2163            .with_vector(&qv)
2164            .with_retrieve_vectors(true);
2165
2166        let results: SearchResults<Document> = index.execute_query(&query).await?;
2167
2168        if std::env::var("MSDK_DEBUG_RAW_SEARCH").ok().as_deref() == Some("1")
2169            && results.query_vector.is_none()
2170        {
2171            use crate::request::Method;
2172            let url = format!("{}/indexes/{}/search", index.client.get_host(), index.uid);
2173            let raw: serde_json::Value = index
2174                .client
2175                .http_client
2176                .request::<(), &SearchQuery<_>, serde_json::Value>(
2177                    &url,
2178                    Method::Post {
2179                        body: &query,
2180                        query: (),
2181                    },
2182                    200,
2183                )
2184                .await
2185                .unwrap();
2186            eprintln!("DEBUG raw search response: {}", raw);
2187        }
2188
2189        assert!(results.query_vector.is_some());
2190        assert_eq!(results.query_vector.as_ref().unwrap().len(), 11);
2191        Ok(())
2192    }
2193
2194    #[meilisearch_test]
2195    async fn test_hybrid(client: Client, index: Index) -> Result<(), Error> {
2196        setup_embedder(&client, &index).await?;
2197        setup_test_index(&client, &index).await?;
2198
2199        // Search for an Harry Potter but with lorem ipsum's id
2200        // Will yield lorem ipsum first, them harry potter documents, then the rest
2201        let results: SearchResults<Document> = index
2202            .search()
2203            .with_hybrid("default", 1.0)
2204            .with_vector(&vectorize(true, 0))
2205            .execute()
2206            .await?;
2207        let ids = results
2208            .hits
2209            .iter()
2210            .map(|hit| hit.result.id)
2211            .collect::<Vec<_>>();
2212        assert_eq!(ids, vec![0, 3, 4, 5, 6, 7, 8, 9, 1, 2]);
2213
2214        Ok(())
2215    }
2216
2217    #[meilisearch_test]
2218    async fn test_facet_search_with_search_query(
2219        client: Client,
2220        index: Index,
2221    ) -> Result<(), Error> {
2222        setup_test_index(&client, &index).await?;
2223        let res = index
2224            .facet_search("kind")
2225            .with_search_query("Harry Potter")
2226            .execute()
2227            .await?;
2228        assert_eq!(res.facet_hits.len(), 1);
2229        assert_eq!(res.facet_hits[0].value, "title");
2230        assert_eq!(res.facet_hits[0].count, 7);
2231        Ok(())
2232    }
2233
2234    #[meilisearch_test]
2235    async fn test_facet_search_with_filter(client: Client, index: Index) -> Result<(), Error> {
2236        setup_test_index(&client, &index).await?;
2237        let res = index
2238            .facet_search("kind")
2239            .with_filter("value = \"The Social Network\"")
2240            .execute()
2241            .await?;
2242        assert_eq!(res.facet_hits.len(), 1);
2243        assert_eq!(res.facet_hits[0].value, "title");
2244        assert_eq!(res.facet_hits[0].count, 1);
2245
2246        let res = index
2247            .facet_search("kind")
2248            .with_filter("NOT value = \"The Social Network\"")
2249            .execute()
2250            .await?;
2251        assert_eq!(res.facet_hits.len(), 2);
2252        Ok(())
2253    }
2254
2255    #[meilisearch_test]
2256    async fn test_facet_search_with_array_filter(
2257        client: Client,
2258        index: Index,
2259    ) -> Result<(), Error> {
2260        setup_test_index(&client, &index).await?;
2261        let res = index
2262            .facet_search("kind")
2263            .with_array_filter(vec![
2264                "value = \"The Social Network\"",
2265                "value = \"The Social Network\"",
2266            ])
2267            .execute()
2268            .await?;
2269        assert_eq!(res.facet_hits.len(), 1);
2270        assert_eq!(res.facet_hits[0].value, "title");
2271        assert_eq!(res.facet_hits[0].count, 1);
2272        Ok(())
2273    }
2274
2275    #[meilisearch_test]
2276    async fn test_facet_search_with_matching_strategy_all(
2277        client: Client,
2278        index: Index,
2279    ) -> Result<(), Error> {
2280        setup_test_index(&client, &index).await?;
2281        let res = index
2282            .facet_search("kind")
2283            .with_search_query("Harry Styles")
2284            .with_matching_strategy(MatchingStrategies::ALL)
2285            .execute()
2286            .await?;
2287        assert_eq!(res.facet_hits.len(), 0);
2288        Ok(())
2289    }
2290
2291    #[meilisearch_test]
2292    async fn test_facet_search_with_matching_strategy_last(
2293        client: Client,
2294        index: Index,
2295    ) -> Result<(), Error> {
2296        setup_test_index(&client, &index).await?;
2297        let res = index
2298            .facet_search("kind")
2299            .with_search_query("Harry Styles")
2300            .with_matching_strategy(MatchingStrategies::LAST)
2301            .execute()
2302            .await?;
2303        assert_eq!(res.facet_hits.len(), 1);
2304        assert_eq!(res.facet_hits[0].value, "title");
2305        assert_eq!(res.facet_hits[0].count, 7);
2306        Ok(())
2307    }
2308}