mirror of
https://github.com/meilisearch/MeiliSearch
synced 2024-11-26 14:54:27 +01:00
Merge #3897
3897: Add automated tests for `/experimental-features` route r=Kerollmops a=dureuill # Pull Request ## What does this PR do? - Make `RuntimeTogglableFeatures` `Eq` - Add various tests for the `/experimental-features` route - Integration tests for the route itself - Integration tests for the effect of enabling `scoreDetails` and `vectorStore` through this route. - Dump integration tests Co-authored-by: Louis Dureuil <louis@meilisearch.com>
This commit is contained in:
commit
0c8dbf6fa6
@ -210,6 +210,7 @@ pub(crate) mod test {
|
|||||||
use big_s::S;
|
use big_s::S;
|
||||||
use maplit::{btreemap, btreeset};
|
use maplit::{btreemap, btreeset};
|
||||||
use meilisearch_types::facet_values_sort::FacetValuesSort;
|
use meilisearch_types::facet_values_sort::FacetValuesSort;
|
||||||
|
use meilisearch_types::features::RuntimeTogglableFeatures;
|
||||||
use meilisearch_types::index_uid_pattern::IndexUidPattern;
|
use meilisearch_types::index_uid_pattern::IndexUidPattern;
|
||||||
use meilisearch_types::keys::{Action, Key};
|
use meilisearch_types::keys::{Action, Key};
|
||||||
use meilisearch_types::milli;
|
use meilisearch_types::milli;
|
||||||
@ -418,7 +419,10 @@ pub(crate) mod test {
|
|||||||
}
|
}
|
||||||
keys.flush().unwrap();
|
keys.flush().unwrap();
|
||||||
|
|
||||||
// ========== TODO: create features here
|
// ========== experimental features
|
||||||
|
let features = create_test_features();
|
||||||
|
|
||||||
|
dump.create_experimental_features(features).unwrap();
|
||||||
|
|
||||||
// create the dump
|
// create the dump
|
||||||
let mut file = tempfile::tempfile().unwrap();
|
let mut file = tempfile::tempfile().unwrap();
|
||||||
@ -428,6 +432,10 @@ pub(crate) mod test {
|
|||||||
file
|
file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_test_features() -> RuntimeTogglableFeatures {
|
||||||
|
RuntimeTogglableFeatures { vector_store: true, ..Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_creating_and_read_dump() {
|
fn test_creating_and_read_dump() {
|
||||||
let mut file = create_test_dump();
|
let mut file = create_test_dump();
|
||||||
@ -472,5 +480,9 @@ pub(crate) mod test {
|
|||||||
for (key, expected) in dump.keys().unwrap().zip(create_test_api_keys()) {
|
for (key, expected) in dump.keys().unwrap().zip(create_test_api_keys()) {
|
||||||
assert_eq!(key.unwrap(), expected);
|
assert_eq!(key.unwrap(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==== checking the features
|
||||||
|
let expected = create_test_features();
|
||||||
|
assert_eq!(dump.features().unwrap().unwrap(), expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,8 +195,53 @@ pub(crate) mod test {
|
|||||||
use meili_snap::insta;
|
use meili_snap::insta;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::reader::v6::RuntimeTogglableFeatures;
|
||||||
|
|
||||||
// TODO: add `features` to tests
|
#[test]
|
||||||
|
fn import_dump_v6_experimental() {
|
||||||
|
let dump = File::open("tests/assets/v6-with-experimental.dump").unwrap();
|
||||||
|
let mut dump = DumpReader::open(dump).unwrap();
|
||||||
|
|
||||||
|
// top level infos
|
||||||
|
insta::assert_display_snapshot!(dump.date().unwrap(), @"2023-07-06 7:10:27.21958 +00:00:00");
|
||||||
|
insta::assert_debug_snapshot!(dump.instance_uid().unwrap(), @"None");
|
||||||
|
|
||||||
|
// tasks
|
||||||
|
let tasks = dump.tasks().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||||
|
let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip();
|
||||||
|
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"d45cd8571703e58ae53c7bd7ce3f5c22");
|
||||||
|
assert_eq!(update_files.len(), 2);
|
||||||
|
assert!(update_files[0].is_none()); // the dump creation
|
||||||
|
assert!(update_files[1].is_none()); // the processed document addition
|
||||||
|
|
||||||
|
// keys
|
||||||
|
let keys = dump.keys().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||||
|
meili_snap::snapshot_hash!(meili_snap::json_string!(keys), @"13c2da155e9729c2344688cab29af71d");
|
||||||
|
|
||||||
|
// indexes
|
||||||
|
let mut indexes = dump.indexes().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||||
|
// the index are not ordered in any way by default
|
||||||
|
indexes.sort_by_key(|index| index.metadata().uid.to_string());
|
||||||
|
|
||||||
|
let mut test = indexes.pop().unwrap();
|
||||||
|
assert!(indexes.is_empty());
|
||||||
|
|
||||||
|
insta::assert_json_snapshot!(test.metadata(), @r###"
|
||||||
|
{
|
||||||
|
"uid": "test",
|
||||||
|
"primaryKey": "id",
|
||||||
|
"createdAt": "2023-07-06T07:07:41.364694Z",
|
||||||
|
"updatedAt": "2023-07-06T07:07:41.396114Z"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
assert_eq!(test.documents().unwrap().count(), 1);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dump.features().unwrap().unwrap(),
|
||||||
|
RuntimeTogglableFeatures { vector_store: true, ..Default::default() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn import_dump_v5() {
|
fn import_dump_v5() {
|
||||||
@ -274,6 +319,8 @@ pub(crate) mod test {
|
|||||||
let documents = spells.documents().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
let documents = spells.documents().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||||
assert_eq!(documents.len(), 10);
|
assert_eq!(documents.len(), 10);
|
||||||
meili_snap::snapshot_hash!(format!("{:#?}", documents), @"235016433dd04262c7f2da01d1e808ce");
|
meili_snap::snapshot_hash!(format!("{:#?}", documents), @"235016433dd04262c7f2da01d1e808ce");
|
||||||
|
|
||||||
|
assert_eq!(dump.features().unwrap(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -292,6 +292,7 @@ pub(crate) mod test {
|
|||||||
│ ├---- update_files/
|
│ ├---- update_files/
|
||||||
│ │ └---- 1.jsonl
|
│ │ └---- 1.jsonl
|
||||||
│ └---- queue.jsonl
|
│ └---- queue.jsonl
|
||||||
|
├---- experimental-features.json
|
||||||
├---- instance_uid.uuid
|
├---- instance_uid.uuid
|
||||||
├---- keys.jsonl
|
├---- keys.jsonl
|
||||||
└---- metadata.json
|
└---- metadata.json
|
||||||
|
BIN
dump/tests/assets/v6-with-experimental.dump
Normal file
BIN
dump/tests/assets/v6-with-experimental.dump
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase", default)]
|
#[serde(rename_all = "camelCase", default)]
|
||||||
pub struct RuntimeTogglableFeatures {
|
pub struct RuntimeTogglableFeatures {
|
||||||
pub score_details: bool,
|
pub score_details: bool,
|
||||||
|
@ -61,6 +61,8 @@ pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'
|
|||||||
("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"},
|
("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"},
|
||||||
("POST", "/keys") => hashset!{"keys.create", "*"},
|
("POST", "/keys") => hashset!{"keys.create", "*"},
|
||||||
("GET", "/keys") => hashset!{"keys.get", "*"},
|
("GET", "/keys") => hashset!{"keys.get", "*"},
|
||||||
|
("GET", "/experimental-features") => hashset!{"experimental.get", "*"},
|
||||||
|
("PATCH", "/experimental-features") => hashset!{"experimental.update", "*"},
|
||||||
};
|
};
|
||||||
|
|
||||||
authorizations
|
authorizations
|
||||||
|
@ -189,6 +189,14 @@ impl Server {
|
|||||||
let url = format!("/tasks/{}", update_id);
|
let url = format!("/tasks/{}", update_id);
|
||||||
self.service.get(url).await
|
self.service.get(url).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 fn default_settings(dir: impl AsRef<Path>) -> Opt {
|
pub fn default_settings(dir: impl AsRef<Path>) -> Opt {
|
||||||
|
109
meilisearch/tests/features/mod.rs
Normal file
109
meilisearch/tests/features/mod.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::common::Server;
|
||||||
|
|
||||||
|
/// Feature name to test against.
|
||||||
|
/// This will have to be changed by a different one when that feature is stabilized.
|
||||||
|
/// All tests that need to set a feature can make use of this constant.
|
||||||
|
const FEATURE_NAME: &str = "vectorStore";
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn experimental_features() {
|
||||||
|
let server = Server::new().await;
|
||||||
|
|
||||||
|
let (response, code) = server.get_features().await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"scoreDetails": false,
|
||||||
|
"vectorStore": false
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let (response, code) = server.set_features(json!({FEATURE_NAME: true})).await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"scoreDetails": false,
|
||||||
|
"vectorStore": true
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let (response, code) = server.get_features().await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"scoreDetails": false,
|
||||||
|
"vectorStore": true
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// sending null does not change the value
|
||||||
|
let (response, code) = server.set_features(json!({FEATURE_NAME: null})).await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"scoreDetails": false,
|
||||||
|
"vectorStore": true
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// not sending the field does not change the value
|
||||||
|
let (response, code) = server.set_features(json!({})).await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"scoreDetails": false,
|
||||||
|
"vectorStore": true
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn errors() {
|
||||||
|
let server = Server::new().await;
|
||||||
|
|
||||||
|
// Sending a feature not in the list is an error
|
||||||
|
let (response, code) = server.set_features(json!({"NotAFeature": true})).await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"message": "Unknown field `NotAFeature`: expected one of `scoreDetails`, `vectorStore`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// The type must be a bool, not a number
|
||||||
|
let (response, code) = server.set_features(json!({FEATURE_NAME: 42})).await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value type at `.vectorStore`: expected a boolean, but found a positive integer: `42`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// The type must be a bool, not a string
|
||||||
|
let (response, code) = server.set_features(json!({FEATURE_NAME: "true"})).await;
|
||||||
|
|
||||||
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value type at `.vectorStore`: expected a boolean, but found a string: `\"true\"`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
}
|
@ -3,6 +3,7 @@ mod common;
|
|||||||
mod dashboard;
|
mod dashboard;
|
||||||
mod documents;
|
mod documents;
|
||||||
mod dumps;
|
mod dumps;
|
||||||
|
mod features;
|
||||||
mod index;
|
mod index;
|
||||||
mod search;
|
mod search;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
@ -752,3 +752,127 @@ async fn faceting_max_values_per_facet() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn experimental_feature_score_details() {
|
||||||
|
let server = Server::new().await;
|
||||||
|
let index = server.index("test");
|
||||||
|
|
||||||
|
let documents = DOCUMENTS.clone();
|
||||||
|
|
||||||
|
index.add_documents(json!(documents), None).await;
|
||||||
|
index.wait_task(0).await;
|
||||||
|
|
||||||
|
index
|
||||||
|
.search(
|
||||||
|
json!({
|
||||||
|
"q": "train dragon",
|
||||||
|
"showRankingScoreDetails": true,
|
||||||
|
}),
|
||||||
|
|response, code| {
|
||||||
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"message": "Computing score details requires enabling the `score details` experimental feature. See https://github.com/meilisearch/product/discussions/674",
|
||||||
|
"code": "feature_not_enabled",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#feature_not_enabled"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (response, code) = server.set_features(json!({"scoreDetails": true})).await;
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(response["scoreDetails"], @"true");
|
||||||
|
|
||||||
|
index
|
||||||
|
.search(
|
||||||
|
json!({
|
||||||
|
"q": "train dragon",
|
||||||
|
"showRankingScoreDetails": true,
|
||||||
|
}),
|
||||||
|
|response, code| {
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "How to Train Your Dragon: The Hidden World",
|
||||||
|
"id": "166428",
|
||||||
|
"_rankingScoreDetails": {
|
||||||
|
"words": {
|
||||||
|
"order": 0,
|
||||||
|
"matchingWords": 2,
|
||||||
|
"maxMatchingWords": 2,
|
||||||
|
"score": 1.0
|
||||||
|
},
|
||||||
|
"typo": {
|
||||||
|
"order": 1,
|
||||||
|
"typoCount": 0,
|
||||||
|
"maxTypoCount": 2,
|
||||||
|
"score": 1.0
|
||||||
|
},
|
||||||
|
"proximity": {
|
||||||
|
"order": 2,
|
||||||
|
"score": 0.875
|
||||||
|
},
|
||||||
|
"attribute": {
|
||||||
|
"order": 3,
|
||||||
|
"attribute_ranking_order_score": 1.0,
|
||||||
|
"query_word_distance_score": 0.8095238095238095,
|
||||||
|
"score": 0.9365079365079364
|
||||||
|
},
|
||||||
|
"exactness": {
|
||||||
|
"order": 4,
|
||||||
|
"matchType": "noExactMatch",
|
||||||
|
"matchingWords": 2,
|
||||||
|
"maxMatchingWords": 2,
|
||||||
|
"score": 0.3333333333333333
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn experimental_feature_vector_store() {
|
||||||
|
let server = Server::new().await;
|
||||||
|
let index = server.index("test");
|
||||||
|
|
||||||
|
let documents = DOCUMENTS.clone();
|
||||||
|
|
||||||
|
index.add_documents(json!(documents), None).await;
|
||||||
|
index.wait_task(0).await;
|
||||||
|
|
||||||
|
let (response, code) = index
|
||||||
|
.search_post(json!({
|
||||||
|
"vector": [1.0, 2.0, 3.0],
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||||
|
{
|
||||||
|
"message": "Passing `vector` as a query parameter requires enabling the `vector store` experimental feature. See https://github.com/meilisearch/product/discussions/677",
|
||||||
|
"code": "feature_not_enabled",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#feature_not_enabled"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let (response, code) = server.set_features(json!({"vectorStore": true})).await;
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(response["vectorStore"], @"true");
|
||||||
|
|
||||||
|
let (response, code) = index
|
||||||
|
.search_post(json!({
|
||||||
|
"vector": [1.0, 2.0, 3.0],
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
meili_snap::snapshot!(code, @"200 OK");
|
||||||
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @"[]");
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user