mirror of
https://github.com/meilisearch/MeiliSearch
synced 2024-12-29 07:51:39 +01:00
5e0485d8dd
4131: Reduce proximity range from 7 to 3 r=Kerollmops a=ManyTheFish ## Summary This PR aims to reduce the impact of the proximity databases on the indexing time and on the database size by reducing the maximum distance between two words to be indexed in the proximity database. ## Stats ### Impact on database size and indexing time ![Impact on datasets](https://github.com/meilisearch/meilisearch/assets/6482087/28ed3d96-bdde-41c1-bdac-e90c1b1dbb23) ### Impact on search relevancy <details> | dataset_name | host_name | Relevancy rate (Precision) | completion_rate 25.00% | completion_rate 50.00% | completion_rate 75.00% | completion_rate 100.00% | |--------------|------------------|------------------------------------|-----------------|-----------------|-----------------|-----------------| | FBIS | 1_4_0 | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | FBIS | 1_4_0 | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | FBIS | 1_4_0 | percentile-50 | 0.00% | 0.00% | 5.00% | 5.56% | | FBIS | 1_4_0 | percentile-75 | 0.00% | 12.50% | 35.00% | 45.00% | | FBIS | 1_4_0 | percentile-90 | 20.00% | 40.00% | | 100.00% | | FBIS | 1_4_0 | average | 5.78% | 11.16% | 21.90% | 26.29% | | FBIS | reduce_proximity | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | FBIS | reduce_proximity | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | FBIS | reduce_proximity | percentile-50 | 0.00% | 0.00% | 5.00% | 5.56% | | FBIS | reduce_proximity | percentile-75 | 0.00% | 15.00% | 35.00% | 40.00% | | FBIS | reduce_proximity | percentile-90 | 20.00% | 40.00% | 85.00% | 100.00% | | FBIS | reduce_proximity | average | 5.55% | 11.34% | 21.75% | 26.14% | | FR94 | 1_4_0 | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | FR94 | 1_4_0 | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | FR94 | 1_4_0 | percentile-50 | 0.00% | 0.00% | 0.00% | 0.00% | | FR94 | 1_4_0 | percentile-75 | 0.00% | 5.00% | 15.00% | 42.11% | | FR94 | 1_4_0 | percentile-90 | 15.00% | 54.55% | 100.00% | 100.00% | | FR94 | 1_4_0 | average | 5.95% | 12.07% | 18.70% | 25.57% | | FR94 | reduce_proximity | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | FR94 | reduce_proximity | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | FR94 | reduce_proximity | percentile-50 | 0.00% | 0.00% | 0.00% | 0.00% | | FR94 | reduce_proximity | percentile-75 | 0.00% | 5.00% | 15.00% | 42.11% | | FR94 | reduce_proximity | percentile-90 | 15.00% | 54.55% | 100.00% | 100.00% | | FR94 | reduce_proximity | average | 5.79% | 12.00% | 18.70% | 25.53% | | FT | 1_4_0 | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | FT | 1_4_0 | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | FT | 1_4_0 | percentile-50 | 0.00% | 0.00% | 5.00% | 10.00% | | FT | 1_4_0 | percentile-75 | 0.00% | 15.00% | 30.00% | 40.00% | | FT | 1_4_0 | percentile-90 | 20.00% | 50.00% | 65.00% | 100.00% | | FT | 1_4_0 | average | 5.08% | 12.58% | 20.00% | 25.49% | | FT | reduce_proximity | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | FT | reduce_proximity | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | FT | reduce_proximity | percentile-50 | 0.00% | 0.00% | 5.00% | 10.00% | | FT | reduce_proximity | percentile-75 | 0.00% | 15.00% | 30.00% | 40.00% | | FT | reduce_proximity | percentile-90 | 10.00% | 45.00% | 60.00% | 100.00% | | FT | reduce_proximity | average | 5.01% | 12.64% | 20.10% | 25.53% | | LAT | 1_4_0 | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | LAT | 1_4_0 | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | LAT | 1_4_0 | percentile-50 | 0.00% | 0.00% | 5.00% | 5.00% | | LAT | 1_4_0 | percentile-75 | 5.00% | 15.00% | 30.00% | 30.00% | | LAT | 1_4_0 | percentile-90 | 15.00% | 45.00% | 60.00% | 80.00% | | LAT | 1_4_0 | average | 4.80% | 11.80% | 17.88% | 21.62% | | LAT | reduce_proximity | percentile-10 | 0.00% | 0.00% | 0.00% | 0.00% | | LAT | reduce_proximity | percentile-25 | 0.00% | 0.00% | 0.00% | 0.00% | | LAT | reduce_proximity | percentile-50 | 0.00% | 0.00% | 5.00% | 5.00% | | LAT | reduce_proximity | percentile-75 | 0.00% | 11.11% | 25.00% | 35.00% | | LAT | reduce_proximity | percentile-90 | 15.00% | 45.00% | 55.00% | 80.00% | | LAT | reduce_proximity | average | 4.43% | 11.23% | 17.32% | 21.45% | </details> ### Impact on Search time | dataset_name | host_name | 25.00% | 50.00% | 75.00% | 100.00% | Average | |--------------|------------------|------------:|------------:|------------:|------------:|-------------| | FBIS | 1_4_0 | 3.45 | 7.446666667 | 9.773489933 | 9.620300752 | 7.572614338 | | FBIS | reduce_proximity | 2.983333333 | 5.316666667 | 6.911073826 | 7.637218045 | 5.712072968 | | FR94 | 1_4_0 | 2.236666667 | 4.45 | 5.523489933 | 4.560150376 | 4.192576744 | | FR94 | reduce_proximity | 2.09 | 3.991666667 | 4.981543624 | 4.266917293 | 3.832531896 | | FT | 1_4_0 | 5.956666667 | 9.656666667 | 13.86912752 | 10.83270677 | 10.0787919 | | FT | reduce_proximity | 4.51 | 5.981666667 | 7.701342282 | 6.766917293 | 6.23998156 | | LAT | 1_4_0 | 5.856666667 | 9.233333333 | 12.98322148 | 10.78759398 | 9.715203865 | | LAT | reduce_proximity | 6.91 | 6.706666667 | 8.463087248 | 8.265037594 | 7.586197877 | ## Technical approach - Ensure the MAX_DISTANCE constant is used everywhere needed - Reduce the MAX_DISTANCE from 8 to 4 ## Related TBD Co-authored-by: ManyTheFish <many@meilisearch.com>
1164 lines
33 KiB
Rust
1164 lines
33 KiB
Rust
// This modules contains all the test concerning search. Each particular feature of the search
|
|
// should be tested in its own module to isolate tests and keep the tests readable.
|
|
|
|
mod distinct;
|
|
mod errors;
|
|
mod facet_search;
|
|
mod formatted;
|
|
mod geo;
|
|
mod multi;
|
|
mod pagination;
|
|
mod restrict_searchable;
|
|
|
|
use once_cell::sync::Lazy;
|
|
|
|
use crate::common::{Server, Value};
|
|
use crate::json;
|
|
|
|
pub(self) static DOCUMENTS: Lazy<Value> = Lazy::new(|| {
|
|
json!([
|
|
{
|
|
"title": "Shazam!",
|
|
"id": "287947",
|
|
},
|
|
{
|
|
"title": "Captain Marvel",
|
|
"id": "299537",
|
|
},
|
|
{
|
|
"title": "Escape Room",
|
|
"id": "522681",
|
|
},
|
|
{
|
|
"title": "How to Train Your Dragon: The Hidden World",
|
|
"id": "166428",
|
|
},
|
|
{
|
|
"title": "Gläss",
|
|
"id": "450465",
|
|
}
|
|
])
|
|
});
|
|
|
|
pub(self) static NESTED_DOCUMENTS: Lazy<Value> = Lazy::new(|| {
|
|
json!([
|
|
{
|
|
"id": 852,
|
|
"father": "jean",
|
|
"mother": "michelle",
|
|
"doggos": [
|
|
{
|
|
"name": "bobby",
|
|
"age": 2,
|
|
},
|
|
{
|
|
"name": "buddy",
|
|
"age": 4,
|
|
},
|
|
],
|
|
"cattos": "pésti",
|
|
},
|
|
{
|
|
"id": 654,
|
|
"father": "pierre",
|
|
"mother": "sabine",
|
|
"doggos": [
|
|
{
|
|
"name": "gros bill",
|
|
"age": 8,
|
|
},
|
|
],
|
|
"cattos": ["simba", "pestiféré"],
|
|
},
|
|
{
|
|
"id": 750,
|
|
"father": "romain",
|
|
"mother": "michelle",
|
|
"cattos": ["enigma"],
|
|
},
|
|
{
|
|
"id": 951,
|
|
"father": "jean-baptiste",
|
|
"mother": "sophie",
|
|
"doggos": [
|
|
{
|
|
"name": "turbo",
|
|
"age": 5,
|
|
},
|
|
{
|
|
"name": "fast",
|
|
"age": 6,
|
|
},
|
|
],
|
|
"cattos": ["moumoute", "gomez"],
|
|
},
|
|
])
|
|
});
|
|
|
|
#[actix_rt::test]
|
|
async fn simple_placeholder_search() {
|
|
let server = Server::new().await;
|
|
let index = server.index("basic");
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(0).await;
|
|
|
|
index
|
|
.search(json!({}), |response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 5);
|
|
})
|
|
.await;
|
|
|
|
let index = server.index("nested");
|
|
let documents = NESTED_DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(json!({}), |response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 4);
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn simple_search() {
|
|
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;
|
|
|
|
index
|
|
.search(json!({"q": "glass"}), |response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1);
|
|
})
|
|
.await;
|
|
|
|
let index = server.index("nested");
|
|
let documents = NESTED_DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(json!({"q": "pésti"}), |response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 2);
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn phrase_search_with_stop_word() {
|
|
// related to https://github.com/meilisearch/meilisearch/issues/3521
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
let (_, code) = index.update_settings(json!({"stopWords": ["the", "of"]})).await;
|
|
meili_snap::snapshot!(code, @"202 Accepted");
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(json!({"q": "how \"to\" train \"the" }), |response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1);
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[cfg(feature = "default")]
|
|
#[actix_rt::test]
|
|
async fn test_kanji_language_detection() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
let documents = json!([
|
|
{ "id": 0, "title": "The quick (\"brown\") fox can't jump 32.3 feet, right? Brr, it's 29.3°F!" },
|
|
{ "id": 1, "title": "東京のお寿司。" },
|
|
{ "id": 2, "title": "הַשּׁוּעָל הַמָּהִיר (״הַחוּם״) לֹא יָכוֹל לִקְפֹּץ 9.94 מֶטְרִים, נָכוֹן? ברר, 1.5°C- בַּחוּץ!" }
|
|
]);
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(0).await;
|
|
|
|
index
|
|
.search(json!({"q": "東京"}), |response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1);
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[cfg(feature = "default")]
|
|
#[actix_rt::test]
|
|
async fn test_thai_language() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
// We don't need documents, the issue is on the query side only.
|
|
let documents = json!([
|
|
{ "id": 0, "title": "สบู่สมุนไพรดอกดาวเรือง 100 กรัม จำนวน 6 ก้อน" },
|
|
{ "id": 1, "title": "สบู่สมุนไพรชาเขียว 100 กรัม จำนวน 6 ก้อน" },
|
|
{ "id": 2, "title": "สบู่สมุนไพรฝางแดงผสมว่านหางจรเข้ 100 กรัม จำนวน 6 ก้อน" }
|
|
]);
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(0).await;
|
|
|
|
index.update_settings(json!({"rankingRules": ["exactness"]})).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(json!({"q": "สบู"}), |response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_multiple_params() {
|
|
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;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "glass",
|
|
"attributesToCrop": ["title:2"],
|
|
"attributesToHighlight": ["title"],
|
|
"limit": 1,
|
|
"offset": 0,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let index = server.index("nested");
|
|
let documents = NESTED_DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "pésti",
|
|
"attributesToCrop": ["catto:2"],
|
|
"attributesToHighlight": ["catto"],
|
|
"limit": 2,
|
|
"offset": 0,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 2);
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_with_filter_string_notation() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
let (_, code) = index.update_settings(json!({"filterableAttributes": ["title"]})).await;
|
|
meili_snap::snapshot!(code, @"202 Accepted");
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
let (_, code) = index.add_documents(documents, None).await;
|
|
meili_snap::snapshot!(code, @"202 Accepted");
|
|
let res = index.wait_task(1).await;
|
|
meili_snap::snapshot!(res["status"], @r###""succeeded""###);
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"filter": "title = Gläss"
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let index = server.index("nested");
|
|
|
|
let (_, code) =
|
|
index.update_settings(json!({"filterableAttributes": ["cattos", "doggos.age"]})).await;
|
|
meili_snap::snapshot!(code, @"202 Accepted");
|
|
|
|
let documents = NESTED_DOCUMENTS.clone();
|
|
let (_, code) = index.add_documents(documents, None).await;
|
|
meili_snap::snapshot!(code, @"202 Accepted");
|
|
let res = index.wait_task(3).await;
|
|
meili_snap::snapshot!(res["status"], @r###""succeeded""###);
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"filter": "cattos = pésti"
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1);
|
|
assert_eq!(response["hits"][0]["id"], json!(852));
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"filter": "doggos.age > 5"
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 2);
|
|
assert_eq!(response["hits"][0]["id"], json!(654));
|
|
assert_eq!(response["hits"][1]["id"], json!(951));
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_with_filter_array_notation() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({"filterableAttributes": ["title"]})).await;
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
let (response, code) = index
|
|
.search_post(json!({
|
|
"filter": ["title = Gläss"]
|
|
}))
|
|
.await;
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1);
|
|
|
|
let (response, code) = index
|
|
.search_post(json!({
|
|
"filter": [["title = Gläss", "title = \"Shazam!\"", "title = \"Escape Room\""]]
|
|
}))
|
|
.await;
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 3);
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_with_sort_on_numbers() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({"sortableAttributes": ["id"]})).await;
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"sort": ["id:asc"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 5);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let index = server.index("nested");
|
|
|
|
index.update_settings(json!({"sortableAttributes": ["doggos.age"]})).await;
|
|
|
|
let documents = NESTED_DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(3).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"sort": ["doggos.age:asc"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 4);
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_with_sort_on_strings() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({"sortableAttributes": ["title"]})).await;
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"sort": ["title:desc"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 5);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let index = server.index("nested");
|
|
|
|
index.update_settings(json!({"sortableAttributes": ["doggos.name"]})).await;
|
|
|
|
let documents = NESTED_DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(3).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"sort": ["doggos.name:asc"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 4);
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_with_multiple_sort() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({"sortableAttributes": ["id", "title"]})).await;
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
let (response, code) = index
|
|
.search_post(json!({
|
|
"sort": ["id:asc", "title:desc"]
|
|
}))
|
|
.await;
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 5);
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_facet_distribution() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({"filterableAttributes": ["title"]})).await;
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"facets": ["title"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
let dist = response["facetDistribution"].as_object().unwrap();
|
|
assert_eq!(dist.len(), 1);
|
|
assert!(dist.get("title").is_some());
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let index = server.index("nested");
|
|
|
|
index.update_settings(json!({"filterableAttributes": ["father", "doggos.name"]})).await;
|
|
|
|
let documents = NESTED_DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(3).await;
|
|
|
|
// TODO: TAMO: fix the test
|
|
index
|
|
.search(
|
|
json!({
|
|
// "facets": ["father", "doggos.name"]
|
|
"facets": ["father"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
let dist = response["facetDistribution"].as_object().unwrap();
|
|
assert_eq!(dist.len(), 1);
|
|
assert_eq!(
|
|
dist["father"],
|
|
json!({ "jean": 1, "pierre": 1, "romain": 1, "jean-baptiste": 1})
|
|
);
|
|
/*
|
|
assert_eq!(
|
|
dist["doggos.name"],
|
|
json!({ "bobby": 1, "buddy": 1, "gros bill": 1, "turbo": 1, "fast": 1})
|
|
);
|
|
*/
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index.update_settings(json!({"filterableAttributes": ["doggos"]})).await;
|
|
index.wait_task(4).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"facets": ["doggos.name"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
let dist = response["facetDistribution"].as_object().unwrap();
|
|
assert_eq!(dist.len(), 1);
|
|
assert_eq!(
|
|
dist["doggos.name"],
|
|
json!({ "bobby": 1, "buddy": 1, "gros bill": 1, "turbo": 1, "fast": 1})
|
|
);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"facets": ["doggos"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
let dist = response["facetDistribution"].as_object().unwrap();
|
|
assert_eq!(dist.len(), 3);
|
|
assert_eq!(
|
|
dist["doggos.name"],
|
|
json!({ "bobby": 1, "buddy": 1, "gros bill": 1, "turbo": 1, "fast": 1})
|
|
);
|
|
assert_eq!(dist["doggos.age"], json!({ "2": 1, "4": 1, "5": 1, "6": 1, "8": 1}));
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn displayed_attributes() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({ "displayedAttributes": ["title"] })).await;
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
let (response, code) =
|
|
index.search_post(json!({ "attributesToRetrieve": ["title", "id"] })).await;
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert!(response["hits"][0].get("title").is_some());
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn placeholder_search_is_hard_limited() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
let documents: Vec<_> = (0..1200).map(|i| json!({ "id": i, "text": "I am unique!" })).collect();
|
|
index.add_documents(documents.into(), None).await;
|
|
index.wait_task(0).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"limit": 1500,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1000);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"offset": 800,
|
|
"limit": 400,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 200);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index.update_settings(json!({ "pagination": { "maxTotalHits": 10_000 } })).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"limit": 1500,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1200);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"offset": 1000,
|
|
"limit": 400,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 200);
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn search_is_hard_limited() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
let documents: Vec<_> = (0..1200).map(|i| json!({ "id": i, "text": "I am unique!" })).collect();
|
|
index.add_documents(documents.into(), None).await;
|
|
index.wait_task(0).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "unique",
|
|
"limit": 1500,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1000);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "unique",
|
|
"offset": 800,
|
|
"limit": 400,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 200);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index.update_settings(json!({ "pagination": { "maxTotalHits": 10_000 } })).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "unique",
|
|
"limit": 1500,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 1200);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "unique",
|
|
"offset": 1000,
|
|
"limit": 400,
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
assert_eq!(response["hits"].as_array().unwrap().len(), 200);
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn faceting_max_values_per_facet() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({ "filterableAttributes": ["number"] })).await;
|
|
|
|
let documents: Vec<_> = (0..10_000).map(|id| json!({ "id": id, "number": id * 10 })).collect();
|
|
index.add_documents(json!(documents), None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"facets": ["number"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
let numbers = response["facetDistribution"]["number"].as_object().unwrap();
|
|
assert_eq!(numbers.len(), 100);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
index.update_settings(json!({ "faceting": { "maxValuesPerFacet": 10_000 } })).await;
|
|
index.wait_task(2).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"facets": ["number"]
|
|
}),
|
|
|response, code| {
|
|
assert_eq!(code, 200, "{}", response);
|
|
let numbers = &response["facetDistribution"]["number"].as_object().unwrap();
|
|
assert_eq!(numbers.len(), 10_000);
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn experimental_feature_score_details() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
|
|
index.add_documents(json!(documents), None).await;
|
|
index.wait_task(0).await;
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "train dragon",
|
|
"showRankingScoreDetails": true,
|
|
}),
|
|
|response, code| {
|
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
|
{
|
|
"message": "Computing score details requires enabling the `score details` experimental feature. See https://github.com/meilisearch/product/discussions/674",
|
|
"code": "feature_not_enabled",
|
|
"type": "invalid_request",
|
|
"link": "https://docs.meilisearch.com/errors#feature_not_enabled"
|
|
}
|
|
"###);
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let (response, code) = server.set_features(json!({"scoreDetails": true})).await;
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(response["scoreDetails"], @"true");
|
|
|
|
index
|
|
.search(
|
|
json!({
|
|
"q": "train dragon",
|
|
"showRankingScoreDetails": true,
|
|
}),
|
|
|response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"title": "How to Train Your Dragon: The Hidden World",
|
|
"id": "166428",
|
|
"_rankingScoreDetails": {
|
|
"words": {
|
|
"order": 0,
|
|
"matchingWords": 2,
|
|
"maxMatchingWords": 2,
|
|
"score": 1.0
|
|
},
|
|
"typo": {
|
|
"order": 1,
|
|
"typoCount": 0,
|
|
"maxTypoCount": 2,
|
|
"score": 1.0
|
|
},
|
|
"proximity": {
|
|
"order": 2,
|
|
"score": 0.75
|
|
},
|
|
"attribute": {
|
|
"order": 3,
|
|
"attributeRankingOrderScore": 1.0,
|
|
"queryWordDistanceScore": 0.8095238095238095,
|
|
"score": 0.9365079365079364
|
|
},
|
|
"exactness": {
|
|
"order": 4,
|
|
"matchType": "noExactMatch",
|
|
"matchingWords": 2,
|
|
"maxMatchingWords": 2,
|
|
"score": 0.3333333333333333
|
|
}
|
|
}
|
|
}
|
|
]
|
|
"###);
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn experimental_feature_vector_store() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
|
|
index.add_documents(json!(documents), None).await;
|
|
index.wait_task(0).await;
|
|
|
|
let (response, code) = index
|
|
.search_post(json!({
|
|
"vector": [1.0, 2.0, 3.0],
|
|
}))
|
|
.await;
|
|
meili_snap::snapshot!(code, @"400 Bad Request");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
|
{
|
|
"message": "Passing `vector` as a query parameter requires enabling the `vector store` experimental feature. See https://github.com/meilisearch/product/discussions/677",
|
|
"code": "feature_not_enabled",
|
|
"type": "invalid_request",
|
|
"link": "https://docs.meilisearch.com/errors#feature_not_enabled"
|
|
}
|
|
"###);
|
|
|
|
let (response, code) = server.set_features(json!({"vectorStore": true})).await;
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(response["vectorStore"], @"true");
|
|
|
|
let (response, code) = index
|
|
.search_post(json!({
|
|
"vector": [1.0, 2.0, 3.0],
|
|
}))
|
|
.await;
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @"[]");
|
|
}
|
|
|
|
#[cfg(feature = "default")]
|
|
#[actix_rt::test]
|
|
async fn camelcased_words() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
// related to https://github.com/meilisearch/meilisearch/issues/3818
|
|
let documents = json!([
|
|
{ "id": 0, "title": "DeLonghi" },
|
|
{ "id": 1, "title": "delonghi" },
|
|
{ "id": 2, "title": "TestAB" },
|
|
{ "id": 3, "title": "TestAb" },
|
|
{ "id": 4, "title": "testab" },
|
|
]);
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(0).await;
|
|
|
|
index
|
|
.search(json!({"q": "deLonghi"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 0,
|
|
"title": "DeLonghi"
|
|
},
|
|
{
|
|
"id": 1,
|
|
"title": "delonghi"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "dellonghi"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 0,
|
|
"title": "DeLonghi"
|
|
},
|
|
{
|
|
"id": 1,
|
|
"title": "delonghi"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "testa"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 2,
|
|
"title": "TestAB"
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "TestAb"
|
|
},
|
|
{
|
|
"id": 4,
|
|
"title": "testab"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "testab"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 2,
|
|
"title": "TestAB"
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "TestAb"
|
|
},
|
|
{
|
|
"id": 4,
|
|
"title": "testab"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "TestaB"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 2,
|
|
"title": "TestAB"
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "TestAb"
|
|
},
|
|
{
|
|
"id": 4,
|
|
"title": "testab"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "Testab"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 2,
|
|
"title": "TestAB"
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "TestAb"
|
|
},
|
|
{
|
|
"id": 4,
|
|
"title": "testab"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "TestAb"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 2,
|
|
"title": "TestAB"
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "TestAb"
|
|
},
|
|
{
|
|
"id": 4,
|
|
"title": "testab"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
// with Typos
|
|
index
|
|
.search(json!({"q": "dellonghi"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 0,
|
|
"title": "DeLonghi"
|
|
},
|
|
{
|
|
"id": 1,
|
|
"title": "delonghi"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "TetsAB"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 2,
|
|
"title": "TestAB"
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "TestAb"
|
|
},
|
|
{
|
|
"id": 4,
|
|
"title": "testab"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "TetsAB"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"id": 2,
|
|
"title": "TestAB"
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "TestAb"
|
|
},
|
|
{
|
|
"id": 4,
|
|
"title": "testab"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
}
|
|
|
|
#[actix_rt::test]
|
|
async fn simple_search_with_strange_synonyms() {
|
|
let server = Server::new().await;
|
|
let index = server.index("test");
|
|
|
|
index.update_settings(json!({ "synonyms": {"&": ["to"], "to": ["&"]} })).await;
|
|
let r = index.wait_task(0).await;
|
|
meili_snap::snapshot!(r["status"], @r###""succeeded""###);
|
|
|
|
let documents = DOCUMENTS.clone();
|
|
index.add_documents(documents, None).await;
|
|
index.wait_task(1).await;
|
|
|
|
index
|
|
.search(json!({"q": "How to train"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"title": "How to Train Your Dragon: The Hidden World",
|
|
"id": "166428"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "How & train"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"title": "How to Train Your Dragon: The Hidden World",
|
|
"id": "166428"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
|
|
index
|
|
.search(json!({"q": "to"}), |response, code| {
|
|
meili_snap::snapshot!(code, @"200 OK");
|
|
meili_snap::snapshot!(meili_snap::json_string!(response["hits"]), @r###"
|
|
[
|
|
{
|
|
"title": "How to Train Your Dragon: The Hidden World",
|
|
"id": "166428"
|
|
}
|
|
]
|
|
"###);
|
|
})
|
|
.await;
|
|
}
|