use crate::{
errors::Error,
indexes::Index,
request::HttpClient,
search::{Filter, Selectors},
};
use either::Either;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Deserialize, Debug, Clone)]
pub struct SimilarResult<T> {
#[serde(flatten)]
pub result: T,
#[serde(rename = "_rankingScore")]
pub ranking_score: Option<f64>,
#[serde(rename = "_rankingScoreDetails")]
pub ranking_score_details: Option<Map<String, Value>>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SimilarResults<T> {
pub hits: Vec<SimilarResult<T>>,
pub offset: Option<usize>,
pub limit: Option<usize>,
pub estimated_total_hits: Option<usize>,
pub processing_time_ms: usize,
pub id: String,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SimilarQuery<'a, Http: HttpClient> {
#[serde(skip_serializing)]
index: &'a Index<Http>,
pub id: &'a str,
pub embedder: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<Filter<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes_to_retrieve: Option<Selectors<&'a [&'a str]>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_ranking_score: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_ranking_score_details: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ranking_score_threshold: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retrieve_vectors: Option<bool>,
}
#[allow(missing_docs)]
impl<'a, Http: HttpClient> SimilarQuery<'a, Http> {
#[must_use]
pub fn new(index: &'a Index<Http>, id: &'a str, embedder: &'a str) -> SimilarQuery<'a, Http> {
SimilarQuery {
index,
id,
embedder,
offset: None,
limit: None,
filter: None,
attributes_to_retrieve: None,
show_ranking_score: None,
show_ranking_score_details: None,
ranking_score_threshold: None,
retrieve_vectors: None,
}
}
pub fn with_offset<'b>(&'b mut self, offset: usize) -> &'b mut SimilarQuery<'a, Http> {
self.offset = Some(offset);
self
}
pub fn with_limit<'b>(&'b mut self, limit: usize) -> &'b mut SimilarQuery<'a, Http> {
self.limit = Some(limit);
self
}
pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut SimilarQuery<'a, Http> {
self.filter = Some(Filter::new(Either::Left(filter)));
self
}
pub fn with_array_filter<'b>(
&'b mut self,
filter: Vec<&'a str>,
) -> &'b mut SimilarQuery<'a, Http> {
self.filter = Some(Filter::new(Either::Right(filter)));
self
}
pub fn with_attributes_to_retrieve<'b>(
&'b mut self,
attributes_to_retrieve: Selectors<&'a [&'a str]>,
) -> &'b mut SimilarQuery<'a, Http> {
self.attributes_to_retrieve = Some(attributes_to_retrieve);
self
}
pub fn with_show_ranking_score<'b>(
&'b mut self,
show_ranking_score: bool,
) -> &'b mut SimilarQuery<'a, Http> {
self.show_ranking_score = Some(show_ranking_score);
self
}
pub fn with_show_ranking_score_details<'b>(
&'b mut self,
show_ranking_score_details: bool,
) -> &'b mut SimilarQuery<'a, Http> {
self.show_ranking_score_details = Some(show_ranking_score_details);
self
}
pub fn with_ranking_score_threshold<'b>(
&'b mut self,
ranking_score_threshold: f64,
) -> &'b mut SimilarQuery<'a, Http> {
self.ranking_score_threshold = Some(ranking_score_threshold);
self
}
pub fn with_retrieve_vectors<'b>(
&'b mut self,
retrieve_vectors: bool,
) -> &'b mut SimilarQuery<'a, Http> {
self.retrieve_vectors = Some(retrieve_vectors);
self
}
pub async fn execute<T: 'static + DeserializeOwned + Send + Sync>(
&'a self,
) -> Result<SimilarResults<T>, Error> {
self.index.execute_similar_query::<T>(self).await
}
}
#[cfg(test)]
mod tests {
use std::vec;
use super::*;
use crate::{
client::*,
search::{
tests::{setup_embedder, setup_test_index, Document},
*,
},
};
use meilisearch_test_macro::meilisearch_test;
#[meilisearch_test]
async fn test_similar_results(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "0", "default");
query.with_limit(1);
let results: SimilarResults<Document> = query.execute().await?;
let result = results.hits.first().unwrap();
assert_eq!(result.result.id, 1);
let mut query = SimilarQuery::new(&index, "3", "default");
query.with_limit(1);
let results: SimilarResults<Document> = query.execute().await?;
let result = results.hits.first().unwrap();
assert_eq!(result.result.id, 4);
Ok(())
}
#[meilisearch_test]
async fn test_query_limit(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
query.with_limit(3);
let results: SimilarResults<Document> = query.execute().await?;
assert_eq!(results.hits.len(), 3);
Ok(())
}
#[meilisearch_test]
async fn test_query_offset(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
query.with_offset(6);
let results: SimilarResults<Document> = query.execute().await?;
assert_eq!(results.hits.len(), 3);
Ok(())
}
#[meilisearch_test]
async fn test_query_filter(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
let results: SimilarResults<Document> =
query.with_filter("kind = \"title\"").execute().await?;
assert_eq!(results.hits.len(), 8);
let results: SimilarResults<Document> =
query.with_filter("NOT kind = \"title\"").execute().await?;
assert_eq!(results.hits.len(), 1);
Ok(())
}
#[meilisearch_test]
async fn test_query_filter_with_array(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
let results: SimilarResults<Document> = query
.with_array_filter(vec!["kind = \"title\"", "kind = \"text\""])
.execute()
.await?;
assert_eq!(results.hits.len(), 0);
let mut query = SimilarQuery::new(&index, "1", "default");
let results: SimilarResults<Document> = query
.with_array_filter(vec!["kind = \"title\"", "number <= 50"])
.execute()
.await?;
assert_eq!(results.hits.len(), 4);
Ok(())
}
#[meilisearch_test]
async fn test_query_attributes_to_retrieve(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
let results: SimilarResults<Document> = query
.with_attributes_to_retrieve(Selectors::All)
.execute()
.await?;
assert_eq!(results.hits.len(), 9);
let mut query = SimilarQuery::new(&index, "1", "default");
query.with_attributes_to_retrieve(Selectors::Some(&["title", "id"])); assert!(query.execute::<Document>().await.is_err()); Ok(())
}
#[meilisearch_test]
async fn test_query_show_ranking_score(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
query.with_show_ranking_score(true);
let results: SimilarResults<Document> = query.execute().await?;
assert!(results.hits[0].ranking_score.is_some());
Ok(())
}
#[meilisearch_test]
async fn test_query_show_ranking_score_details(
client: Client,
index: Index,
) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
query.with_show_ranking_score_details(true);
let results: SimilarResults<Document> = query.execute().await?;
assert!(results.hits[0].ranking_score_details.is_some());
Ok(())
}
#[meilisearch_test]
async fn test_query_show_ranking_score_threshold(
client: Client,
index: Index,
) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
query.with_ranking_score_threshold(1.0);
let results: SimilarResults<Document> = query.execute().await?;
assert!(results.hits.is_empty());
Ok(())
}
#[meilisearch_test]
async fn test_query_retrieve_vectors(client: Client, index: Index) -> Result<(), Error> {
setup_embedder(&client, &index).await?;
setup_test_index(&client, &index).await?;
let mut query = SimilarQuery::new(&index, "1", "default");
query.with_retrieve_vectors(true);
let results: SimilarResults<Document> = query.execute().await?;
assert!(results.hits[0].result._vectors.is_some());
Ok(())
}
}