From d262b1df327e80579985d46bf5696305fb2ee9d7 Mon Sep 17 00:00:00 2001 From: Tamo Date: Tue, 30 Jul 2024 10:27:57 +0200 Subject: [PATCH] craft an API over the Shared Server and Shared index to avoid hard to debug mistakes --- meilisearch/tests/common/index.rs | 257 ++++++++++++++---------- meilisearch/tests/common/mod.rs | 270 +++++++++++++++++++++++++- meilisearch/tests/common/server.rs | 238 +++++++++++++++-------- meilisearch/tests/documents/errors.rs | 102 ++++------ meilisearch/tests/index/errors.rs | 41 ++-- meilisearch/tests/search/errors.rs | 11 +- meilisearch/tests/search/mod.rs | 241 +---------------------- meilisearch/tests/settings/errors.rs | 24 +-- milli/src/search/facet/filter.rs | 30 ++- 9 files changed, 668 insertions(+), 546 deletions(-) diff --git a/meilisearch/tests/common/index.rs b/meilisearch/tests/common/index.rs index 045f8673c..381bd1cb4 100644 --- a/meilisearch/tests/common/index.rs +++ b/meilisearch/tests/common/index.rs @@ -1,4 +1,5 @@ use std::fmt::Write; +use std::marker::PhantomData; use std::panic::{catch_unwind, resume_unwind, UnwindSafe}; use std::time::Duration; @@ -9,19 +10,24 @@ use urlencoding::encode as urlencode; use super::encoder::Encoder; use super::service::Service; use super::Value; +use super::{Owned, Shared}; use crate::json; -pub struct Index<'a> { +pub struct Index<'a, State = Owned> { pub uid: String, pub service: &'a Service, - pub encoder: Encoder, + pub(super) encoder: Encoder, + pub(super) marker: PhantomData, } -#[allow(dead_code)] -impl Index<'_> { - pub async fn get(&self) -> (Value, StatusCode) { - let url = format!("/indexes/{}", urlencode(self.uid.as_ref())); - self.service.get(url).await +impl<'a> Index<'a, Owned> { + pub fn to_shared(&self) -> Index<'a, Shared> { + Index { + uid: self.uid.clone(), + service: self.service, + encoder: self.encoder, + marker: PhantomData, + } } pub async fn load_test_set(&self) -> u64 { @@ -57,11 +63,7 @@ impl Index<'_> { } pub async fn create(&self, primary_key: Option<&str>) -> (Value, StatusCode) { - let body = json!({ - "uid": self.uid, - "primaryKey": primary_key, - }); - self.service.post_encoded("/indexes", body, self.encoder).await + self._create(primary_key).await } pub async fn update_raw(&self, body: Value) -> (Value, StatusCode) { @@ -88,13 +90,7 @@ impl Index<'_> { documents: Value, primary_key: Option<&str>, ) -> (Value, StatusCode) { - let url = match primary_key { - Some(key) => { - format!("/indexes/{}/documents?primaryKey={}", urlencode(self.uid.as_ref()), key) - } - None => format!("/indexes/{}/documents", urlencode(self.uid.as_ref())), - }; - self.service.post_encoded(url, documents, self.encoder).await + self._add_documents(documents, primary_key).await } pub async fn raw_add_documents( @@ -136,80 +132,11 @@ impl Index<'_> { } } - pub async fn wait_task(&self, update_id: u64) -> Value { - // try several times to get status, or panic to not wait forever - let url = format!("/tasks/{}", update_id); - for _ in 0..100 { - let (response, status_code) = self.service.get(&url).await; - assert_eq!(200, status_code, "response: {}", response); - - if response["status"] == "succeeded" || response["status"] == "failed" { - return response; - } - - // wait 0.5 second. - sleep(Duration::from_millis(500)).await; - } - panic!("Timeout waiting for update id"); - } - - pub async fn get_task(&self, update_id: u64) -> (Value, StatusCode) { - let url = format!("/tasks/{}", update_id); - self.service.get(url).await - } - pub async fn list_tasks(&self) -> (Value, StatusCode) { let url = format!("/tasks?indexUids={}", self.uid); self.service.get(url).await } - pub async fn filtered_tasks( - &self, - types: &[&str], - statuses: &[&str], - canceled_by: &[&str], - ) -> (Value, StatusCode) { - let mut url = format!("/tasks?indexUids={}", self.uid); - if !types.is_empty() { - let _ = write!(url, "&types={}", types.join(",")); - } - if !statuses.is_empty() { - let _ = write!(url, "&statuses={}", statuses.join(",")); - } - if !canceled_by.is_empty() { - let _ = write!(url, "&canceledBy={}", canceled_by.join(",")); - } - self.service.get(url).await - } - - pub async fn get_document(&self, id: u64, options: Option) -> (Value, StatusCode) { - let mut url = format!("/indexes/{}/documents/{}", urlencode(self.uid.as_ref()), id); - if let Some(options) = options { - write!(url, "{}", yaup::to_string(&options).unwrap()).unwrap(); - } - self.service.get(url).await - } - - pub async fn get_document_by_filter(&self, payload: Value) -> (Value, StatusCode) { - let url = format!("/indexes/{}/documents/fetch", urlencode(self.uid.as_ref())); - self.service.post(url, payload).await - } - - pub async fn get_all_documents_raw(&self, options: &str) -> (Value, StatusCode) { - let url = format!("/indexes/{}/documents{}", urlencode(self.uid.as_ref()), options); - self.service.get(url).await - } - - pub async fn get_all_documents(&self, options: GetAllDocumentsOptions) -> (Value, StatusCode) { - let url = format!( - "/indexes/{}/documents{}", - urlencode(self.uid.as_ref()), - yaup::to_string(&options).unwrap() - ); - - self.service.get(url).await - } - pub async fn delete_document(&self, id: u64) -> (Value, StatusCode) { let url = format!("/indexes/{}/documents/{}", urlencode(self.uid.as_ref()), id); self.service.delete(url).await @@ -237,14 +164,8 @@ impl Index<'_> { self.service.post_encoded(url, body, self.encoder).await } - pub async fn settings(&self) -> (Value, StatusCode) { - let url = format!("/indexes/{}/settings", urlencode(self.uid.as_ref())); - self.service.get(url).await - } - pub async fn update_settings(&self, settings: Value) -> (Value, StatusCode) { - let url = format!("/indexes/{}/settings", urlencode(self.uid.as_ref())); - self.service.patch_encoded(url, settings, self.encoder).await + self._update_settings(settings).await } pub async fn update_settings_displayed_attributes( @@ -327,6 +248,146 @@ impl Index<'_> { self.service.delete(url).await } + pub async fn update_distinct_attribute(&self, value: Value) -> (Value, StatusCode) { + let url = + format!("/indexes/{}/settings/{}", urlencode(self.uid.as_ref()), "distinct-attribute"); + self.service.put_encoded(url, value, self.encoder).await + } +} + +impl<'a> Index<'a, Shared> { + /// You cannot modify the content of a shared index, thus the delete_document_by_filter call + /// must fail. If the task successfully enqueue itself, we'll wait for the task to finishes, + /// and if it succeed the function will panic. + pub async fn delete_document_by_filter_fail(&self, body: Value) -> (Value, StatusCode) { + let (mut task, code) = self._delete_document_by_filter(body).await; + if code.is_success() { + task = self.wait_task(task.uid()).await; + if task.is_success() { + panic!( + "`delete_document_by_filter_fail` succeeded: {}", + serde_json::to_string_pretty(&task).unwrap() + ); + } + } + (task, code) + } +} + +#[allow(dead_code)] +impl Index<'_, State> { + pub async fn get(&self) -> (Value, StatusCode) { + let url = format!("/indexes/{}", urlencode(self.uid.as_ref())); + self.service.get(url).await + } + + /// add_documents is not allowed on shared index but we need to use it to initialize + /// a bunch of very common indexes in `common/mod.rs`. + pub(super) async fn _add_documents( + &self, + documents: Value, + primary_key: Option<&str>, + ) -> (Value, StatusCode) { + let url = match primary_key { + Some(key) => { + format!("/indexes/{}/documents?primaryKey={}", urlencode(self.uid.as_ref()), key) + } + None => format!("/indexes/{}/documents", urlencode(self.uid.as_ref())), + }; + self.service.post_encoded(url, documents, self.encoder).await + } + + pub(super) async fn _update_settings(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings", urlencode(self.uid.as_ref())); + self.service.patch_encoded(url, settings, self.encoder).await + } + + pub(super) async fn _delete_document_by_filter(&self, body: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/documents/delete", urlencode(self.uid.as_ref())); + self.service.post_encoded(url, body, self.encoder).await + } + + pub(super) async fn _create(&self, primary_key: Option<&str>) -> (Value, StatusCode) { + let body = json!({ + "uid": self.uid, + "primaryKey": primary_key, + }); + self.service.post_encoded("/indexes", body, self.encoder).await + } + pub async fn wait_task(&self, update_id: u64) -> Value { + // try several times to get status, or panic to not wait forever + let url = format!("/tasks/{}", update_id); + for _ in 0..100 { + let (response, status_code) = self.service.get(&url).await; + assert_eq!(200, status_code, "response: {}", response); + + if response["status"] == "succeeded" || response["status"] == "failed" { + return response; + } + + // wait 0.5 second. + sleep(Duration::from_millis(500)).await; + } + panic!("Timeout waiting for update id"); + } + + pub async fn get_task(&self, update_id: u64) -> (Value, StatusCode) { + let url = format!("/tasks/{}", update_id); + self.service.get(url).await + } + + pub async fn filtered_tasks( + &self, + types: &[&str], + statuses: &[&str], + canceled_by: &[&str], + ) -> (Value, StatusCode) { + let mut url = format!("/tasks?indexUids={}", self.uid); + if !types.is_empty() { + let _ = write!(url, "&types={}", types.join(",")); + } + if !statuses.is_empty() { + let _ = write!(url, "&statuses={}", statuses.join(",")); + } + if !canceled_by.is_empty() { + let _ = write!(url, "&canceledBy={}", canceled_by.join(",")); + } + self.service.get(url).await + } + + pub async fn get_document(&self, id: u64, options: Option) -> (Value, StatusCode) { + let mut url = format!("/indexes/{}/documents/{}", urlencode(self.uid.as_ref()), id); + if let Some(options) = options { + write!(url, "{}", yaup::to_string(&options).unwrap()).unwrap(); + } + self.service.get(url).await + } + + pub async fn get_document_by_filter(&self, payload: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/documents/fetch", urlencode(self.uid.as_ref())); + self.service.post(url, payload).await + } + + pub async fn get_all_documents_raw(&self, options: &str) -> (Value, StatusCode) { + let url = format!("/indexes/{}/documents{}", urlencode(self.uid.as_ref()), options); + self.service.get(url).await + } + + pub async fn get_all_documents(&self, options: GetAllDocumentsOptions) -> (Value, StatusCode) { + let url = format!( + "/indexes/{}/documents{}", + urlencode(self.uid.as_ref()), + yaup::to_string(&options).unwrap() + ); + + self.service.get(url).await + } + + pub async fn settings(&self) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings", urlencode(self.uid.as_ref())); + self.service.get(url).await + } + pub async fn stats(&self) -> (Value, StatusCode) { let url = format!("/indexes/{}/stats", urlencode(self.uid.as_ref())); self.service.get(url).await @@ -411,12 +472,6 @@ impl Index<'_> { self.service.post_encoded(url, query, self.encoder).await } - pub async fn update_distinct_attribute(&self, value: Value) -> (Value, StatusCode) { - let url = - format!("/indexes/{}/settings/{}", urlencode(self.uid.as_ref()), "distinct-attribute"); - self.service.put_encoded(url, value, self.encoder).await - } - pub async fn get_distinct_attribute(&self) -> (Value, StatusCode) { let url = format!("/indexes/{}/settings/{}", urlencode(self.uid.as_ref()), "distinct-attribute"); diff --git a/meilisearch/tests/common/mod.rs b/meilisearch/tests/common/mod.rs index 0c4e8e25c..58f531b6d 100644 --- a/meilisearch/tests/common/mod.rs +++ b/meilisearch/tests/common/mod.rs @@ -8,10 +8,16 @@ use std::fmt::{self, Display}; #[allow(unused)] pub use index::GetAllDocumentsOptions; use meili_snap::json_string; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; #[allow(unused)] pub use server::{default_settings, Server}; +use crate::common::index::Index; + +pub enum Shared {} +pub enum Owned {} + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct Value(pub serde_json::Value); @@ -27,10 +33,20 @@ impl Value { } } + /// Return `true` if the `status` field is set to `succeeded`. + /// Panic if the `status` field doesn't exists. + #[track_caller] + pub fn is_success(&self) -> bool { + if !self["status"].is_string() { + panic!("Called `is_success` on {}", serde_json::to_string_pretty(&self.0).unwrap()); + } + self["status"] == serde_json::Value::String(String::from("succeeded")) + } + // Panic if the json doesn't contain the `status` field set to "succeeded" #[track_caller] pub fn succeeded(&self) -> &Self { - if self["status"] != serde_json::Value::String(String::from("succeeded")) { + if !self.is_success() { panic!("Called succeeded on {}", serde_json::to_string_pretty(&self.0).unwrap()); } self @@ -122,3 +138,255 @@ macro_rules! test_post_get_search { .map_err(|e| panic!("panic in post route: {:?}", e.downcast_ref::<&str>().unwrap())); }; } + +pub async fn shared_does_not_exists_index() -> &'static Index<'static, Shared> { + static INDEX: Lazy> = Lazy::new(|| { + let server = Server::new_shared(); + server._index("DOES_NOT_EXISTS").to_shared() + }); + &INDEX +} + +pub async fn shared_empty_index() -> &'static Index<'static, Shared> { + static INDEX: Lazy> = Lazy::new(|| { + let server = Server::new_shared(); + server._index("EMPTY_INDEX").to_shared() + }); + let index = Lazy::get(&INDEX); + // That means the lazy has never been initialized, we need to create the index and index the documents + if index.is_none() { + let (response, _code) = INDEX._create(None).await; + INDEX.wait_task(response.uid()).await.succeeded(); + } + &INDEX +} + +pub static DOCUMENTS: Lazy = Lazy::new(|| { + json!([ + { + "title": "Shazam!", + "id": "287947", + "_vectors": { "manual": [1, 2, 3]}, + }, + { + "title": "Captain Marvel", + "id": "299537", + "_vectors": { "manual": [1, 2, 54] }, + }, + { + "title": "Escape Room", + "id": "522681", + "_vectors": { "manual": [10, -23, 32] }, + }, + { + "title": "How to Train Your Dragon: The Hidden World", + "id": "166428", + "_vectors": { "manual": [-100, 231, 32] }, + }, + { + "title": "Gläss", + "id": "450465", + "_vectors": { "manual": [-100, 340, 90] }, + } + ]) +}); + +pub async fn shared_index_with_documents() -> &'static Index<'static, Shared> { + // We cannot store a `Shared` index directly because we need to do more initialization work later on + static INDEX: Lazy> = Lazy::new(|| { + let server = Server::new_shared(); + server._index("SHARED_DOCUMENTS").to_shared() + }); + let index = Lazy::get(&INDEX); + // That means the lazy has never been initialized, we need to create the index and index the documents + if index.is_none() { + let documents = DOCUMENTS.clone(); + let (response, _code) = INDEX._add_documents(documents, None).await; + INDEX.wait_task(response.uid()).await.succeeded(); + let (response, _code) = INDEX + ._update_settings( + json!({"filterableAttributes": ["id", "title"], "sortableAttributes": ["id", "title"]}), + ) + .await; + INDEX.wait_task(response.uid()).await.succeeded(); + } + &INDEX +} + +pub static SCORE_DOCUMENTS: Lazy = Lazy::new(|| { + json!([ + { + "title": "Batman the dark knight returns: Part 1", + "id": "A", + }, + { + "title": "Batman the dark knight returns: Part 2", + "id": "B", + }, + { + "title": "Batman Returns", + "id": "C", + }, + { + "title": "Batman", + "id": "D", + }, + { + "title": "Badman", + "id": "E", + } + ]) +}); + +pub static NESTED_DOCUMENTS: Lazy = Lazy::new(|| { + json!([ + { + "id": 852, + "father": "jean", + "mother": "michelle", + "doggos": [ + { + "name": "bobby", + "age": 2, + }, + { + "name": "buddy", + "age": 4, + }, + ], + "cattos": "pésti", + "_vectors": { "manual": [1, 2, 3]}, + }, + { + "id": 654, + "father": "pierre", + "mother": "sabine", + "doggos": [ + { + "name": "gros bill", + "age": 8, + }, + ], + "cattos": ["simba", "pestiféré"], + "_vectors": { "manual": [1, 2, 54] }, + }, + { + "id": 750, + "father": "romain", + "mother": "michelle", + "cattos": ["enigma"], + "_vectors": { "manual": [10, 23, 32] }, + }, + { + "id": 951, + "father": "jean-baptiste", + "mother": "sophie", + "doggos": [ + { + "name": "turbo", + "age": 5, + }, + { + "name": "fast", + "age": 6, + }, + ], + "cattos": ["moumoute", "gomez"], + "_vectors": { "manual": [10, 23, 32] }, + }, + ]) +}); + +pub async fn shared_index_with_nested_documents() -> &'static Index<'static, Shared> { + static INDEX: Lazy> = Lazy::new(|| { + let server = Server::new_shared(); + server._index("SHARED_NESTED_DOCUMENTS").to_shared() + }); + let index = Lazy::get(&INDEX); + // That means the lazy has never been initialized, we need to create the index and index the documents + if index.is_none() { + let documents = NESTED_DOCUMENTS.clone(); + let (response, _code) = INDEX._add_documents(documents, None).await; + INDEX.wait_task(response.uid()).await.succeeded(); + let (response, _code) = INDEX + ._update_settings( + json!({"filterableAttributes": ["father", "doggos"], "sortableAttributes": ["doggos"]}), + ) + .await; + INDEX.wait_task(response.uid()).await.succeeded(); + } + &INDEX +} + +pub static FRUITS_DOCUMENTS: Lazy = Lazy::new(|| { + json!([ + { + "name": "Exclusive sale: green apple", + "id": "green-apple-boosted", + "BOOST": true + }, + { + "name": "Pear", + "id": "pear", + }, + { + "name": "Red apple gala", + "id": "red-apple-gala", + }, + { + "name": "Exclusive sale: Red Tomato", + "id": "red-tomatoes-boosted", + "BOOST": true + }, + { + "name": "Exclusive sale: Red delicious apple", + "id": "red-delicious-boosted", + "BOOST": true, + } + ]) +}); + +pub static VECTOR_DOCUMENTS: Lazy = Lazy::new(|| { + json!([ + { + "id": "A", + "description": "the dog barks at the cat", + "_vectors": { + // dimensions [canine, feline, young] + "animal": [0.9, 0.8, 0.05], + // dimensions [negative/positive, energy] + "sentiment": [-0.1, 0.55] + } + }, + { + "id": "B", + "description": "the kitten scratched the beagle", + "_vectors": { + // dimensions [canine, feline, young] + "animal": [0.8, 0.9, 0.5], + // dimensions [negative/positive, energy] + "sentiment": [-0.2, 0.65] + } + }, + { + "id": "C", + "description": "the dog had to stay alone today", + "_vectors": { + // dimensions [canine, feline, young] + "animal": [0.85, 0.02, 0.1], + // dimensions [negative/positive, energy] + "sentiment": [-1.0, 0.1] + } + }, + { + "id": "D", + "description": "the little boy pets the puppy", + "_vectors": { + // dimensions [canine, feline, young] + "animal": [0.8, 0.09, 0.8], + // dimensions [negative/positive, energy] + "sentiment": [0.8, 0.3] + } + }, + ]) +}); diff --git a/meilisearch/tests/common/server.rs b/meilisearch/tests/common/server.rs index 28ddbe754..e69b3eb6e 100644 --- a/meilisearch/tests/common/server.rs +++ b/meilisearch/tests/common/server.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] +use std::marker::PhantomData; use std::path::Path; use std::str::FromStr as _; use std::time::Duration; @@ -20,41 +21,22 @@ use uuid::Uuid; use super::index::Index; use super::service::Service; +use super::{Owned, Shared}; use crate::common::encoder::Encoder; use crate::common::Value; use crate::json; -pub struct Server { +pub struct Server { pub service: Service, // hold ownership to the tempdir while we use the server instance. _dir: Option, + _marker: PhantomData, } pub static TEST_TEMP_DIR: Lazy = Lazy::new(|| TempDir::new().unwrap()); -pub static TEST_SHARED_INSTANCE: Lazy = Lazy::new(Server::init_new_shared_instance); - -impl Server { - fn init_new_shared_instance() -> Self { - let dir = TempDir::new().unwrap(); - - if cfg!(windows) { - std::env::set_var("TMP", TEST_TEMP_DIR.path()); - } else { - std::env::set_var("TMPDIR", TEST_TEMP_DIR.path()); - } - - let options = default_settings(dir.path()); - - let (index_scheduler, auth) = setup_meilisearch(&options).unwrap(); - let service = Service { index_scheduler, auth, options, api_key: None }; - - Server { service, _dir: Some(dir) } - } - - pub fn new_shared() -> &'static Self { - &TEST_SHARED_INSTANCE - } +pub static TEST_SHARED_INSTANCE: Lazy> = Lazy::new(Server::init_new_shared_instance); +impl Server { pub async fn new() -> Self { let dir = TempDir::new().unwrap(); @@ -69,7 +51,7 @@ impl Server { let (index_scheduler, auth) = setup_meilisearch(&options).unwrap(); let service = Service { index_scheduler, auth, options, api_key: None }; - Server { service, _dir: Some(dir) } + Server { service, _dir: Some(dir), _marker: PhantomData } } pub async fn new_auth_with_options(mut options: Opt, dir: TempDir) -> Self { @@ -84,7 +66,7 @@ impl Server { let (index_scheduler, auth) = setup_meilisearch(&options).unwrap(); let service = Service { index_scheduler, auth, options, api_key: None }; - Server { service, _dir: Some(dir) } + Server { service, _dir: Some(dir), _marker: PhantomData } } pub async fn new_auth() -> Self { @@ -97,9 +79,139 @@ impl Server { let (index_scheduler, auth) = setup_meilisearch(&options)?; let service = Service { index_scheduler, auth, options, api_key: None }; - Ok(Server { service, _dir: None }) + Ok(Server { service, _dir: None, _marker: PhantomData }) } + /// Returns a view to an index. There is no guarantee that the index exists. + pub fn index(&self, uid: impl AsRef) -> Index<'_> { + self.index_with_encoder(uid, Encoder::Plain) + } + + pub async fn create_index(&self, body: Value) -> (Value, StatusCode) { + self.service.post("/indexes", body).await + } + + pub fn index_with_encoder(&self, uid: impl AsRef, encoder: Encoder) -> Index<'_> { + Index { + uid: uid.as_ref().to_string(), + service: &self.service, + encoder, + marker: PhantomData, + } + } + + pub async fn list_indexes( + &self, + offset: Option, + limit: Option, + ) -> (Value, StatusCode) { + let (offset, limit) = ( + offset.map(|offset| format!("offset={offset}")), + limit.map(|limit| format!("limit={limit}")), + ); + let query_parameter = offset + .as_ref() + .zip(limit.as_ref()) + .map(|(offset, limit)| format!("{offset}&{limit}")) + .or_else(|| offset.xor(limit)); + if let Some(query_parameter) = query_parameter { + self.service.get(format!("/indexes?{query_parameter}")).await + } else { + self.service.get("/indexes").await + } + } + + pub async fn stats(&self) -> (Value, StatusCode) { + self.service.get("/stats").await + } + + pub async fn tasks(&self) -> (Value, StatusCode) { + self.service.get("/tasks").await + } + + pub async fn set_features(&self, value: Value) -> (Value, StatusCode) { + self.service.patch("/experimental-features", value).await + } + + pub async fn get_metrics(&self) -> (Value, StatusCode) { + self.service.get("/metrics").await + } +} + +impl Server { + fn init_new_shared_instance() -> Server { + let dir = TempDir::new().unwrap(); + + if cfg!(windows) { + std::env::set_var("TMP", TEST_TEMP_DIR.path()); + } else { + std::env::set_var("TMPDIR", TEST_TEMP_DIR.path()); + } + + let options = default_settings(dir.path()); + + let (index_scheduler, auth) = setup_meilisearch(&options).unwrap(); + let service = Service { index_scheduler, auth, options, api_key: None }; + + Server { service, _dir: Some(dir), _marker: PhantomData } + } + + pub fn new_shared() -> &'static Server { + &TEST_SHARED_INSTANCE + } + + /// You shouldn't access random indexes on a shared instance thus this method + /// must fail. + pub async fn get_index_fail(&self, uid: impl AsRef) -> (Value, StatusCode) { + let url = format!("/indexes/{}", urlencoding::encode(uid.as_ref())); + let (value, code) = self.service.get(url).await; + if code.is_success() { + panic!("`get_index_fail` succeeded with uid: {}", uid.as_ref()); + } + (value, code) + } + + pub async fn delete_index_fail(&self, uid: impl AsRef) -> (Value, StatusCode) { + let url = format!("/indexes/{}", urlencoding::encode(uid.as_ref())); + let (value, code) = self.service.delete(url).await; + if code.is_success() { + panic!("`delete_index_fail` succeeded with uid: {}", uid.as_ref()); + } + (value, code) + } + + pub async fn update_raw_index_fail( + &self, + uid: impl AsRef, + body: Value, + ) -> (Value, StatusCode) { + let url = format!("/indexes/{}", urlencoding::encode(uid.as_ref())); + let (value, code) = self.service.patch_encoded(url, body, Encoder::Plain).await; + if code.is_success() { + panic!("`update_raw_index_fail` succeeded with uid: {}", uid.as_ref()); + } + (value, code) + } + + /// Since this call updates the state of the instance, it must fail. + /// If it doesn't fail, the test will panic to help you debug what + /// is going on. + pub async fn create_index_fail(&self, body: Value) -> (Value, StatusCode) { + let (mut task, code) = self._create_index(body).await; + if code.is_success() { + task = self.wait_task(task.uid()).await; + if task.is_success() { + panic!( + "`create_index_fail` succeeded: {}", + serde_json::to_string_pretty(&task).unwrap() + ); + } + } + (task, code) + } +} + +impl Server { pub async fn init_web_app( &self, ) -> impl actix_web::dev::Service< @@ -131,25 +243,30 @@ impl Server { .await } + pub(super) fn _index(&self, uid: impl AsRef) -> Index<'_> { + Index { + uid: uid.as_ref().to_string(), + service: &self.service, + encoder: Encoder::Plain, + marker: PhantomData, + } + } + /// Returns a view to an index. There is no guarantee that the index exists. pub fn unique_index(&self) -> Index<'_> { let uuid = Uuid::new_v4(); - self.index_with_encoder(uuid.to_string(), Encoder::Plain) + Index { + uid: uuid.to_string(), + service: &self.service, + encoder: Encoder::Plain, + marker: PhantomData, + } } - /// Returns a view to an index. There is no guarantee that the index exists. - pub fn index(&self, uid: impl AsRef) -> Index<'_> { - self.index_with_encoder(uid, Encoder::Plain) - } - - pub async fn create_index(&self, body: Value) -> (Value, StatusCode) { + pub(super) async fn _create_index(&self, body: Value) -> (Value, StatusCode) { self.service.post("/indexes", body).await } - pub fn index_with_encoder(&self, uid: impl AsRef, encoder: Encoder) -> Index<'_> { - Index { uid: uid.as_ref().to_string(), service: &self.service, encoder } - } - pub async fn multi_search(&self, queries: Value) -> (Value, StatusCode) { self.service.post("/multi-search", queries).await } @@ -158,45 +275,12 @@ impl Server { self.service.get(format!("/indexes{parameters}")).await } - pub async fn list_indexes( - &self, - offset: Option, - limit: Option, - ) -> (Value, StatusCode) { - let (offset, limit) = ( - offset.map(|offset| format!("offset={offset}")), - limit.map(|limit| format!("limit={limit}")), - ); - let query_parameter = offset - .as_ref() - .zip(limit.as_ref()) - .map(|(offset, limit)| format!("{offset}&{limit}")) - .or_else(|| offset.xor(limit)); - if let Some(query_parameter) = query_parameter { - self.service.get(format!("/indexes?{query_parameter}")).await - } else { - self.service.get("/indexes").await - } - } - - pub async fn version(&self) -> (Value, StatusCode) { - self.service.get("/version").await - } - - pub async fn stats(&self) -> (Value, StatusCode) { - self.service.get("/stats").await - } - - pub async fn tasks(&self) -> (Value, StatusCode) { - self.service.get("/tasks").await - } - pub async fn tasks_filter(&self, filter: &str) -> (Value, StatusCode) { self.service.get(format!("/tasks?{}", filter)).await } - pub async fn get_dump_status(&self, uid: &str) -> (Value, StatusCode) { - self.service.get(format!("/dumps/{}/status", uid)).await + pub async fn version(&self) -> (Value, StatusCode) { + self.service.get("/version").await } pub async fn create_dump(&self) -> (Value, StatusCode) { @@ -244,14 +328,6 @@ impl Server { pub async fn get_features(&self) -> (Value, StatusCode) { self.service.get("/experimental-features").await } - - pub async fn set_features(&self, value: Value) -> (Value, StatusCode) { - self.service.patch("/experimental-features", value).await - } - - pub async fn get_metrics(&self) -> (Value, StatusCode) { - self.service.get("/metrics").await - } } pub fn default_settings(dir: impl AsRef) -> Opt { diff --git a/meilisearch/tests/documents/errors.rs b/meilisearch/tests/documents/errors.rs index 2d2c2bd6b..2c5f2a965 100644 --- a/meilisearch/tests/documents/errors.rs +++ b/meilisearch/tests/documents/errors.rs @@ -1,14 +1,15 @@ use meili_snap::*; use urlencoding::encode; -use crate::common::Server; +use crate::common::{ + shared_does_not_exists_index, shared_empty_index, shared_index_with_documents, Server, +}; use crate::json; #[actix_rt::test] async fn get_all_documents_bad_offset() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.get_all_documents_raw("?offset").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -46,8 +47,7 @@ async fn get_all_documents_bad_offset() { #[actix_rt::test] async fn get_all_documents_bad_limit() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.get_all_documents_raw("?limit").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -84,17 +84,13 @@ async fn get_all_documents_bad_limit() { #[actix_rt::test] async fn get_all_documents_bad_filter() { - let server = Server::new_shared(); - let index = server.index("test"); + let index = shared_does_not_exists_index().await; - // Since the filter can't be parsed automatically by deserr, we have the wrong error message - // if the index does not exist: we could expect to get an error message about the invalid filter before - // the existence of the index is checked, but it is not the case. let (response, code) = index.get_all_documents_raw("?filter").await; snapshot!(code, @"404 Not Found"); snapshot!(json_string!(response), @r###" { - "message": "Index `test` not found.", + "message": "Index `DOES_NOT_EXISTS` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" @@ -105,7 +101,7 @@ async fn get_all_documents_bad_filter() { snapshot!(code, @"404 Not Found"); snapshot!(json_string!(response), @r###" { - "message": "Index `test` not found.", + "message": "Index `DOES_NOT_EXISTS` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" @@ -116,15 +112,14 @@ async fn get_all_documents_bad_filter() { snapshot!(code, @"404 Not Found"); snapshot!(json_string!(response), @r###" { - "message": "Index `test` not found.", + "message": "Index `DOES_NOT_EXISTS` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" } "###); - let (response, _code) = index.create(None).await; - server.wait_task(response.uid()).await.succeeded(); + let index = shared_empty_index().await; let (response, code) = index.get_all_documents_raw("?filter").await; snapshot!(code, @"200 OK"); @@ -163,8 +158,7 @@ async fn get_all_documents_bad_filter() { #[actix_rt::test] async fn delete_documents_batch() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.delete_batch_raw(json!("doggo")).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -180,8 +174,7 @@ async fn delete_documents_batch() { #[actix_rt::test] async fn replace_documents_missing_payload() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_add_documents("", vec![("Content-Type", "application/json")], "").await; snapshot!(code, @"400 Bad Request"); @@ -222,8 +215,7 @@ async fn replace_documents_missing_payload() { #[actix_rt::test] async fn update_documents_missing_payload() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_update_documents("", Some("application/json"), "").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -261,8 +253,7 @@ async fn update_documents_missing_payload() { #[actix_rt::test] async fn replace_documents_missing_content_type() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_add_documents("", Vec::new(), "").await; snapshot!(code, @"415 Unsupported Media Type"); snapshot!(json_string!(response), @r###" @@ -290,8 +281,7 @@ async fn replace_documents_missing_content_type() { #[actix_rt::test] async fn update_documents_missing_content_type() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_update_documents("", None, "").await; snapshot!(code, @"415 Unsupported Media Type"); snapshot!(json_string!(response), @r###" @@ -319,8 +309,7 @@ async fn update_documents_missing_content_type() { #[actix_rt::test] async fn replace_documents_bad_content_type() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_add_documents("", vec![("Content-Type", "doggo")], "").await; snapshot!(code, @"415 Unsupported Media Type"); snapshot!(json_string!(response), @r###" @@ -336,8 +325,7 @@ async fn replace_documents_bad_content_type() { #[actix_rt::test] async fn update_documents_bad_content_type() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_update_documents("", Some("doggo"), "").await; snapshot!(code, @"415 Unsupported Media Type"); snapshot!(json_string!(response), @r###" @@ -353,8 +341,7 @@ async fn update_documents_bad_content_type() { #[actix_rt::test] async fn replace_documents_bad_csv_delimiter() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index .raw_add_documents("", vec![("Content-Type", "application/json")], "?csvDelimiter") .await; @@ -402,8 +389,7 @@ async fn replace_documents_bad_csv_delimiter() { #[actix_rt::test] async fn update_documents_bad_csv_delimiter() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_update_documents("", Some("application/json"), "?csvDelimiter").await; snapshot!(code, @"400 Bad Request"); @@ -449,8 +435,7 @@ async fn update_documents_bad_csv_delimiter() { #[actix_rt::test] async fn replace_documents_csv_delimiter_with_bad_content_type() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index .raw_add_documents("", vec![("Content-Type", "application/json")], "?csvDelimiter=a") .await; @@ -481,8 +466,7 @@ async fn replace_documents_csv_delimiter_with_bad_content_type() { #[actix_rt::test] async fn update_documents_csv_delimiter_with_bad_content_type() { let server = Server::new_shared(); - let index = server.index("test"); - + let index = server.unique_index(); let (response, code) = index.raw_update_documents("", Some("application/json"), "?csvDelimiter=a").await; snapshot!(code, @"415 Unsupported Media Type"); @@ -511,9 +495,8 @@ async fn update_documents_csv_delimiter_with_bad_content_type() { #[actix_rt::test] async fn delete_document_by_filter() { let server = Server::new_shared(); - let index = server.index("tests-documents-errors-delete_document_by_filter"); + let index = server.unique_index(); - // send a bad payload type let (response, code) = index.delete_document_by_filter(json!("hello")).await; snapshot!(code, @"400 Bad Request"); snapshot!(response, @r###" @@ -573,15 +556,14 @@ async fn delete_document_by_filter() { } "###); + let index = shared_does_not_exists_index().await; // index does not exists - let (response, code) = - index.delete_document_by_filter(json!({ "filter": "doggo = bernese"})).await; - snapshot!(code, @"202 Accepted"); - let response = server.wait_task(response.uid()).await; + let (response, _code) = + index.delete_document_by_filter_fail(json!({ "filter": "doggo = bernese"})).await; snapshot!(response, @r###" { "uid": "[uid]", - "indexUid": "tests-documents-errors-delete_document_by_filter", + "indexUid": "DOES_NOT_EXISTS", "status": "failed", "type": "documentDeletion", "canceledBy": null, @@ -591,7 +573,7 @@ async fn delete_document_by_filter() { "originalFilter": "\"doggo = bernese\"" }, "error": { - "message": "Index `tests-documents-errors-delete_document_by_filter` not found.", + "message": "Index `DOES_NOT_EXISTS` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" @@ -603,19 +585,14 @@ async fn delete_document_by_filter() { } "###); - let (response, code) = index.create(None).await; - snapshot!(code, @"202 Accepted"); - server.wait_task(response.uid()).await.succeeded(); - // no filterable are set - let (response, code) = - index.delete_document_by_filter(json!({ "filter": "doggo = bernese"})).await; - snapshot!(code, @"202 Accepted"); - let response = server.wait_task(response.uid()).await; + let index = shared_empty_index().await; + let (response, _code) = + index.delete_document_by_filter_fail(json!({ "filter": "doggo = bernese"})).await; snapshot!(response, @r###" { "uid": "[uid]", - "indexUid": "tests-documents-errors-delete_document_by_filter", + "indexUid": "EMPTY_INDEX", "status": "failed", "type": "documentDeletion", "canceledBy": null, @@ -637,19 +614,16 @@ async fn delete_document_by_filter() { } "###); - let (response, code) = index.update_settings_filterable_attributes(json!(["doggo"])).await; - snapshot!(code, @"202 Accepted"); - server.wait_task(response.uid()).await.succeeded(); - // not filterable while there is a filterable attribute + let index = shared_index_with_documents().await; let (response, code) = - index.delete_document_by_filter(json!({ "filter": "catto = jorts"})).await; + index.delete_document_by_filter_fail(json!({ "filter": "catto = jorts"})).await; snapshot!(code, @"202 Accepted"); let response = server.wait_task(response.uid()).await; snapshot!(response, @r###" { "uid": "[uid]", - "indexUid": "tests-documents-errors-delete_document_by_filter", + "indexUid": "SHARED_DOCUMENTS", "status": "failed", "type": "documentDeletion", "canceledBy": null, @@ -659,7 +633,7 @@ async fn delete_document_by_filter() { "originalFilter": "\"catto = jorts\"" }, "error": { - "message": "Attribute `catto` is not filterable. Available filterable attributes are: `doggo`.\n1:6 catto = jorts", + "message": "Attribute `catto` is not filterable. Available filterable attributes are: `id`, `title`.\n1:6 catto = jorts", "code": "invalid_document_filter", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_document_filter" @@ -675,7 +649,7 @@ async fn delete_document_by_filter() { #[actix_rt::test] async fn fetch_document_by_filter() { let server = Server::new_shared(); - let index = server.index("doggo"); + let index = server.unique_index(); index.update_settings_filterable_attributes(json!(["color"])).await; index .add_documents( @@ -772,9 +746,7 @@ async fn fetch_document_by_filter() { #[actix_rt::test] async fn retrieve_vectors() { let server = Server::new_shared(); - let index = server.index("doggo"); - - // GET ALL DOCUMENTS BY QUERY + let index = server.unique_index(); let (response, _code) = index.get_all_documents_raw("?retrieveVectors=tamo").await; snapshot!(response, @r###" { diff --git a/meilisearch/tests/index/errors.rs b/meilisearch/tests/index/errors.rs index bccfd4a90..2b422d685 100644 --- a/meilisearch/tests/index/errors.rs +++ b/meilisearch/tests/index/errors.rs @@ -1,6 +1,6 @@ use meili_snap::*; -use crate::common::Server; +use crate::common::{shared_does_not_exists_index, Server}; use crate::json; #[actix_rt::test] @@ -55,7 +55,7 @@ async fn get_indexes_unknown_field() { async fn create_index_missing_uid() { let server = Server::new_shared(); - let (response, code) = server.create_index(json!({ "primaryKey": "doggo" })).await; + let (response, code) = server.create_index_fail(json!({ "primaryKey": "doggo" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { @@ -71,7 +71,7 @@ async fn create_index_missing_uid() { async fn create_index_bad_uid() { let server = Server::new_shared(); - let (response, code) = server.create_index(json!({ "uid": "the best doggo" })).await; + let (response, code) = server.create_index_fail(json!({ "uid": "the best doggo" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { @@ -82,7 +82,7 @@ async fn create_index_bad_uid() { } "###); - let (response, code) = server.create_index(json!({ "uid": true })).await; + let (response, code) = server.create_index_fail(json!({ "uid": true })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { @@ -99,7 +99,7 @@ async fn create_index_bad_primary_key() { let server = Server::new_shared(); let (response, code) = server - .create_index(json!({ "uid": "doggo", "primaryKey": ["the", "best", "doggo"] })) + .create_index_fail(json!({ "uid": "doggo", "primaryKey": ["the", "best", "doggo"] })) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -116,7 +116,8 @@ async fn create_index_bad_primary_key() { async fn create_index_unknown_field() { let server = Server::new_shared(); - let (response, code) = server.create_index(json!({ "uid": "doggo", "doggo": "bernese" })).await; + let (response, code) = + server.create_index_fail(json!({ "uid": "doggo", "doggo": "bernese" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { @@ -131,9 +132,7 @@ async fn create_index_unknown_field() { #[actix_rt::test] async fn get_index_bad_uid() { let server = Server::new_shared(); - let index = server.index("the good doggo"); - - let (response, code) = index.get().await; + let (response, code) = server.get_index_fail("the good doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { @@ -148,8 +147,7 @@ async fn get_index_bad_uid() { #[actix_rt::test] async fn update_index_bad_primary_key() { let server = Server::new_shared(); - let index = server.index("doggo"); - + let index = server.unique_index(); let (response, code) = index.update_raw(json!({ "primaryKey": ["doggo"] })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -165,8 +163,7 @@ async fn update_index_bad_primary_key() { #[actix_rt::test] async fn update_index_immutable_uid() { let server = Server::new_shared(); - let index = server.index("doggo"); - + let index = server.unique_index(); let (response, code) = index.update_raw(json!({ "uid": "doggo" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -182,8 +179,7 @@ async fn update_index_immutable_uid() { #[actix_rt::test] async fn update_index_immutable_created_at() { let server = Server::new_shared(); - let index = server.index("doggo"); - + let index = server.unique_index(); let (response, code) = index.update_raw(json!({ "createdAt": "doggo" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -199,8 +195,7 @@ async fn update_index_immutable_created_at() { #[actix_rt::test] async fn update_index_immutable_updated_at() { let server = Server::new_shared(); - let index = server.index("doggo"); - + let index = server.unique_index(); let (response, code) = index.update_raw(json!({ "updatedAt": "doggo" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -216,8 +211,7 @@ async fn update_index_immutable_updated_at() { #[actix_rt::test] async fn update_index_unknown_field() { let server = Server::new_shared(); - let index = server.index("doggo"); - + let index = server.unique_index(); let (response, code) = index.update_raw(json!({ "doggo": "bork" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" @@ -233,9 +227,8 @@ async fn update_index_unknown_field() { #[actix_rt::test] async fn update_index_bad_uid() { let server = Server::new_shared(); - let index = server.index("the good doggo"); - - let (response, code) = index.update_raw(json!({ "primaryKey": "doggo" })).await; + let (response, code) = + server.update_raw_index_fail("the good doggo", json!({ "primaryKey": "doggo" })).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { @@ -250,9 +243,7 @@ async fn update_index_bad_uid() { #[actix_rt::test] async fn delete_index_bad_uid() { let server = Server::new_shared(); - let index = server.index("the good doggo"); - - let (response, code) = index.delete().await; + let (response, code) = server.delete_index_fail("the good doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { diff --git a/meilisearch/tests/search/errors.rs b/meilisearch/tests/search/errors.rs index 2dbb80f6e..fee7eef7d 100644 --- a/meilisearch/tests/search/errors.rs +++ b/meilisearch/tests/search/errors.rs @@ -1,15 +1,13 @@ use meili_snap::*; -use crate::common::Server; +use crate::common::{shared_does_not_exists_index, Server}; use crate::json; #[actix_rt::test] async fn search_unexisting_index() { - let server = Server::new_shared(); - let index = server.index("search_unexisting_index"); - + let index = shared_does_not_exists_index().await; let expected_response = json!({ - "message": "Index `search_unexisting_index` not found.", + "message": "Index `DOES_NOT_EXISTS` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" @@ -937,8 +935,7 @@ async fn sort_reserved_attribute() { #[actix_rt::test] async fn sort_unsortable_attribute() { let server = Server::new_shared(); - let index = server.index("sort_unsortable_attribute"); - + let index = server.unique_index(); let (response, _code) = index.update_settings(json!({"sortableAttributes": ["id"]})).await; index.wait_task(response.uid()).await.succeeded(); diff --git a/meilisearch/tests/search/mod.rs b/meilisearch/tests/search/mod.rs index 77be4c13a..54c0b343e 100644 --- a/meilisearch/tests/search/mod.rs +++ b/meilisearch/tests/search/mod.rs @@ -15,247 +15,14 @@ mod restrict_searchable; mod search_queue; use meilisearch::Opt; -use once_cell::sync::Lazy; use tempfile::TempDir; -use uuid::Uuid; -use crate::common::index::Index; -use crate::common::{default_settings, Server, Value}; +use crate::common::{ + default_settings, shared_index_with_documents, shared_index_with_nested_documents, Server, + DOCUMENTS, FRUITS_DOCUMENTS, NESTED_DOCUMENTS, SCORE_DOCUMENTS, VECTOR_DOCUMENTS, +}; use crate::json; -static DOCUMENTS: Lazy = Lazy::new(|| { - json!([ - { - "title": "Shazam!", - "id": "287947", - "_vectors": { "manual": [1, 2, 3]}, - }, - { - "title": "Captain Marvel", - "id": "299537", - "_vectors": { "manual": [1, 2, 54] }, - }, - { - "title": "Escape Room", - "id": "522681", - "_vectors": { "manual": [10, -23, 32] }, - }, - { - "title": "How to Train Your Dragon: The Hidden World", - "id": "166428", - "_vectors": { "manual": [-100, 231, 32] }, - }, - { - "title": "Gläss", - "id": "450465", - "_vectors": { "manual": [-100, 340, 90] }, - } - ]) -}); - -pub async fn shared_index_with_documents() -> &'static Index<'static> { - static INDEX: Lazy> = Lazy::new(|| { - let server = Server::new_shared(); - let uuid = Uuid::new_v4(); - let index = server.index(uuid.to_string()); - index - }); - let index = Lazy::get(&INDEX); - // That means the lazy has never been initialized, we need to create the index and index the documents - if index.is_none() { - let documents = DOCUMENTS.clone(); - let (response, _code) = INDEX.add_documents(documents, None).await; - INDEX.wait_task(response.uid()).await.succeeded(); - let (response, _code) = INDEX - .update_settings( - json!({"filterableAttributes": ["id", "title"], "sortableAttributes": ["id", "title"]}), - ) - .await; - INDEX.wait_task(response.uid()).await.succeeded(); - } - &INDEX -} - -static SCORE_DOCUMENTS: Lazy = Lazy::new(|| { - json!([ - { - "title": "Batman the dark knight returns: Part 1", - "id": "A", - }, - { - "title": "Batman the dark knight returns: Part 2", - "id": "B", - }, - { - "title": "Batman Returns", - "id": "C", - }, - { - "title": "Batman", - "id": "D", - }, - { - "title": "Badman", - "id": "E", - } - ]) -}); - -static NESTED_DOCUMENTS: Lazy = Lazy::new(|| { - json!([ - { - "id": 852, - "father": "jean", - "mother": "michelle", - "doggos": [ - { - "name": "bobby", - "age": 2, - }, - { - "name": "buddy", - "age": 4, - }, - ], - "cattos": "pésti", - "_vectors": { "manual": [1, 2, 3]}, - }, - { - "id": 654, - "father": "pierre", - "mother": "sabine", - "doggos": [ - { - "name": "gros bill", - "age": 8, - }, - ], - "cattos": ["simba", "pestiféré"], - "_vectors": { "manual": [1, 2, 54] }, - }, - { - "id": 750, - "father": "romain", - "mother": "michelle", - "cattos": ["enigma"], - "_vectors": { "manual": [10, 23, 32] }, - }, - { - "id": 951, - "father": "jean-baptiste", - "mother": "sophie", - "doggos": [ - { - "name": "turbo", - "age": 5, - }, - { - "name": "fast", - "age": 6, - }, - ], - "cattos": ["moumoute", "gomez"], - "_vectors": { "manual": [10, 23, 32] }, - }, - ]) -}); - -pub async fn shared_index_with_nested_documents() -> &'static Index<'static> { - static INDEX: Lazy> = Lazy::new(|| { - let server = Server::new_shared(); - let uuid = Uuid::new_v4(); - let index = server.index(uuid.to_string()); - index - }); - let index = Lazy::get(&INDEX); - // That means the lazy has never been initialized, we need to create the index and index the documents - if index.is_none() { - let documents = NESTED_DOCUMENTS.clone(); - let (response, _code) = INDEX.add_documents(documents, None).await; - INDEX.wait_task(response.uid()).await.succeeded(); - let (response, _code) = INDEX - .update_settings( - json!({"filterableAttributes": ["father", "doggos"], "sortableAttributes": ["doggos"]}), - ) - .await; - INDEX.wait_task(response.uid()).await.succeeded(); - } - &INDEX -} - -static FRUITS_DOCUMENTS: Lazy = Lazy::new(|| { - json!([ - { - "name": "Exclusive sale: green apple", - "id": "green-apple-boosted", - "BOOST": true - }, - { - "name": "Pear", - "id": "pear", - }, - { - "name": "Red apple gala", - "id": "red-apple-gala", - }, - { - "name": "Exclusive sale: Red Tomato", - "id": "red-tomatoes-boosted", - "BOOST": true - }, - { - "name": "Exclusive sale: Red delicious apple", - "id": "red-delicious-boosted", - "BOOST": true, - } - ]) -}); - -static VECTOR_DOCUMENTS: Lazy = Lazy::new(|| { - json!([ - { - "id": "A", - "description": "the dog barks at the cat", - "_vectors": { - // dimensions [canine, feline, young] - "animal": [0.9, 0.8, 0.05], - // dimensions [negative/positive, energy] - "sentiment": [-0.1, 0.55] - } - }, - { - "id": "B", - "description": "the kitten scratched the beagle", - "_vectors": { - // dimensions [canine, feline, young] - "animal": [0.8, 0.9, 0.5], - // dimensions [negative/positive, energy] - "sentiment": [-0.2, 0.65] - } - }, - { - "id": "C", - "description": "the dog had to stay alone today", - "_vectors": { - // dimensions [canine, feline, young] - "animal": [0.85, 0.02, 0.1], - // dimensions [negative/positive, energy] - "sentiment": [-1.0, 0.1] - } - }, - { - "id": "D", - "description": "the little boy pets the puppy", - "_vectors": { - // dimensions [canine, feline, young] - "animal": [0.8, 0.09, 0.8], - // dimensions [negative/positive, energy] - "sentiment": [0.8, 0.3] - } - }, - ]) -}); - #[actix_rt::test] async fn simple_placeholder_search() { let index = shared_index_with_documents().await; diff --git a/meilisearch/tests/settings/errors.rs b/meilisearch/tests/settings/errors.rs index c0dc1975a..ed1e0298f 100644 --- a/meilisearch/tests/settings/errors.rs +++ b/meilisearch/tests/settings/errors.rs @@ -6,7 +6,7 @@ use crate::json; #[actix_rt::test] async fn settings_bad_displayed_attributes() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "displayedAttributes": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -34,7 +34,7 @@ async fn settings_bad_displayed_attributes() { #[actix_rt::test] async fn settings_bad_searchable_attributes() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "searchableAttributes": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -62,7 +62,7 @@ async fn settings_bad_searchable_attributes() { #[actix_rt::test] async fn settings_bad_filterable_attributes() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "filterableAttributes": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -90,7 +90,7 @@ async fn settings_bad_filterable_attributes() { #[actix_rt::test] async fn settings_bad_sortable_attributes() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "sortableAttributes": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -118,7 +118,7 @@ async fn settings_bad_sortable_attributes() { #[actix_rt::test] async fn settings_bad_ranking_rules() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "rankingRules": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -146,7 +146,7 @@ async fn settings_bad_ranking_rules() { #[actix_rt::test] async fn settings_bad_stop_words() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "stopWords": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -174,7 +174,7 @@ async fn settings_bad_stop_words() { #[actix_rt::test] async fn settings_bad_synonyms() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "synonyms": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -202,7 +202,7 @@ async fn settings_bad_synonyms() { #[actix_rt::test] async fn settings_bad_distinct_attribute() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "distinctAttribute": ["doggo"] })).await; snapshot!(code, @"400 Bad Request"); @@ -230,7 +230,7 @@ async fn settings_bad_distinct_attribute() { #[actix_rt::test] async fn settings_bad_typo_tolerance() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "typoTolerance": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -285,7 +285,7 @@ async fn settings_bad_typo_tolerance() { #[actix_rt::test] async fn settings_bad_faceting() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "faceting": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -313,7 +313,7 @@ async fn settings_bad_faceting() { #[actix_rt::test] async fn settings_bad_pagination() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "pagination": "doggo" })).await; snapshot!(code, @"400 Bad Request"); @@ -341,7 +341,7 @@ async fn settings_bad_pagination() { #[actix_rt::test] async fn settings_bad_search_cutoff_ms() { let server = Server::new_shared(); - let index = server.index("test"); + let index = server.unique_index(); let (response, code) = index.update_settings(json!({ "searchCutoffMs": "doggo" })).await; snapshot!(code, @"400 Bad Request"); diff --git a/milli/src/search/facet/filter.rs b/milli/src/search/facet/filter.rs index e3a99f356..9ce201aca 100644 --- a/milli/src/search/facet/filter.rs +++ b/milli/src/search/facet/filter.rs @@ -75,25 +75,21 @@ impl<'a> Display for FilterError<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::AttributeNotFilterable { attribute, filterable_fields } => { + write!(f, "Attribute `{attribute}` is not filterable.")?; if filterable_fields.is_empty() { - write!( - f, - "Attribute `{}` is not filterable. This index does not have configured filterable attributes.", - attribute, - ) + write!(f, " This index does not have configured filterable attributes.") } else { - let filterables_list = filterable_fields - .iter() - .map(AsRef::as_ref) - .collect::>() - .join(" "); - - write!( - f, - "Attribute `{}` is not filterable. Available filterable attributes are: `{}`.", - attribute, - filterables_list, - ) + write!(f, " Available filterable attributes are: ")?; + let mut filterables_list = + filterable_fields.iter().map(AsRef::as_ref).collect::>(); + filterables_list.sort_unstable(); + for (idx, filterable) in filterables_list.iter().enumerate() { + write!(f, "`{filterable}`")?; + if idx != filterables_list.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, ".") } } Self::TooDeep => write!(