use meili_snap::{json_string, snapshot}; use super::{DOCUMENTS, FRUITS_DOCUMENTS, NESTED_DOCUMENTS}; use crate::common::Server; use crate::json; use crate::search::{SCORE_DOCUMENTS, VECTOR_DOCUMENTS}; #[actix_rt::test] async fn search_empty_list() { let server = Server::new().await; let (response, code) = server.multi_search(json!({"queries": []})).await; snapshot!(code, @"200 OK"); snapshot!(json_string!(response), @r###" { "results": [] } "###); } #[actix_rt::test] async fn federation_empty_list() { let server = Server::new().await; let (response, code) = server.multi_search(json!({"federation": {}, "queries": []})).await; snapshot!(code, @"200 OK"); snapshot!(json_string!(response, {".processingTimeMs" => "[time]"}), @r###" { "hits": [], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 0 } "###); } #[actix_rt::test] async fn search_json_object() { let server = Server::new().await; let (response, code) = server.multi_search(json!({})).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Missing field `queries`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad_request" } "###); } #[actix_rt::test] async fn federation_no_queries() { let server = Server::new().await; let (response, code) = server.multi_search(json!({"federation": {}})).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Missing field `queries`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad_request" } "###); } #[actix_rt::test] async fn search_json_array() { let server = Server::new().await; let (response, code) = server.multi_search(json!([])).await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Invalid value type: expected an object, but found an array: `[]`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad_request" } "###); } #[actix_rt::test] async fn simple_search_single_index() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid": "test", "q": "glass"}, {"indexUid": "test", "q": "captain"}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response["results"], { "[].processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" [ { "indexUid": "test", "hits": [ { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] } } ], "query": "glass", "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 1 }, { "indexUid": "test", "hits": [ { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] } } ], "query": "captain", "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 1 } ] "###); } #[actix_rt::test] async fn federation_single_search_single_index() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] }, "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 1 } "###); } #[actix_rt::test] async fn federation_multiple_search_single_index() { let server = Server::new().await; let index = server.index("test"); let documents = SCORE_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid": "test", "q": "the bat"}, {"indexUid": "test", "q": "badman returns"}, {"indexUid" : "test", "q": "batman"}, {"indexUid": "test", "q": "batman returns"}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Batman", "id": "D", "_federation": { "indexUid": "test", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "id": "C", "_federation": { "indexUid": "test", "queriesPosition": 3, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 1", "id": "A", "_federation": { "indexUid": "test", "queriesPosition": 2, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 2", "id": "B", "_federation": { "indexUid": "test", "queriesPosition": 2, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Badman", "id": "E", "_federation": { "indexUid": "test", "queriesPosition": 1, "weightedRankingScore": 0.5 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 5 } "###); } #[actix_rt::test] async fn federation_two_search_single_index() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "test", "q": "captain"}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] }, "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "test", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 2 } "###); } #[actix_rt::test] async fn simple_search_missing_index_uid() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"queries": [ {"q": "glass"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, @r###" { "message": "Missing field `indexUid` inside `.queries[0]`", "code": "missing_index_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#missing_index_uid" } "###); } #[actix_rt::test] async fn federation_simple_search_missing_index_uid() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"q": "glass"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, @r###" { "message": "Missing field `indexUid` inside `.queries[0]`", "code": "missing_index_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#missing_index_uid" } "###); } #[actix_rt::test] async fn simple_search_illegal_index_uid() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid": "hé", "q": "glass"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, @r###" { "message": "Invalid value at `.queries[0].indexUid`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_), and can not be more than 512 bytes.", "code": "invalid_index_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_index_uid" } "###); } #[actix_rt::test] async fn federation_search_illegal_index_uid() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid": "hé", "q": "glass"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, @r###" { "message": "Invalid value at `.queries[0].indexUid`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_), and can not be more than 512 bytes.", "code": "invalid_index_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_index_uid" } "###); } #[actix_rt::test] async fn simple_search_two_indexes() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti"}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response["results"], { "[].processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" [ { "indexUid": "test", "hits": [ { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] } } ], "query": "glass", "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 1 }, { "indexUid": "nested", "hits": [ { "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 ] } } ], "query": "pésti", "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 2 } ] "###); } #[actix_rt::test] async fn federation_two_search_two_indexes() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti"}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] }, "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "id": 852, "father": "jean", "mother": "michelle", "doggos": [ { "name": "bobby", "age": 2 }, { "name": "buddy", "age": 4 } ], "cattos": "pésti", "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "id": 654, "father": "pierre", "mother": "sabine", "doggos": [ { "name": "gros bill", "age": 8 } ], "cattos": [ "simba", "pestiféré" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 1, "weightedRankingScore": 0.7803030303030303 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 3 } "###); } #[actix_rt::test] async fn federation_multiple_search_multiple_indexes() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let index = server.index("score"); let documents = SCORE_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(2).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid" : "test", "q": "captain"}, {"indexUid": "nested", "q": "pésti"}, {"indexUid" : "test", "q": "Escape"}, {"indexUid": "nested", "q": "jean"}, {"indexUid": "score", "q": "jean"}, {"indexUid": "test", "q": "the bat"}, {"indexUid": "score", "q": "the bat"}, {"indexUid": "score", "q": "badman returns"}, {"indexUid" : "score", "q": "batman"}, {"indexUid": "score", "q": "batman returns"}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] }, "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "id": 852, "father": "jean", "mother": "michelle", "doggos": [ { "name": "bobby", "age": 2 }, { "name": "buddy", "age": 4 } ], "cattos": "pésti", "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman", "id": "D", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "id": "C", "_federation": { "indexUid": "score", "queriesPosition": 10, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "test", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Escape Room", "id": "522681", "color": [ "yellow", "red" ], "_vectors": { "manual": [ 10, -23, 32 ] }, "_federation": { "indexUid": "test", "queriesPosition": 3, "weightedRankingScore": 0.9848484848484848 } }, { "id": 951, "father": "jean-baptiste", "mother": "sophie", "doggos": [ { "name": "turbo", "age": 5 }, { "name": "fast", "age": 6 } ], "cattos": [ "moumoute", "gomez" ], "_vectors": { "manual": [ 10, 23, 32 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 4, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 1", "id": "A", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 2", "id": "B", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "id": 654, "father": "pierre", "mother": "sabine", "doggos": [ { "name": "gros bill", "age": 8 } ], "cattos": [ "simba", "pestiféré" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 0.7803030303030303 } }, { "title": "Badman", "id": "E", "_federation": { "indexUid": "score", "queriesPosition": 8, "weightedRankingScore": 0.5 } }, { "title": "How to Train Your Dragon: The Hidden World", "id": "166428", "color": [ "green", "red" ], "_vectors": { "manual": [ -100, 231, 32 ] }, "_federation": { "indexUid": "test", "queriesPosition": 6, "weightedRankingScore": 0.4166666666666667 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 12 } "###); } #[actix_rt::test] async fn search_one_index_doesnt_exist() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti"}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[1]`: Index `nested` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" } "###); } #[actix_rt::test] async fn federation_one_index_doesnt_exist() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti"}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[1]`: Index `nested` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" } "###); } #[actix_rt::test] async fn search_multiple_indexes_dont_exist() { let server = Server::new().await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti"}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[0]`: Index `test` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" } "###); } #[actix_rt::test] async fn federation_multiple_indexes_dont_exist() { let server = Server::new().await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti"}, ]})) .await; snapshot!(code, @"400 Bad Request"); // order of indexes that are not found depends on the alphabetical order of index names // the query index is the lowest index with that index snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[1]`: Index `nested` not found.", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" } "###); } #[actix_rt::test] async fn search_one_query_error() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass", "facets": ["title"]}, {"indexUid": "nested", "q": "pésti"}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[0]`: Invalid facet distribution, this index does not have configured filterable attributes.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" } "###); } #[actix_rt::test] async fn federation_one_query_error() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti", "filter": ["title = toto"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[1]`: Index `nested`: Attribute `title` is not filterable. This index does not have configured filterable attributes.\n1:6 title = toto", "code": "invalid_search_filter", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_filter" } "###); } #[actix_rt::test] async fn federation_one_query_sort_error() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti", "sort": ["doggos:desc"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[1]`: Index `nested`: Attribute `doggos` is not sortable. This index does not have configured sortable attributes.", "code": "invalid_search_sort", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_sort" } "###); } #[actix_rt::test] async fn search_multiple_query_errors() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass", "facets": ["title"]}, {"indexUid": "nested", "q": "pésti", "facets": ["doggos"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[0]`: Invalid facet distribution, this index does not have configured filterable attributes.", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_facets" } "###); } #[actix_rt::test] async fn federation_multiple_query_errors() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass", "filter": ["title = toto"]}, {"indexUid": "nested", "q": "pésti", "filter": ["doggos IN [intel, kefir]"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[0]`: Index `test`: Attribute `title` is not filterable. This index does not have configured filterable attributes.\n1:6 title = toto", "code": "invalid_search_filter", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_filter" } "###); } #[actix_rt::test] async fn federation_multiple_query_sort_errors() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass", "sort": ["title:desc"]}, {"indexUid": "nested", "q": "pésti", "sort": ["doggos:desc"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[0]`: Index `test`: Attribute `title` is not sortable. This index does not have configured sortable attributes.", "code": "invalid_search_sort", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_sort" } "###); } #[actix_rt::test] async fn federation_multiple_query_errors_interleaved() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti", "filter": ["doggos IN [intel, kefir]"]}, {"indexUid" : "test", "q": "glass", "filter": ["title = toto"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[1]`: Index `nested`: Attribute `doggos` is not filterable. This index does not have configured filterable attributes.\n1:7 doggos IN [intel, kefir]", "code": "invalid_search_filter", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_filter" } "###); } #[actix_rt::test] async fn federation_multiple_query_sort_errors_interleaved() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "test", "q": "glass"}, {"indexUid": "nested", "q": "pésti", "sort": ["doggos:desc"]}, {"indexUid" : "test", "q": "glass", "sort": ["title:desc"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { "message": "Inside `.queries[1]`: Index `nested`: Attribute `doggos` is not sortable. This index does not have configured sortable attributes.", "code": "invalid_search_sort", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_sort" } "###); } #[actix_rt::test] async fn federation_filter() { let server = Server::new().await; let index = server.index("fruits"); let documents = FRUITS_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings( json!({"searchableAttributes": ["name"], "filterableAttributes": ["BOOST"]}), ) .await; index.wait_task(value.uid()).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red", "filter": "BOOST = true", "showRankingScore": true, "federationOptions": {"weight": 3.0}}, {"indexUid": "fruits", "q": "apple red", "showRankingScore": true}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "name": "Exclusive sale: Red delicious apple", "id": "red-delicious-boosted", "BOOST": true, "_federation": { "indexUid": "fruits", "queriesPosition": 0, "weightedRankingScore": 2.7281746031746033 }, "_rankingScore": 0.9093915343915344 }, { "name": "Exclusive sale: green apple", "id": "green-apple-boosted", "BOOST": true, "_federation": { "indexUid": "fruits", "queriesPosition": 0, "weightedRankingScore": 1.318181818181818 }, "_rankingScore": 0.4393939393939394 }, { "name": "Red apple gala", "id": "red-apple-gala", "_federation": { "indexUid": "fruits", "queriesPosition": 1, "weightedRankingScore": 0.953042328042328 }, "_rankingScore": 0.953042328042328 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 3 } "###); } #[actix_rt::test] async fn federation_sort_same_indexes_same_criterion_same_direction() { let server = Server::new().await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["mother"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // two identical placeholder search should have all results from first query let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "", "sort": ["mother:asc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "", "sort": ["mother:asc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "id": 852, "father": "jean", "mother": "michelle", "doggos": [ { "name": "bobby", "age": 2 }, { "name": "buddy", "age": 4 } ], "cattos": "pésti", "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 750, "father": "romain", "mother": "michelle", "cattos": [ "enigma" ], "_vectors": { "manual": [ 10, 23, 32 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 654, "father": "pierre", "mother": "sabine", "doggos": [ { "name": "gros bill", "age": 8 } ], "cattos": [ "simba", "pestiféré" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 951, "father": "jean-baptiste", "mother": "sophie", "doggos": [ { "name": "turbo", "age": 5 }, { "name": "fast", "age": 6 } ], "cattos": [ "moumoute", "gomez" ], "_vectors": { "manual": [ 10, 23, 32 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 4 } "###); // mix and match query let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "pésti", "sort": ["mother:asc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "jean", "sort": ["mother:asc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "id": 852, "father": "jean", "mother": "michelle", "doggos": [ { "name": "bobby", "age": 2 }, { "name": "buddy", "age": 4 } ], "cattos": "pésti", "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 654, "father": "pierre", "mother": "sabine", "doggos": [ { "name": "gros bill", "age": 8 } ], "cattos": [ "simba", "pestiféré" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 0.7803030303030303 }, "_rankingScore": 0.7803030303030303 }, { "id": 951, "father": "jean-baptiste", "mother": "sophie", "doggos": [ { "name": "turbo", "age": 5 }, { "name": "fast", "age": 6 } ], "cattos": [ "moumoute", "gomez" ], "_vectors": { "manual": [ 10, 23, 32 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 }, "_rankingScore": 0.9848484848484848 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 3 } "###); } #[actix_rt::test] async fn federation_sort_same_indexes_same_criterion_opposite_direction() { let server = Server::new().await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["mother"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // two identical placeholder search should have all results from first query let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "", "sort": ["mother:asc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "", "sort": ["mother:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: The results of queries #0 and #1 are incompatible: \n 1. `queries[0].sort[0]`, `nested.rankingRules[0]`: ascending sort rule(s) on field `mother`\n 2. `queries[1].sort[0]`, `nested.rankingRules[0]`: descending sort rule(s) on field `mother`\n - cannot compare two sort rules in opposite directions\n - note: The ranking rules of query #0 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n - note: The ranking rules of query #1 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); // mix and match query: should be ranked by ranking score let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "pésti", "sort": ["mother:asc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "jean", "sort": ["mother:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: The results of queries #0 and #1 are incompatible: \n 1. `queries[0].sort[0]`, `nested.rankingRules[0]`: ascending sort rule(s) on field `mother`\n 2. `queries[1].sort[0]`, `nested.rankingRules[0]`: descending sort rule(s) on field `mother`\n - cannot compare two sort rules in opposite directions\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); } #[actix_rt::test] async fn federation_sort_same_indexes_different_criterion_same_direction() { let server = Server::new().await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["mother", "father"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // return mothers and fathers ordered accross fields. let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "", "sort": ["mother:asc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "", "sort": ["father:asc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "id": 852, "father": "jean", "mother": "michelle", "doggos": [ { "name": "bobby", "age": 2 }, { "name": "buddy", "age": 4 } ], "cattos": "pésti", "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 951, "father": "jean-baptiste", "mother": "sophie", "doggos": [ { "name": "turbo", "age": 5 }, { "name": "fast", "age": 6 } ], "cattos": [ "moumoute", "gomez" ], "_vectors": { "manual": [ 10, 23, 32 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 750, "father": "romain", "mother": "michelle", "cattos": [ "enigma" ], "_vectors": { "manual": [ 10, 23, 32 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 654, "father": "pierre", "mother": "sabine", "doggos": [ { "name": "gros bill", "age": 8 } ], "cattos": [ "simba", "pestiféré" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 4 } "###); // mix and match query: will be sorted across mother and father names let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "pésti", "sort": ["mother:desc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "jean-bap", "sort": ["father:desc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "jea", "sort": ["father:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "id": 654, "father": "pierre", "mother": "sabine", "doggos": [ { "name": "gros bill", "age": 8 } ], "cattos": [ "simba", "pestiféré" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 0.7803030303030303 }, "_rankingScore": 0.7803030303030303 }, { "id": 852, "father": "jean", "mother": "michelle", "doggos": [ { "name": "bobby", "age": 2 }, { "name": "buddy", "age": 4 } ], "cattos": "pésti", "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "id": 951, "father": "jean-baptiste", "mother": "sophie", "doggos": [ { "name": "turbo", "age": 5 }, { "name": "fast", "age": 6 } ], "cattos": [ "moumoute", "gomez" ], "_vectors": { "manual": [ 10, 23, 32 ] }, "_federation": { "indexUid": "nested", "queriesPosition": 1, "weightedRankingScore": 0.9991181657848324 }, "_rankingScore": 0.9991181657848324 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 3 } "###); } #[actix_rt::test] async fn federation_sort_same_indexes_different_criterion_opposite_direction() { let server = Server::new().await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["mother", "father"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // two identical placeholder search should have all results from first query let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "", "sort": ["mother:asc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "", "sort": ["father:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: The results of queries #0 and #1 are incompatible: \n 1. `queries[0].sort[0]`, `nested.rankingRules[0]`: ascending sort rule(s) on field `mother`\n 2. `queries[1].sort[0]`, `nested.rankingRules[0]`: descending sort rule(s) on field `father`\n - cannot compare two sort rules in opposite directions\n - note: The ranking rules of query #0 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n - note: The ranking rules of query #1 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); // mix and match query: should be ranked by ranking score let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "nested", "q": "pésti", "sort": ["mother:asc"], "showRankingScore": true }, {"indexUid" : "nested", "q": "jean", "sort": ["father:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: The results of queries #0 and #1 are incompatible: \n 1. `queries[0].sort[0]`, `nested.rankingRules[0]`: ascending sort rule(s) on field `mother`\n 2. `queries[1].sort[0]`, `nested.rankingRules[0]`: descending sort rule(s) on field `father`\n - cannot compare two sort rules in opposite directions\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); } #[actix_rt::test] async fn federation_sort_different_indexes_same_criterion_same_direction() { let server = Server::new().await; let index = server.index("movies"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("batman"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // return titles ordered accross indexes let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "", "sort": ["title:asc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Badman", "id": "E", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman", "id": "D", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman Returns", "id": "C", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman the dark knight returns: Part 1", "id": "A", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman the dark knight returns: Part 2", "id": "B", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Escape Room", "id": "522681", "color": [ "yellow", "red" ], "_vectors": { "manual": [ 10, -23, 32 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "How to Train Your Dragon: The Hidden World", "id": "166428", "color": [ "green", "red" ], "_vectors": { "manual": [ -100, 231, 32 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Shazam!", "id": "287947", "color": [ "green", "blue" ], "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 10 } "###); // mix and match query: will be sorted across indexes let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "batman", "q": "badman returns", "sort": ["title:desc"], "showRankingScore": true }, {"indexUid" : "movies", "q": "captain", "sort": ["title:desc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "the bat", "sort": ["title:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 }, "_rankingScore": 0.9848484848484848 }, { "title": "Batman the dark knight returns: Part 2", "id": "B", "_federation": { "indexUid": "batman", "queriesPosition": 2, "weightedRankingScore": 0.9528218694885362 }, "_rankingScore": 0.9528218694885362 }, { "title": "Batman the dark knight returns: Part 1", "id": "A", "_federation": { "indexUid": "batman", "queriesPosition": 2, "weightedRankingScore": 0.9528218694885362 }, "_rankingScore": 0.9528218694885362 }, { "title": "Batman Returns", "id": "C", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.8317901234567902 }, "_rankingScore": 0.8317901234567902 }, { "title": "Batman", "id": "D", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.23106060606060605 }, "_rankingScore": 0.23106060606060605 }, { "title": "Badman", "id": "E", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.5 }, "_rankingScore": 0.5 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 6 } "###); } #[actix_rt::test] async fn federation_sort_different_ranking_rules() { let server = Server::new().await; let index = server.index("movies"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("batman"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "words", "typo", "proximity", "attribute", "sort", "exactness" ] })) .await; index.wait_task(value.uid()).await; // return titles ordered accross indexes let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "", "sort": ["title:asc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Badman", "id": "E", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman", "id": "D", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman Returns", "id": "C", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman the dark knight returns: Part 1", "id": "A", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman the dark knight returns: Part 2", "id": "B", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Escape Room", "id": "522681", "color": [ "yellow", "red" ], "_vectors": { "manual": [ 10, -23, 32 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "How to Train Your Dragon: The Hidden World", "id": "166428", "color": [ "green", "red" ], "_vectors": { "manual": [ -100, 231, 32 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Shazam!", "id": "287947", "color": [ "green", "blue" ], "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 10 } "###); // mix and match query: order difficult to understand let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "batman", "q": "badman returns", "sort": ["title:desc"], "showRankingScore": true }, {"indexUid" : "movies", "q": "captain", "sort": ["title:desc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "the bat", "sort": ["title:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: The results of queries #2 and #1 are incompatible: \n 1. `queries[2]`, `batman.rankingRules[0..=3]`: relevancy rule(s) words, typo, proximity, attribute\n 2. `queries[1].sort[0]`, `movies.rankingRules[0]`: descending sort rule(s) on field `title`\n - cannot compare a relevancy rule with a sort rule\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); } #[actix_rt::test] async fn federation_sort_different_indexes_same_criterion_opposite_direction() { let server = Server::new().await; let index = server.index("movies"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("batman"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // all results from query 0 let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "", "sort": ["title:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[0]`: The results of queries #1 and #0 are incompatible: \n 1. `queries[1].sort[0]`, `batman.rankingRules[0]`: descending sort rule(s) on field `title`\n 2. `queries[0].sort[0]`, `movies.rankingRules[0]`: ascending sort rule(s) on field `title`\n - cannot compare two sort rules in opposite directions\n - note: The ranking rules of query #1 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n - note: The ranking rules of query #0 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); // mix and match query: will be sorted by ranking score let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "batman", "q": "badman returns", "sort": ["title:asc"], "showRankingScore": true }, {"indexUid" : "movies", "q": "captain", "sort": ["title:desc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "the bat", "sort": ["title:asc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: The results of queries #2 and #1 are incompatible: \n 1. `queries[2].sort[0]`, `batman.rankingRules[0]`: ascending sort rule(s) on field `title`\n 2. `queries[1].sort[0]`, `movies.rankingRules[0]`: descending sort rule(s) on field `title`\n - cannot compare two sort rules in opposite directions\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); } #[actix_rt::test] async fn federation_sort_different_indexes_different_criterion_same_direction() { let server = Server::new().await; let index = server.index("movies"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("batman"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["id"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // return titles ordered accross indexes let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "", "sort": ["id:asc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Batman the dark knight returns: Part 1", "id": "A", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman the dark knight returns: Part 2", "id": "B", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman Returns", "id": "C", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Batman", "id": "D", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Badman", "id": "E", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Escape Room", "id": "522681", "color": [ "yellow", "red" ], "_vectors": { "manual": [ 10, -23, 32 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Gläss", "id": "450465", "color": [ "blue", "red" ], "_vectors": { "manual": [ -100, 340, 90 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "How to Train Your Dragon: The Hidden World", "id": "166428", "color": [ "green", "red" ], "_vectors": { "manual": [ -100, 231, 32 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 }, { "title": "Shazam!", "id": "287947", "color": [ "green", "blue" ], "_vectors": { "manual": [ 1, 2, 3 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_rankingScore": 1.0 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 10 } "###); // mix and match query: will be sorted across indexes and criterion let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "batman", "q": "badman returns", "sort": ["id:desc"], "showRankingScore": true }, {"indexUid" : "movies", "q": "captain", "sort": ["title:desc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "the bat", "sort": ["id:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Badman", "id": "E", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.5 }, "_rankingScore": 0.5 }, { "title": "Batman", "id": "D", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.23106060606060605 }, "_rankingScore": 0.23106060606060605 }, { "title": "Captain Marvel", "id": "299537", "color": [ "yellow", "blue" ], "_vectors": { "manual": [ 1, 2, 54 ] }, "_federation": { "indexUid": "movies", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 }, "_rankingScore": 0.9848484848484848 }, { "title": "Batman Returns", "id": "C", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.8317901234567902 }, "_rankingScore": 0.8317901234567902 }, { "title": "Batman the dark knight returns: Part 2", "id": "B", "_federation": { "indexUid": "batman", "queriesPosition": 2, "weightedRankingScore": 0.9528218694885362 }, "_rankingScore": 0.9528218694885362 }, { "title": "Batman the dark knight returns: Part 1", "id": "A", "_federation": { "indexUid": "batman", "queriesPosition": 2, "weightedRankingScore": 0.9528218694885362 }, "_rankingScore": 0.9528218694885362 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 6 } "###); } #[actix_rt::test] async fn federation_sort_different_indexes_different_criterion_opposite_direction() { let server = Server::new().await; let index = server.index("movies"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("batman"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["id"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // all results from query 0 first let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "", "sort": ["id:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[0]`: The results of queries #1 and #0 are incompatible: \n 1. `queries[1].sort[0]`, `batman.rankingRules[0]`: descending sort rule(s) on field `id`\n 2. `queries[0].sort[0]`, `movies.rankingRules[0]`: ascending sort rule(s) on field `title`\n - cannot compare two sort rules in opposite directions\n - note: The ranking rules of query #1 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n - note: The ranking rules of query #0 were modified during canonicalization:\n 1. Removed relevancy rule `words` at position #1 in ranking rules because the query is a placeholder search (`q`: \"\")\n 2. Removed relevancy rule `typo` at position #2 in ranking rules because the query is a placeholder search (`q`: \"\")\n 3. Removed relevancy rule `proximity` at position #3 in ranking rules because the query is a placeholder search (`q`: \"\")\n 4. Removed relevancy rule `attribute` at position #4 in ranking rules because the query is a placeholder search (`q`: \"\")\n 5. Removed relevancy rule `exactness` at position #5 in ranking rules because the query is a placeholder search (`q`: \"\")\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); // mix and match query: more or less by ranking score let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "batman", "q": "badman returns", "sort": ["id:desc"], "showRankingScore": true }, {"indexUid" : "movies", "q": "captain", "sort": ["title:asc"], "showRankingScore": true }, {"indexUid" : "batman", "q": "the bat", "sort": ["id:desc"], "showRankingScore": true }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: The results of queries #2 and #1 are incompatible: \n 1. `queries[2].sort[0]`, `batman.rankingRules[0]`: descending sort rule(s) on field `id`\n 2. `queries[1].sort[0]`, `movies.rankingRules[0]`: ascending sort rule(s) on field `title`\n - cannot compare two sort rules in opposite directions\n", "code": "invalid_multi_search_query_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_ranking_rules" } "###); } #[actix_rt::test] async fn federation_limit_offset() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let index = server.index("score"); let documents = SCORE_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(2).await; { let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "id": 852, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "score", "queriesPosition": 10, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "test", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Escape Room", "_federation": { "indexUid": "test", "queriesPosition": 3, "weightedRankingScore": 0.9848484848484848 } }, { "id": 951, "_federation": { "indexUid": "nested", "queriesPosition": 4, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "id": 654, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 0.7803030303030303 } }, { "title": "Badman", "_federation": { "indexUid": "score", "queriesPosition": 8, "weightedRankingScore": 0.5 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "test", "queriesPosition": 6, "weightedRankingScore": 0.4166666666666667 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 12 } "###); } { let (response, code) = server .multi_search(json!({"federation": {"limit": 1}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 } } ], "processingTimeMs": "[time]", "limit": 1, "offset": 0, "estimatedTotalHits": 12 } "###); } { let (response, code) = server .multi_search(json!({"federation": {"offset": 2}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Batman", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "score", "queriesPosition": 10, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "test", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Escape Room", "_federation": { "indexUid": "test", "queriesPosition": 3, "weightedRankingScore": 0.9848484848484848 } }, { "id": 951, "_federation": { "indexUid": "nested", "queriesPosition": 4, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "id": 654, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 0.7803030303030303 } }, { "title": "Badman", "_federation": { "indexUid": "score", "queriesPosition": 8, "weightedRankingScore": 0.5 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "test", "queriesPosition": 6, "weightedRankingScore": 0.4166666666666667 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 2, "estimatedTotalHits": 12 } "###); } { let (response, code) = server .multi_search(json!({"federation": {"offset": 12}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [], "processingTimeMs": "[time]", "limit": 20, "offset": 12, "estimatedTotalHits": 12 } "###); } } #[actix_rt::test] async fn federation_formatting() { let server = Server::new().await; let index = server.index("test"); let documents = DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(0).await; let index = server.index("nested"); let documents = NESTED_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(1).await; let index = server.index("score"); let documents = SCORE_DOCUMENTS.clone(); index.add_documents(documents, None).await; index.wait_task(2).await; { let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"], "attributesToHighlight": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 }, "_formatted": { "title": "Gläss" } }, { "id": 852, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 1.0 }, "_formatted": { "title": "Batman" } }, { "title": "Batman Returns", "_federation": { "indexUid": "score", "queriesPosition": 10, "weightedRankingScore": 1.0 }, "_formatted": { "title": "Batman Returns" } }, { "title": "Captain Marvel", "_federation": { "indexUid": "test", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 }, "_formatted": { "title": "Captain Marvel" } }, { "title": "Escape Room", "_federation": { "indexUid": "test", "queriesPosition": 3, "weightedRankingScore": 0.9848484848484848 }, "_formatted": { "title": "Escape Room" } }, { "id": 951, "_federation": { "indexUid": "nested", "queriesPosition": 4, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 }, "_formatted": { "title": "Batman the dark knight returns: Part 1" } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 }, "_formatted": { "title": "Batman the dark knight returns: Part 2" } }, { "id": 654, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 0.7803030303030303 } }, { "title": "Badman", "_federation": { "indexUid": "score", "queriesPosition": 8, "weightedRankingScore": 0.5 }, "_formatted": { "title": "Badman" } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "test", "queriesPosition": 6, "weightedRankingScore": 0.4166666666666667 }, "_formatted": { "title": "How to Train Your Dragon: The Hidden World" } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 12 } "###); } { let (response, code) = server .multi_search(json!({"federation": {"limit": 1}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Gläss", "_federation": { "indexUid": "test", "queriesPosition": 0, "weightedRankingScore": 1.0 } } ], "processingTimeMs": "[time]", "limit": 1, "offset": 0, "estimatedTotalHits": 12 } "###); } { let (response, code) = server .multi_search(json!({"federation": {"offset": 2}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "title": "Batman", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "score", "queriesPosition": 10, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "test", "queriesPosition": 1, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Escape Room", "_federation": { "indexUid": "test", "queriesPosition": 3, "weightedRankingScore": 0.9848484848484848 } }, { "id": 951, "_federation": { "indexUid": "nested", "queriesPosition": 4, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "score", "queriesPosition": 9, "weightedRankingScore": 0.9848484848484848 } }, { "id": 654, "_federation": { "indexUid": "nested", "queriesPosition": 2, "weightedRankingScore": 0.7803030303030303 } }, { "title": "Badman", "_federation": { "indexUid": "score", "queriesPosition": 8, "weightedRankingScore": 0.5 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "test", "queriesPosition": 6, "weightedRankingScore": 0.4166666666666667 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 2, "estimatedTotalHits": 12 } "###); } { let (response, code) = server .multi_search(json!({"federation": {"offset": 12}, "queries": [ {"indexUid" : "test", "q": "glass", "attributesToRetrieve": ["title"]}, {"indexUid" : "test", "q": "captain", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "pésti", "attributesToRetrieve": ["id"]}, {"indexUid" : "test", "q": "Escape", "attributesToRetrieve": ["title"]}, {"indexUid": "nested", "q": "jean", "attributesToRetrieve": ["id"]}, {"indexUid": "score", "q": "jean", "attributesToRetrieve": ["title"]}, {"indexUid": "test", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "the bat", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "badman returns", "attributesToRetrieve": ["title"]}, {"indexUid" : "score", "q": "batman", "attributesToRetrieve": ["title"]}, {"indexUid": "score", "q": "batman returns", "attributesToRetrieve": ["title"]}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [], "processingTimeMs": "[time]", "limit": 20, "offset": 12, "estimatedTotalHits": 12 } "###); } } #[actix_rt::test] async fn federation_invalid_weight() { let server = Server::new().await; let index = server.index("fruits"); let documents = FRUITS_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings( json!({"searchableAttributes": ["name"], "filterableAttributes": ["BOOST"]}), ) .await; index.wait_task(value.uid()).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red", "filter": "BOOST = true", "showRankingScore": true, "federationOptions": {"weight": 3.0}}, {"indexUid": "fruits", "q": "apple red", "showRankingScore": true, "federationOptions": {"weight": -12}}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Invalid value at `.queries[1].federationOptions.weight`: the value of `weight` is invalid, expected a positive float (>= 0.0).", "code": "invalid_multi_search_weight", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_weight" } "###); } #[actix_rt::test] async fn federation_null_weight() { let server = Server::new().await; let index = server.index("fruits"); let documents = FRUITS_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings( json!({"searchableAttributes": ["name"], "filterableAttributes": ["BOOST"]}), ) .await; index.wait_task(value.uid()).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red", "filter": "BOOST = true", "showRankingScore": true, "federationOptions": {"weight": 3.0}}, {"indexUid": "fruits", "q": "apple red", "showRankingScore": true, "federationOptions": {"weight": 0.0} }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "name": "Exclusive sale: Red delicious apple", "id": "red-delicious-boosted", "BOOST": true, "_federation": { "indexUid": "fruits", "queriesPosition": 0, "weightedRankingScore": 2.7281746031746033 }, "_rankingScore": 0.9093915343915344 }, { "name": "Exclusive sale: green apple", "id": "green-apple-boosted", "BOOST": true, "_federation": { "indexUid": "fruits", "queriesPosition": 0, "weightedRankingScore": 1.318181818181818 }, "_rankingScore": 0.4393939393939394 }, { "name": "Red apple gala", "id": "red-apple-gala", "_federation": { "indexUid": "fruits", "queriesPosition": 1, "weightedRankingScore": 0.0 }, "_rankingScore": 0.953042328042328 } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 3 } "###); } #[actix_rt::test] async fn federation_federated_contains_pagination() { let server = Server::new().await; let index = server.index("fruits"); let documents = FRUITS_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; // fail when a federated query contains "limit" let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red", "limit": 5}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: Using pagination options is not allowed in federated queries.\n - Hint: remove `limit` from query #1 or remove `federation` from the request\n - Hint: pass `federation.limit` and `federation.offset` for pagination in federated search", "code": "invalid_multi_search_query_pagination", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_pagination" } "###); // fail when a federated query contains "offset" let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red", "offset": 5}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: Using pagination options is not allowed in federated queries.\n - Hint: remove `offset` from query #1 or remove `federation` from the request\n - Hint: pass `federation.limit` and `federation.offset` for pagination in federated search", "code": "invalid_multi_search_query_pagination", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_pagination" } "###); // fail when a federated query contains "page" let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red", "page": 2}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: Using pagination options is not allowed in federated queries.\n - Hint: remove `page` from query #1 or remove `federation` from the request\n - Hint: pass `federation.limit` and `federation.offset` for pagination in federated search", "code": "invalid_multi_search_query_pagination", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_pagination" } "###); // fail when a federated query contains "hitsPerPage" let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red", "hitsPerPage": 5}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: Using pagination options is not allowed in federated queries.\n - Hint: remove `hitsPerPage` from query #1 or remove `federation` from the request\n - Hint: pass `federation.limit` and `federation.offset` for pagination in federated search", "code": "invalid_multi_search_query_pagination", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_pagination" } "###); } #[actix_rt::test] async fn federation_federated_contains_facets() { let server = Server::new().await; let index = server.index("fruits"); let (value, _) = index .update_settings( json!({"searchableAttributes": ["name"], "filterableAttributes": ["BOOST"]}), ) .await; index.wait_task(value.uid()).await; let documents = FRUITS_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; // empty facets are actually OK let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red", "facets": []}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "name": "Red apple gala", "id": "red-apple-gala", "_federation": { "indexUid": "fruits", "queriesPosition": 0, "weightedRankingScore": 0.953042328042328 } }, { "name": "Exclusive sale: Red delicious apple", "id": "red-delicious-boosted", "BOOST": true, "_federation": { "indexUid": "fruits", "queriesPosition": 0, "weightedRankingScore": 0.9093915343915344 } }, { "name": "Exclusive sale: green apple", "id": "green-apple-boosted", "BOOST": true, "_federation": { "indexUid": "fruits", "queriesPosition": 0, "weightedRankingScore": 0.4393939393939394 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 3 } "###); // fails let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red", "facets": ["BOOSTED"]}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: Using facet options is not allowed in federated queries.\n - Hint: remove `facets` from query #1 or remove `federation` from the request\n - Hint: pass `federation.facetsByIndex.fruits: [\"BOOSTED\"]` for facets in federated search", "code": "invalid_multi_search_query_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_query_facets" } "###); } #[actix_rt::test] async fn federation_non_faceted_for_an_index() { let server = Server::new().await; let index = server.index("fruits"); let (value, _) = index .update_settings( json!({"searchableAttributes": ["name"], "filterableAttributes": ["BOOST", "id", "name"]}), ) .await; index.wait_task(value.uid()).await; let index = server.index("fruits-no-name"); let (value, _) = index .update_settings( json!({"searchableAttributes": ["name"], "filterableAttributes": ["BOOST", "id"]}), ) .await; index.wait_task(value.uid()).await; let index = server.index("fruits-no-facets"); let (value, _) = index.update_settings(json!({"searchableAttributes": ["name"]})).await; index.wait_task(value.uid()).await; let documents = FRUITS_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; // fails let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "fruits": ["BOOST", "id", "name"], "fruits-no-name": ["BOOST", "id", "name"], } }, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits-no-name", "q": "apple red"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.federation.facetsByIndex.fruits-no-name`: Invalid facet distribution, attribute `name` is not filterable. The available filterable attributes are `BOOST, id`.\n - Note: index `fruits-no-name` used in `.queries[1]`", "code": "invalid_multi_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets" } "###); // still fails let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "fruits": ["BOOST", "id", "name"], "fruits-no-name": ["BOOST", "id", "name"], } }, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.federation.facetsByIndex.fruits-no-name`: Invalid facet distribution, attribute `name` is not filterable. The available filterable attributes are `BOOST, id`.\n - Note: index `fruits-no-name` is not used in queries", "code": "invalid_multi_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets" } "###); // fails let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "fruits": ["BOOST", "id", "name"], "fruits-no-name": ["BOOST", "id"], "fruits-no-facets": ["BOOST", "id"], } }, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.federation.facetsByIndex.fruits-no-facets`: Invalid facet distribution, this index does not have configured filterable attributes.\n - Note: index `fruits-no-facets` is not used in queries", "code": "invalid_multi_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facets" } "###); // also fails let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "zorglub": ["BOOST", "id", "name"], "fruits": ["BOOST", "id", "name"], } }, "queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red"}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.federation.facetsByIndex.zorglub`: Index `zorglub` not found.\n - Note: index `zorglub` is not used in queries", "code": "index_not_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index_not_found" } "###); } #[actix_rt::test] async fn federation_non_federated_contains_federation_option() { let server = Server::new().await; let index = server.index("fruits"); let documents = FRUITS_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; // fail when a non-federated query contains "federationOptions" let (response, code) = server .multi_search(json!({"queries": [ {"indexUid" : "fruits", "q": "apple red"}, {"indexUid": "fruits", "q": "apple red", "federationOptions": {}}, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.queries[1]`: Using `federationOptions` is not allowed in a non-federated search.\n - Hint: remove `federationOptions` from query #1 or add `federation` to the request.", "code": "invalid_multi_search_federation_options", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_federation_options" } "###); } #[actix_rt::test] async fn federation_vector_single_index() { let server = Server::new().await; let (_, code) = server .set_features(json!({ "vectorStore": true })) .await; snapshot!(code, @"200 OK"); let index = server.index("vectors"); let (value, _) = index .update_settings(json!({"embedders": { "animal": { "source": "userProvided", "dimensions": 3 }, "sentiment": { "source": "userProvided", "dimensions": 2 } }})) .await; index.wait_task(value.uid()).await; let documents = VECTOR_DOCUMENTS.clone(); let (value, code) = index.add_documents(documents, None).await; snapshot!(code, @"202 Accepted"); index.wait_task(value.uid()).await; // same embedder let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "vectors", "vector": [1.0, 0.0, 0.5], "hybrid": {"semanticRatio": 1.0, "embedder": "animal"}}, {"indexUid": "vectors", "vector": [0.5, 0.5, 0.5], "hybrid": {"semanticRatio": 1.0, "embedder": "animal"}}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "id": "B", "description": "the kitten scratched the beagle", "_federation": { "indexUid": "vectors", "queriesPosition": 1, "weightedRankingScore": 0.9870882034301758 } }, { "id": "D", "description": "the little boy pets the puppy", "_federation": { "indexUid": "vectors", "queriesPosition": 0, "weightedRankingScore": 0.9728479385375975 } }, { "id": "C", "description": "the dog had to stay alone today", "_federation": { "indexUid": "vectors", "queriesPosition": 0, "weightedRankingScore": 0.9701486229896544 } }, { "id": "A", "description": "the dog barks at the cat", "_federation": { "indexUid": "vectors", "queriesPosition": 1, "weightedRankingScore": 0.9191691875457764 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 4, "semanticHitCount": 4 } "###); // distinct embedder let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "vectors", "vector": [1.0, 0.0, 0.5], "hybrid": {"semanticRatio": 1.0, "embedder": "animal"}}, // joyful and energetic first {"indexUid": "vectors", "vector": [0.8, 0.6], "hybrid": {"semanticRatio": 1.0, "embedder": "sentiment"}}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "id": "D", "description": "the little boy pets the puppy", "_federation": { "indexUid": "vectors", "queriesPosition": 1, "weightedRankingScore": 0.979868710041046 } }, { "id": "C", "description": "the dog had to stay alone today", "_federation": { "indexUid": "vectors", "queriesPosition": 0, "weightedRankingScore": 0.9701486229896544 } }, { "id": "B", "description": "the kitten scratched the beagle", "_federation": { "indexUid": "vectors", "queriesPosition": 0, "weightedRankingScore": 0.8601469993591309 } }, { "id": "A", "description": "the dog barks at the cat", "_federation": { "indexUid": "vectors", "queriesPosition": 0, "weightedRankingScore": 0.8432406187057495 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 4, "semanticHitCount": 4 } "###); // hybrid search, distinct embedder let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "vectors", "vector": [1.0, 0.0, 0.5], "hybrid": {"semanticRatio": 1.0, "embedder": "animal"}, "showRankingScore": true}, // joyful and energetic first {"indexUid": "vectors", "vector": [0.8, 0.6], "q": "beagle", "hybrid": {"semanticRatio": 1.0, "embedder": "sentiment"},"showRankingScore": true}, {"indexUid": "vectors", "q": "dog", "showRankingScore": true}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "id": "D", "description": "the little boy pets the puppy", "_federation": { "indexUid": "vectors", "queriesPosition": 1, "weightedRankingScore": 0.979868710041046 }, "_rankingScore": "[score]" }, { "id": "C", "description": "the dog had to stay alone today", "_federation": { "indexUid": "vectors", "queriesPosition": 0, "weightedRankingScore": 0.9701486229896544 }, "_rankingScore": "[score]" }, { "id": "A", "description": "the dog barks at the cat", "_federation": { "indexUid": "vectors", "queriesPosition": 2, "weightedRankingScore": 0.9242424242424242 }, "_rankingScore": "[score]" }, { "id": "B", "description": "the kitten scratched the beagle", "_federation": { "indexUid": "vectors", "queriesPosition": 0, "weightedRankingScore": 0.8601469993591309 }, "_rankingScore": "[score]" } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 4, "semanticHitCount": 3 } "###); } #[actix_rt::test] async fn federation_vector_two_indexes() { let server = Server::new().await; let (_, code) = server .set_features(json!({ "vectorStore": true })) .await; snapshot!(code, @"200 OK"); let index = server.index("vectors-animal"); let (value, _) = index .update_settings(json!({"embedders": { "animal": { "source": "userProvided", "dimensions": 3 }, }})) .await; index.wait_task(value.uid()).await; let documents = VECTOR_DOCUMENTS.clone(); let (value, code) = index.add_documents(documents, None).await; snapshot!(code, @"202 Accepted"); index.wait_task(value.uid()).await; let index = server.index("vectors-sentiment"); let (value, _) = index .update_settings(json!({"embedders": { "sentiment": { "source": "userProvided", "dimensions": 2 } }})) .await; index.wait_task(value.uid()).await; let documents = VECTOR_DOCUMENTS.clone(); let (value, code) = index.add_documents(documents, None).await; snapshot!(code, @"202 Accepted"); index.wait_task(value.uid()).await; let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "vectors-animal", "vector": [1.0, 0.0, 0.5], "hybrid": {"semanticRatio": 1.0, "embedder": "animal"}, "retrieveVectors": true}, // joyful and energetic first {"indexUid": "vectors-sentiment", "vector": [0.8, 0.6], "hybrid": {"semanticRatio": 1.0, "embedder": "sentiment"}, "retrieveVectors": true}, {"indexUid": "vectors-sentiment", "q": "dog", "retrieveVectors": true}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "id": "D", "description": "the little boy pets the puppy", "_vectors": { "animal": [ 0.8, 0.09, 0.8 ], "sentiment": { "embeddings": [ [ 0.800000011920929, 0.30000001192092896 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 1, "weightedRankingScore": 0.979868710041046 } }, { "id": "D", "description": "the little boy pets the puppy", "_vectors": { "sentiment": [ 0.8, 0.3 ], "animal": { "embeddings": [ [ 0.800000011920929, 0.09000000357627869, 0.800000011920929 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.9728479385375975 } }, { "id": "C", "description": "the dog had to stay alone today", "_vectors": { "sentiment": [ -1.0, 0.1 ], "animal": { "embeddings": [ [ 0.8500000238418579, 0.019999999552965164, 0.10000000149011612 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.9701486229896544 } }, { "id": "A", "description": "the dog barks at the cat", "_vectors": { "animal": [ 0.9, 0.8, 0.05 ], "sentiment": { "embeddings": [ [ -0.10000000149011612, 0.550000011920929 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 2, "weightedRankingScore": 0.9242424242424242 } }, { "id": "C", "description": "the dog had to stay alone today", "_vectors": { "animal": [ 0.85, 0.02, 0.1 ], "sentiment": { "embeddings": [ [ -1.0, 0.10000000149011612 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 2, "weightedRankingScore": 0.9242424242424242 } }, { "id": "B", "description": "the kitten scratched the beagle", "_vectors": { "sentiment": [ -0.2, 0.65 ], "animal": { "embeddings": [ [ 0.800000011920929, 0.8999999761581421, 0.5 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.8601469993591309 } }, { "id": "A", "description": "the dog barks at the cat", "_vectors": { "sentiment": [ -0.1, 0.55 ], "animal": { "embeddings": [ [ 0.8999999761581421, 0.800000011920929, 0.05000000074505806 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.8432406187057495 } }, { "id": "B", "description": "the kitten scratched the beagle", "_vectors": { "animal": [ 0.8, 0.9, 0.5 ], "sentiment": { "embeddings": [ [ -0.20000000298023224, 0.6499999761581421 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 1, "weightedRankingScore": 0.6690993905067444 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 8, "semanticHitCount": 6 } "###); // hybrid search, distinct embedder let (response, code) = server .multi_search(json!({"federation": {}, "queries": [ {"indexUid" : "vectors-animal", "vector": [1.0, 0.0, 0.5], "hybrid": {"semanticRatio": 1.0, "embedder": "animal"}, "showRankingScore": true, "retrieveVectors": true}, {"indexUid": "vectors-sentiment", "vector": [-1, 0.6], "q": "beagle", "hybrid": {"semanticRatio": 1.0, "embedder": "sentiment"}, "showRankingScore": true, "retrieveVectors": true,}, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]", ".**._rankingScore" => "[score]" }, @r###" { "hits": [ { "id": "D", "description": "the little boy pets the puppy", "_vectors": { "sentiment": [ 0.8, 0.3 ], "animal": { "embeddings": [ [ 0.800000011920929, 0.09000000357627869, 0.800000011920929 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.9728479385375975 }, "_rankingScore": "[score]" }, { "id": "C", "description": "the dog had to stay alone today", "_vectors": { "sentiment": [ -1.0, 0.1 ], "animal": { "embeddings": [ [ 0.8500000238418579, 0.019999999552965164, 0.10000000149011612 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.9701486229896544 }, "_rankingScore": "[score]" }, { "id": "C", "description": "the dog had to stay alone today", "_vectors": { "animal": [ 0.85, 0.02, 0.1 ], "sentiment": { "embeddings": [ [ -1.0, 0.10000000149011612 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 1, "weightedRankingScore": 0.9522157907485962 }, "_rankingScore": "[score]" }, { "id": "B", "description": "the kitten scratched the beagle", "_vectors": { "animal": [ 0.8, 0.9, 0.5 ], "sentiment": { "embeddings": [ [ -0.20000000298023224, 0.6499999761581421 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 1, "weightedRankingScore": 0.8719604015350342 }, "_rankingScore": "[score]" }, { "id": "B", "description": "the kitten scratched the beagle", "_vectors": { "sentiment": [ -0.2, 0.65 ], "animal": { "embeddings": [ [ 0.800000011920929, 0.8999999761581421, 0.5 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.8601469993591309 }, "_rankingScore": "[score]" }, { "id": "A", "description": "the dog barks at the cat", "_vectors": { "sentiment": [ -0.1, 0.55 ], "animal": { "embeddings": [ [ 0.8999999761581421, 0.800000011920929, 0.05000000074505806 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-animal", "queriesPosition": 0, "weightedRankingScore": 0.8432406187057495 }, "_rankingScore": "[score]" }, { "id": "A", "description": "the dog barks at the cat", "_vectors": { "animal": [ 0.9, 0.8, 0.05 ], "sentiment": { "embeddings": [ [ -0.10000000149011612, 0.550000011920929 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 1, "weightedRankingScore": 0.8297949433326721 }, "_rankingScore": "[score]" }, { "id": "D", "description": "the little boy pets the puppy", "_vectors": { "animal": [ 0.8, 0.09, 0.8 ], "sentiment": { "embeddings": [ [ 0.800000011920929, 0.30000001192092896 ] ], "regenerate": false } }, "_federation": { "indexUid": "vectors-sentiment", "queriesPosition": 1, "weightedRankingScore": 0.18887794017791748 }, "_rankingScore": "[score]" } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 8, "semanticHitCount": 8 } "###); } #[actix_rt::test] async fn federation_facets_different_indexes_same_facet() { let server = Server::new().await; let index = server.index("movies"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "filterableAttributes": ["title", "color"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("batman"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "filterableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("batman-2"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "filterableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // return titles ordered accross indexes let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "movies": ["title", "color"], "batman": ["title"], "batman-2": ["title"], } }, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman-2", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Badman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Badman", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Escape Room", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Gläss", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Shazam!", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 15, "facetsByIndex": { "batman": { "distribution": { "title": { "Badman": 1, "Batman": 1, "Batman Returns": 1, "Batman the dark knight returns: Part 1": 1, "Batman the dark knight returns: Part 2": 1 } }, "stats": {} }, "batman-2": { "distribution": { "title": { "Badman": 1, "Batman": 1, "Batman Returns": 1, "Batman the dark knight returns: Part 1": 1, "Batman the dark knight returns: Part 2": 1 } }, "stats": {} }, "movies": { "distribution": { "color": { "blue": 3, "green": 2, "red": 3, "yellow": 2 }, "title": { "Captain Marvel": 1, "Escape Room": 1, "Gläss": 1, "How to Train Your Dragon: The Hidden World": 1, "Shazam!": 1 } }, "stats": {} } } } "###); let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "movies": ["title"], "batman": ["title"], "batman-2": ["title"] }, "mergeFacets": {} }, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman-2", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Badman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Badman", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Escape Room", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Gläss", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Shazam!", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 15, "facetDistribution": { "title": { "Badman": 2, "Batman": 2, "Batman Returns": 2, "Batman the dark knight returns: Part 1": 2, "Batman the dark knight returns: Part 2": 2, "Captain Marvel": 1, "Escape Room": 1, "Gläss": 1, "How to Train Your Dragon: The Hidden World": 1, "Shazam!": 1 } }, "facetStats": {} } "###); // mix and match query: will be sorted across indexes let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "movies": [], "batman": ["title"], "batman-2": ["title"] } }, "queries": [ {"indexUid" : "batman", "q": "badman returns", "sort": ["title:desc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman-2", "q": "badman returns", "sort": ["title:desc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "movies", "q": "captain", "sort": ["title:desc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman", "q": "the bat", "sort": ["title:desc"], "attributesToRetrieve": ["title"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Captain Marvel", "_federation": { "indexUid": "movies", "queriesPosition": 2, "weightedRankingScore": 0.9848484848484848 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman", "queriesPosition": 3, "weightedRankingScore": 0.9528218694885362 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman-2", "queriesPosition": 1, "weightedRankingScore": 0.7028218694885362 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman", "queriesPosition": 3, "weightedRankingScore": 0.9528218694885362 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman-2", "queriesPosition": 1, "weightedRankingScore": 0.7028218694885362 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.8317901234567902 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman-2", "queriesPosition": 1, "weightedRankingScore": 0.8317901234567902 } }, { "title": "Batman", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.23106060606060605 } }, { "title": "Batman", "_federation": { "indexUid": "batman-2", "queriesPosition": 1, "weightedRankingScore": 0.23106060606060605 } }, { "title": "Badman", "_federation": { "indexUid": "batman", "queriesPosition": 0, "weightedRankingScore": 0.5 } }, { "title": "Badman", "_federation": { "indexUid": "batman-2", "queriesPosition": 1, "weightedRankingScore": 0.5 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 11, "facetsByIndex": { "batman": { "distribution": { "title": { "Badman": 1, "Batman": 1, "Batman Returns": 1, "Batman the dark knight returns: Part 1": 1, "Batman the dark knight returns: Part 2": 1 } }, "stats": {} }, "batman-2": { "distribution": { "title": { "Badman": 1, "Batman": 1, "Batman Returns": 1, "Batman the dark knight returns: Part 1": 1, "Batman the dark knight returns: Part 2": 1 } }, "stats": {} }, "movies": { "distribution": {}, "stats": {} } } } "###); } #[actix_rt::test] async fn federation_facets_same_indexes() { let server = Server::new().await; let index = server.index("doggos"); let documents = NESTED_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "filterableAttributes": ["father", "mother", "doggos.age"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("doggos-2"); let documents = NESTED_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "filterableAttributes": ["father", "mother", "doggos.age"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "doggos": ["father", "mother", "doggos.age"] } }, "queries": [ {"indexUid" : "doggos", "q": "je", "attributesToRetrieve": ["id"] }, {"indexUid" : "doggos", "q": "michel", "attributesToRetrieve": ["id"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "id": 852, "_federation": { "indexUid": "doggos", "queriesPosition": 0, "weightedRankingScore": 0.9621212121212122 } }, { "id": 951, "_federation": { "indexUid": "doggos", "queriesPosition": 0, "weightedRankingScore": 0.9621212121212122 } }, { "id": 750, "_federation": { "indexUid": "doggos", "queriesPosition": 1, "weightedRankingScore": 0.9621212121212122 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 3, "facetsByIndex": { "doggos": { "distribution": { "doggos.age": { "2": 1, "4": 1, "5": 1, "6": 1 }, "father": { "jean": 1, "jean-baptiste": 1, "romain": 1 }, "mother": { "michelle": 2, "sophie": 1 } }, "stats": { "doggos.age": { "min": 2.0, "max": 6.0 } } } } } "###); let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "doggos": ["father", "mother", "doggos.age"], "doggos-2": ["father", "mother", "doggos.age"] } }, "queries": [ {"indexUid" : "doggos", "q": "je", "attributesToRetrieve": ["id"] }, {"indexUid" : "doggos-2", "q": "michel", "attributesToRetrieve": ["id"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "id": 852, "_federation": { "indexUid": "doggos", "queriesPosition": 0, "weightedRankingScore": 0.9621212121212122 } }, { "id": 951, "_federation": { "indexUid": "doggos", "queriesPosition": 0, "weightedRankingScore": 0.9621212121212122 } }, { "id": 852, "_federation": { "indexUid": "doggos-2", "queriesPosition": 1, "weightedRankingScore": 0.9621212121212122 } }, { "id": 750, "_federation": { "indexUid": "doggos-2", "queriesPosition": 1, "weightedRankingScore": 0.9621212121212122 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 4, "facetsByIndex": { "doggos": { "distribution": { "doggos.age": { "2": 1, "4": 1, "5": 1, "6": 1 }, "father": { "jean": 1, "jean-baptiste": 1 }, "mother": { "michelle": 1, "sophie": 1 } }, "stats": { "doggos.age": { "min": 2.0, "max": 6.0 } } }, "doggos-2": { "distribution": { "doggos.age": { "2": 1, "4": 1 }, "father": { "jean": 1, "romain": 1 }, "mother": { "michelle": 2 } }, "stats": { "doggos.age": { "min": 2.0, "max": 4.0 } } } } } "###); let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "doggos": ["father", "mother", "doggos.age"], "doggos-2": ["father", "mother", "doggos.age"] }, "mergeFacets": {}, }, "queries": [ {"indexUid" : "doggos", "q": "je", "attributesToRetrieve": ["id"] }, {"indexUid" : "doggos-2", "q": "michel", "attributesToRetrieve": ["id"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "id": 852, "_federation": { "indexUid": "doggos", "queriesPosition": 0, "weightedRankingScore": 0.9621212121212122 } }, { "id": 951, "_federation": { "indexUid": "doggos", "queriesPosition": 0, "weightedRankingScore": 0.9621212121212122 } }, { "id": 852, "_federation": { "indexUid": "doggos-2", "queriesPosition": 1, "weightedRankingScore": 0.9621212121212122 } }, { "id": 750, "_federation": { "indexUid": "doggos-2", "queriesPosition": 1, "weightedRankingScore": 0.9621212121212122 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 4, "facetDistribution": { "doggos.age": { "2": 2, "4": 2, "5": 1, "6": 1 }, "father": { "jean": 2, "jean-baptiste": 1, "romain": 1 }, "mother": { "michelle": 3, "sophie": 1 } }, "facetStats": { "doggos.age": { "min": 2.0, "max": 6.0 } } } "###); } #[actix_rt::test] async fn federation_inconsistent_merge_order() { let server = Server::new().await; let index = server.index("movies"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "filterableAttributes": ["title", "color"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; let index = server.index("movies-2"); let documents = DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "filterableAttributes": ["title", "color"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ], "faceting": { "sortFacetValuesBy": { "color": "count" } } })) .await; index.wait_task(value.uid()).await; let index = server.index("batman"); let documents = SCORE_DOCUMENTS.clone(); let (value, _) = index.add_documents(documents, None).await; index.wait_task(value.uid()).await; let (value, _) = index .update_settings(json!({ "sortableAttributes": ["title"], "filterableAttributes": ["title"], "rankingRules": [ "sort", "words", "typo", "proximity", "attribute", "exactness" ] })) .await; index.wait_task(value.uid()).await; // without merging, it works let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "movies": ["title", "color"], "batman": ["title"], "movies-2": ["title", "color"], } }, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "movies-2", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Badman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Escape Room", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Escape Room", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Gläss", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Gläss", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Shazam!", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Shazam!", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 15, "facetsByIndex": { "batman": { "distribution": { "title": { "Badman": 1, "Batman": 1, "Batman Returns": 1, "Batman the dark knight returns: Part 1": 1, "Batman the dark knight returns: Part 2": 1 } }, "stats": {} }, "movies": { "distribution": { "color": { "blue": 3, "green": 2, "red": 3, "yellow": 2 }, "title": { "Captain Marvel": 1, "Escape Room": 1, "Gläss": 1, "How to Train Your Dragon: The Hidden World": 1, "Shazam!": 1 } }, "stats": {} }, "movies-2": { "distribution": { "color": { "red": 3, "blue": 3, "yellow": 2, "green": 2 }, "title": { "Captain Marvel": 1, "Escape Room": 1, "Gläss": 1, "How to Train Your Dragon: The Hidden World": 1, "Shazam!": 1 } }, "stats": {} } } } "###); // fails with merging let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "movies": ["title", "color"], "batman": ["title"], "movies-2": ["title", "color"], }, "mergeFacets": {} }, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "movies-2", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, ]})) .await; snapshot!(code, @"400 Bad Request"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "message": "Inside `.federation.facetsByIndex.movies-2`: Inconsistent order for values in facet `color`: index `movies` orders alphabetically, but index `movies-2` orders by count.\n - Hint: Remove `federation.mergeFacets` or change `faceting.sortFacetValuesBy` to be consistent in settings.\n - Note: index `movies-2` used in `.queries[2]`", "code": "invalid_multi_search_facet_order", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_multi_search_facet_order" } "###); // can limit the number of values let (response, code) = server .multi_search(json!({"federation": { "facetsByIndex": { "movies": ["title", "color"], "batman": ["title"], "movies-2": ["title"], }, "mergeFacets": { "maxValuesPerFacet": 3, } }, "queries": [ {"indexUid" : "movies", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "batman", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, {"indexUid" : "movies-2", "q": "", "sort": ["title:asc"], "attributesToRetrieve": ["title"] }, ]})) .await; snapshot!(code, @"200 OK"); insta::assert_json_snapshot!(response, { ".processingTimeMs" => "[time]" }, @r###" { "hits": [ { "title": "Badman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman Returns", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 1", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Batman the dark knight returns: Part 2", "_federation": { "indexUid": "batman", "queriesPosition": 1, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Captain Marvel", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Escape Room", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Escape Room", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Gläss", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Gläss", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "How to Train Your Dragon: The Hidden World", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } }, { "title": "Shazam!", "_federation": { "indexUid": "movies", "queriesPosition": 0, "weightedRankingScore": 1.0 } }, { "title": "Shazam!", "_federation": { "indexUid": "movies-2", "queriesPosition": 2, "weightedRankingScore": 1.0 } } ], "processingTimeMs": "[time]", "limit": 20, "offset": 0, "estimatedTotalHits": 15, "facetDistribution": { "color": { "blue": 3, "green": 2, "red": 3 }, "title": { "Badman": 1, "Batman": 1, "Batman Returns": 1 } }, "facetStats": {} } "###); }