Skip to main content

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    /// The search query's performance trace.
139    /// Returned when `showPerformanceDetails` is enabled.
140    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/// Some list fields in a `SearchQuery` can be set to a wildcard value.
168///
169/// This structure allows you to choose between the wildcard value and an exhaustive list of selectors.
170#[derive(Debug, Clone)]
171pub enum Selectors<T> {
172    /// A list of selectors.
173    Some(T),
174    /// The wildcard.
175    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/// Configures Meilisearch to return search results based on a query’s meaning and context
188#[derive(Debug, Serialize, Clone)]
189#[serde(rename_all = "camelCase")]
190pub struct HybridSearch<'a> {
191    /// Indicates one of the embedders configured for the queried index
192    pub embedder: &'a str,
193    /// number between `0` and `1`:
194    /// - `0.0` indicates full keyword search
195    /// - `1.0` indicates full semantic search
196    pub semantic_ratio: f32,
197}
198
199type AttributeToCrop<'a> = (&'a str, Option<usize>);
200
201/// A struct representing a query.
202///
203/// You can add search parameters using the builder syntax.
204///
205/// See [this page](https://www.meilisearch.com/docs/reference/api/search#query-q) for the official list and description of all parameters.
206///
207/// # Examples
208///
209/// ```
210/// # use serde::{Serialize, Deserialize};
211/// # use meilisearch_sdk::{client::Client, search::*, indexes::Index};
212/// #
213/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
214/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
215/// #
216/// #[derive(Serialize, Deserialize, Debug)]
217/// struct Movie {
218///     name: String,
219///     description: String,
220/// }
221/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
222/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
223/// # let index = client
224/// #  .create_index("search_query_builder", None)
225/// #  .await
226/// #  .unwrap()
227/// #  .wait_for_completion(&client, None, None)
228/// #  .await.unwrap()
229/// #  .try_make_index(&client)
230/// #  .unwrap();
231///
232/// let mut res = SearchQuery::new(&index)
233///     .with_query("space")
234///     .with_offset(42)
235///     .with_limit(21)
236///     .execute::<Movie>()
237///     .await
238///     .unwrap();
239///
240/// assert_eq!(res.limit, Some(21));
241/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
242/// # });
243/// ```
244///
245/// ```
246/// # use meilisearch_sdk::{client::Client, search::*, indexes::Index};
247/// #
248/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
249/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
250/// #
251/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
252/// # let index = client.index("search_query_builder_build");
253/// let query = index.search()
254///     .with_query("space")
255///     .with_offset(42)
256///     .with_limit(21)
257///     .build(); // you can also execute() instead of build()
258/// ```
259#[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    /// The text that will be searched for among the documents.
265    #[serde(skip_serializing_if = "Option::is_none")]
266    #[serde(rename = "q")]
267    pub query: Option<&'a str>,
268    /// The number of documents to skip.
269    ///
270    /// If the value of the parameter `offset` is `n`, the `n` first documents (ordered by relevance) will not be returned.
271    /// This is helpful for pagination.
272    ///
273    /// Example: If you want to skip the first document, set offset to `1`.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub offset: Option<usize>,
276    /// The maximum number of documents returned.
277    ///
278    /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response.
279    /// This is helpful for pagination.
280    ///
281    /// Example: If you don't want to get more than two documents, set limit to `2`.
282    ///
283    /// **Default: `20`**
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub limit: Option<usize>,
286    /// The page number on which you paginate.
287    ///
288    /// Pagination starts at 1. If page is 0, no results are returned.
289    ///
290    /// **Default: None unless `hits_per_page` is defined, in which case page is `1`**
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub page: Option<usize>,
293    /// The maximum number of results in a page. A page can contain less results than the number of hits_per_page.
294    ///
295    /// **Default: None unless `page` is defined, in which case `20`**
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub hits_per_page: Option<usize>,
298    /// Filter applied to documents.
299    ///
300    /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub filter: Option<Filter<'a>>,
303    /// Facets for which to retrieve the matching count.
304    ///
305    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
306    ///
307    /// **Default: all attributes found in the documents.**
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub facets: Option<Selectors<&'a [&'a str]>>,
310    /// Attributes to sort.
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub sort: Option<&'a [&'a str]>,
313    /// Attributes to perform the search on.
314    ///
315    /// Specify the subset of searchableAttributes for a search without modifying Meilisearch’s index settings.
316    ///
317    /// **Default: all searchable attributes found in the documents.**
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub attributes_to_search_on: Option<&'a [&'a str]>,
320    /// Attributes to display in the returned documents.
321    ///
322    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
323    ///
324    /// **Default: all attributes found in the documents.**
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub attributes_to_retrieve: Option<Selectors<&'a [&'a str]>>,
327    /// Attributes whose values have to be cropped.
328    ///
329    /// Attributes are composed by the attribute name and an optional `usize` that overwrites the `crop_length` parameter.
330    ///
331    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
332    #[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    /// Maximum number of words including the matched query term(s) contained in the returned cropped value(s).
336    ///
337    /// See [attributes_to_crop](#structfield.attributes_to_crop).
338    ///
339    /// **Default: `10`**
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub crop_length: Option<usize>,
342    /// Marker at the start and the end of a cropped value.
343    ///
344    /// ex: `...middle of a crop...`
345    ///
346    /// **Default: `...`**
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub crop_marker: Option<&'a str>,
349    /// Attributes whose values will contain **highlighted matching terms**.
350    ///
351    /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub attributes_to_highlight: Option<Selectors<&'a [&'a str]>>,
354    /// Tag in front of a highlighted term.
355    ///
356    /// ex: `<mytag>hello world`
357    ///
358    /// **Default: `<em>`**
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub highlight_pre_tag: Option<&'a str>,
361    /// Tag after a highlighted term.
362    ///
363    /// ex: `hello world</ mytag>`
364    ///
365    /// **Default: `</em>`**
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub highlight_post_tag: Option<&'a str>,
368    /// Defines whether an object that contains information about the matches should be returned or not.
369    ///
370    /// **Default: `false`**
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub show_matches_position: Option<bool>,
373
374    /// Defines whether to show the relevancy score of the match.
375    ///
376    /// **Default: `false`**
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub show_ranking_score: Option<bool>,
379
380    ///Adds a detailed global ranking score field to each document.
381    ///
382    /// **Default: `false`**
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub show_ranking_score_details: Option<bool>,
385
386    /// Defines the strategy on how to handle queries containing multiple words.
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub matching_strategy: Option<MatchingStrategies>,
389
390    ///Defines one attribute in the filterableAttributes list as a distinct attribute.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub distinct: Option<&'a str>,
393
394    ///Excludes results below the specified ranking score.
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub ranking_score_threshold: Option<f64>,
397
398    /// Defines the language of the search query.
399    #[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    /// Configures Meilisearch to return search results based on a query’s meaning and context.
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub hybrid: Option<HybridSearch<'a>>,
408
409    /// Use a custom vector to perform a search query.
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub vector: Option<&'a [f32]>,
412
413    /// Defines whether document embeddings are returned with search results.
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub retrieve_vectors: Option<bool>,
416
417    /// Provides multimodal data for search queries.
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub media: Option<Value>,
420
421    /// Request exhaustive facet counts up to the limit defined by `maxTotalHits`.
422    ///
423    /// When set to `true`, Meilisearch computes exact facet counts instead of approximate ones.
424    /// Default is `false`.
425    #[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    /// Defines whether to return performance trace.
432    ///
433    /// **Default: `false`**
434    #[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    /// Weight multiplier for this query when merging federated results
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub weight: Option<f32>,
444    /// Remote instance name to target when sharding; corresponds to a key in network.remotes
445    #[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    /// Add the page number on which to paginate.
505    ///
506    /// # Example
507    ///
508    /// ```
509    /// # use serde::{Serialize, Deserialize};
510    /// # use meilisearch_sdk::{client::*, indexes::*, search::*};
511    /// #
512    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
513    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
514    /// #
515    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
516    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
517    /// # #[derive(Serialize, Deserialize, Debug)]
518    /// # struct Movie {
519    /// #     name: String,
520    /// #     description: String,
521    /// # }
522    /// # client.create_index("search_with_page", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
523    /// let mut index = client.index("search_with_page");
524    ///
525    /// let mut query = SearchQuery::new(&index);
526    /// query.with_query("").with_page(2);
527    /// let res = query.execute::<Movie>().await.unwrap();
528    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
529    /// # });
530    /// ```
531    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    /// Add the maximum number of results per page.
537    ///
538    /// # Example
539    ///
540    /// ```
541    /// # use serde::{Serialize, Deserialize};
542    /// # use meilisearch_sdk::{client::*, indexes::*, search::*};
543    /// #
544    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
545    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
546    /// #
547    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
548    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
549    /// # #[derive(Serialize, Deserialize, Debug)]
550    /// # struct Movie {
551    /// #     name: String,
552    /// #     description: String,
553    /// # }
554    /// # client.create_index("search_with_hits_per_page", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
555    /// let mut index = client.index("search_with_hits_per_page");
556    ///
557    /// let mut query = SearchQuery::new(&index);
558    /// query.with_query("").with_hits_per_page(2);
559    /// let res = query.execute::<Movie>().await.unwrap();
560    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
561    /// # });
562    /// ```
563    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    /// Defines whether document embeddings are returned with search results.
585    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    /// Configures Meilisearch to return search results based on a query’s meaning and context
705    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    /// Use a custom vector to perform a search query
718    ///
719    /// `vector` is mandatory when performing searches with `userProvided` embedders.
720    /// You may also use `vector` to override an embedder’s automatic vector generation.
721    ///
722    /// `vector` dimensions must match the dimensions of the embedder.
723    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    /// Attach media fragments to the search query.
729    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    /// Request exhaustive facet count in the response.
757    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    /// Request performance details in the response.
766    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    /// Execute the query and fetch the results.
775    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    // The weird `serialize = ""` is actually useful: without it, serde adds the
788    // bound `Http: Serialize` to the `Serialize` impl block, but that's not
789    // necessary. `SearchQuery` always implements `Serialize` (regardless of
790    // type parameter), so no bound is fine.
791    #[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    /// Adds the `federation` parameter, turning the search into a federated search.
840    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    /// Execute the query and fetch the results.
852    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/// The `federation` field of the multi search API.
883/// See [the docs](https://www.meilisearch.com/docs/reference/api/multi_search#federation).
884#[derive(Debug, Serialize, Clone, Default)]
885#[serde(rename_all = "camelCase")]
886pub struct FederationOptions {
887    /// Number of documents to skip
888    #[serde(skip_serializing_if = "Option::is_none")]
889    pub offset: Option<usize>,
890
891    /// Maximum number of documents returned
892    #[serde(skip_serializing_if = "Option::is_none")]
893    pub limit: Option<usize>,
894
895    /// Display facet information for the specified indexes
896    #[serde(skip_serializing_if = "Option::is_none")]
897    pub facets_by_index: Option<HashMap<String, Vec<String>>>,
898
899    /// Request to merge the facets to enforce a maximum number of values per facet.
900    #[serde(skip_serializing_if = "Option::is_none")]
901    pub merge_facets: Option<MergeFacets>,
902
903    /// Request performance details in the response.
904    #[serde(skip_serializing_if = "Option::is_none")]
905    pub show_performance_details: Option<bool>,
906}
907
908impl<'a, Http: HttpClient> FederatedMultiSearchQuery<'a, '_, Http> {
909    /// Execute the query and fetch the results.
910    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/// Returned by federated multi search.
926#[derive(Debug, Deserialize, Clone)]
927#[serde(rename_all = "camelCase")]
928pub struct FederatedMultiSearchResponse<T> {
929    /// Merged results of the query.
930    pub hits: Vec<SearchResult<T>>,
931
932    /// Number of documents skipped.
933    pub offset: usize,
934
935    /// Number of results returned.
936    pub limit: usize,
937
938    /// Estimated total number of matches.
939    pub estimated_total_hits: usize,
940
941    /// Processing time of the query.
942    pub processing_time_ms: usize,
943
944    /// [Data for facets present in the search results](https://www.meilisearch.com/docs/reference/api/multi_search#facetsbyindex)
945    pub facets_by_index: Option<ComputedFacets>,
946
947    /// [Distribution of the given facets](https://www.meilisearch.com/docs/reference/api/multi_search#mergefacets)
948    pub facet_distribution: Option<HashMap<String, HashMap<String, usize>>>,
949
950    /// [The numeric `min` and `max` values per facet](https://www.meilisearch.com/docs/reference/api/multi_search#mergefacets)
951    pub facet_stats: Option<HashMap<String, FacetStats>>,
952
953    /// Indicates which remote requests failed and why
954    pub remote_errors: Option<HashMap<String, MeilisearchError>>,
955
956    /// The performance trace for the query.
957    pub performance_details: Option<Value>,
958}
959
960/// Returned for each hit in `_federation` when doing federated multi search.
961#[derive(Serialize, Deserialize, Debug, Clone)]
962#[serde(rename_all = "camelCase")]
963pub struct FederationHitInfo {
964    /// Index of origin for this document
965    pub index_uid: String,
966
967    /// Array index number of the query in the request’s queries array
968    pub queries_position: usize,
969
970    /// Remote instance of origin for this document
971    pub remote: Option<String>,
972
973    /// The product of the _rankingScore of the hit and the weight of the query of origin.
974    pub weighted_ranking_score: f32,
975}
976
977/// A struct representing a facet-search query.
978///
979/// You can add search parameters using the builder syntax.
980///
981/// See [this page](https://www.meilisearch.com/docs/reference/api/facet_search) for the official list and description of all parameters.
982///
983/// # Examples
984///
985/// ```
986/// # use serde::{Serialize, Deserialize};
987/// # use meilisearch_sdk::{client::*, indexes::*, search::*};
988/// #
989/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
990/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
991/// #
992/// #[derive(Serialize)]
993/// struct Movie {
994///     name: String,
995///     genre: String,
996/// }
997/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
998/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
999/// let movies = client.index("execute_query3");
1000///
1001/// // add some documents
1002/// # 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();
1003/// # movies.set_filterable_attributes(["genre"]).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
1004///
1005/// let query = FacetSearchQuery::new(&movies, "genre").with_facet_query("scifi").build();
1006/// let res = movies.execute_facet_query(&query).await.unwrap();
1007///
1008/// assert!(res.facet_hits.len() > 0);
1009/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
1010/// # });
1011/// ```
1012///
1013/// ```
1014/// # use meilisearch_sdk::{client::*, indexes::*, search::*};
1015/// #
1016/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1017/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
1018/// #
1019/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
1020/// # let index = client.index("facet_search_query_builder_build");
1021/// let query = index.facet_search("kind")
1022///     .with_facet_query("space")
1023///     .build(); // you can also execute() instead of build()
1024/// ```
1025
1026#[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    /// The facet name to search values on.
1032    pub facet_name: &'a str,
1033    /// The search query for the facet values.
1034    #[serde(skip_serializing_if = "Option::is_none")]
1035    pub facet_query: Option<&'a str>,
1036    /// The text that will be searched for among the documents.
1037    #[serde(skip_serializing_if = "Option::is_none")]
1038    #[serde(rename = "q")]
1039    pub search_query: Option<&'a str>,
1040    /// Filter applied to documents.
1041    ///
1042    /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
1043    #[serde(skip_serializing_if = "Option::is_none")]
1044    pub filter: Option<Filter<'a>>,
1045    /// Defines the strategy on how to handle search queries containing multiple words.
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub matching_strategy: Option<MatchingStrategies>,
1048    /// Restrict search to the specified attributes
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub attributes_to_search_on: Option<&'a [&'a str]>,
1051    /// Return an exhaustive count of facets, up to the limit defined by maxTotalHits. Default is false.
1052    #[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        // Search with big weight on the test index
1385        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        // Search with big weight on the video index
1415        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        // Make sure federation options are applied
1445        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"])); // omit the "value" field
1656        assert!(index.execute_query::<Document>(&query).await.is_err()); // error: missing "value" field
1657        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        // Request exhaustive facet counts for a specific facet and ensure the server
2065        // returns the exhaustive flag in the response.
2066        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                // Server doesn't support this field on /search yet; treat as a skip.
2087                Ok(())
2088            }
2089            Err(e) => Err(e),
2090        }
2091    }
2092
2093    #[test]
2094    fn test_search_query_serialization_exhaustive_facet_count() {
2095        // Build a query and ensure it serializes using the expected camelCase field name
2096        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        // Search for an Harry Potter but with lorem ipsum's id
2226        // Will yield lorem ipsum first, them harry potter documents, then the rest
2227        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}