Files

1358 lines
38 KiB
Rust

use meili_snap::{json_string, snapshot};
use crate::common::Server;
use crate::json;
#[actix_web::test]
async fn routes_are_disabled_by_default() {
let server = Server::new().await;
let (value, code) = server.list_dynamic_search_rules().await;
snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(value), @r#"
{
"message": "Using the `/dynamic-search-rules` routes requires enabling the `dynamic search rules` experimental feature. See https://github.com/orgs/meilisearch/discussions/884",
"code": "feature_not_enabled",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#feature_not_enabled"
}
"#);
}
async fn dynamic_search_rules_server() -> Server {
let server = Server::new().await;
let (value, code) = server.set_features(json!({ "dynamicSearchRules": true })).await;
assert_eq!(code, 200, "{value}");
assert_eq!(value["dynamicSearchRules"], json!(true));
server
}
async fn create_simple_dynamic_search_rule(server: &Server, uid: &str, active: bool, doc_id: &str) {
let (value, code) = server
.create_dynamic_search_rule(
uid,
json!({
"active": active,
"actions": [
{
"selector": { "id": doc_id },
"action": { "type": "pin", "position": 0 }
}
]
}),
)
.await;
assert_eq!(code, 201, "{value}");
}
#[actix_web::test]
async fn list_empty() {
let server = dynamic_search_rules_server().await;
let (value, code) = server.list_dynamic_search_rules().await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"results": [],
"offset": 0,
"limit": 20,
"total": 0
}
"#);
}
#[actix_web::test]
async fn list_supports_pagination() {
let server = dynamic_search_rules_server().await;
create_simple_dynamic_search_rule(&server, "rule-a", false, "0").await;
create_simple_dynamic_search_rule(&server, "rule-b", true, "1").await;
create_simple_dynamic_search_rule(&server, "rule-c", false, "2").await;
let (value, code) = server
.list_dynamic_search_rules_with(json!({
"offset": 1,
"limit": 1
}))
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"results": [
{
"uid": "rule-b",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "1"
},
"action": {
"type": "pin",
"position": 0
}
}
]
}
],
"offset": 1,
"limit": 1,
"total": 3
}
"#);
}
#[actix_web::test]
async fn list_filters_by_attribute_patterns() {
let server = dynamic_search_rules_server().await;
create_simple_dynamic_search_rule(&server, "promo-active", true, "1").await;
create_simple_dynamic_search_rule(&server, "promo-inactive", false, "2").await;
create_simple_dynamic_search_rule(&server, "standard-active", true, "3").await;
let (value, code) = server
.list_dynamic_search_rules_with(json!({
"filter": {
"attributePatterns": ["promo*"]
}
}))
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"results": [
{
"uid": "promo-active",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "1"
},
"action": {
"type": "pin",
"position": 0
}
}
]
},
{
"uid": "promo-inactive",
"active": false,
"conditions": [],
"actions": [
{
"selector": {
"id": "2"
},
"action": {
"type": "pin",
"position": 0
}
}
]
}
],
"offset": 0,
"limit": 20,
"total": 2
}
"#);
}
#[actix_web::test]
async fn list_filters_by_active_and_combines_filters() {
let server = dynamic_search_rules_server().await;
create_simple_dynamic_search_rule(&server, "promo-active", true, "1").await;
create_simple_dynamic_search_rule(&server, "promo-inactive", false, "2").await;
create_simple_dynamic_search_rule(&server, "standard-active", true, "3").await;
let (value, code) = server
.list_dynamic_search_rules_with(json!({
"filter": {
"active": true
}
}))
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"results": [
{
"uid": "promo-active",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "1"
},
"action": {
"type": "pin",
"position": 0
}
}
]
},
{
"uid": "standard-active",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "3"
},
"action": {
"type": "pin",
"position": 0
}
}
]
}
],
"offset": 0,
"limit": 20,
"total": 2
}
"#);
let (value, code) = server
.list_dynamic_search_rules_with(json!({
"filter": {
"attributePatterns": ["promo*"],
"active": true
}
}))
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"results": [
{
"uid": "promo-active",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "1"
},
"action": {
"type": "pin",
"position": 0
}
}
]
}
],
"offset": 0,
"limit": 20,
"total": 1
}
"#);
}
#[actix_web::test]
async fn create_and_get() {
let server = dynamic_search_rules_server().await;
let (value, code) = server
.create_dynamic_search_rule(
"rule-1",
json!({
"actions": [
{
"selector": { "id": "42" },
"action": { "type": "pin", "position": 1 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
snapshot!(json_string!(value), name: "create_rule_1");
let (value, code) = server.get_dynamic_search_rule("rule-1").await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "get_rule_1");
}
#[actix_web::test]
async fn create_full_rule() {
let server = dynamic_search_rules_server().await;
let (value, code) = server
.create_dynamic_search_rule("black-friday", json!({
"description": "Black Friday 2025 rules",
"priority": 10,
"active": true,
"conditions": [
{ "scope": "query", "isEmpty": true },
{ "scope": "time", "start": "2025-11-28T00:00:00Z", "end": "2025-11-28T23:59:59Z" }
],
"actions": [
{
"selector": { "indexUid": "products", "id": "123" },
"action": { "type": "pin", "position": 1 }
},
{
"selector": { "indexUid": "products", "id": "456" },
"action": { "type": "pin", "position": 0 }
},
{
"selector": { "id": "789" },
"action": { "type": "pin", "position": 3 }
},
{
"selector": { "id": "999" },
"action": { "type": "pin", "position": 8 }
}
]
}))
.await;
snapshot!(code, @"201 Created");
snapshot!(json_string!(value));
let (get_value, code) = server.get_dynamic_search_rule("black-friday").await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(get_value), @r#"
{
"uid": "black-friday",
"description": "Black Friday 2025 rules",
"priority": 10,
"active": true,
"conditions": [
{
"scope": "query",
"isEmpty": true
},
{
"scope": "time",
"start": "2025-11-28T00:00:00Z",
"end": "2025-11-28T23:59:59Z"
}
],
"actions": [
{
"selector": {
"indexUid": "products",
"id": "123"
},
"action": {
"type": "pin",
"position": 1
}
},
{
"selector": {
"indexUid": "products",
"id": "456"
},
"action": {
"type": "pin",
"position": 0
}
},
{
"selector": {
"id": "789"
},
"action": {
"type": "pin",
"position": 3
}
},
{
"selector": {
"id": "999"
},
"action": {
"type": "pin",
"position": 8
}
}
]
}
"#);
}
#[actix_web::test]
async fn create_rejects_query_condition_with_both_is_empty_and_contains() {
let server = dynamic_search_rules_server().await;
let (value, code) = server
.create_dynamic_search_rule(
"invalid-query-condition",
json!({
"conditions": [
{ "scope": "query", "isEmpty": false, "contains": "batman" }
],
"actions": [
{
"selector": { "id": "42" },
"action": { "type": "pin", "position": 0 }
}
]
}),
)
.await;
snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(value), @r#"
{
"message": "Invalid value at `.conditions[0]`: either `isEmpty` or `contains` can be used, not all at once",
"code": "bad_request",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad_request"
}
"#);
}
#[actix_web::test]
async fn full_lifecycle() {
let server = dynamic_search_rules_server().await;
let (_, code) = server
.create_dynamic_search_rule("rule-a", json!({
"actions": [{ "selector": { "id": "0" }, "action": { "type": "pin", "position": 0 } }]
}))
.await;
snapshot!(code, @"201 Created");
let (_, code) = server
.create_dynamic_search_rule("rule-b", json!({
"actions": [{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } }]
}))
.await;
snapshot!(code, @"201 Created");
let (value, code) = server.list_dynamic_search_rules().await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "list_rules");
let (_, code) = server.delete_dynamic_search_rule("rule-a").await;
snapshot!(code, @"204 No Content");
let (value, code) = server.list_dynamic_search_rules().await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "list_rules_after_delete_rule_a");
let (_, code) = server.get_dynamic_search_rule("rule-a").await;
snapshot!(code, @"404 Not Found");
let (_, code) = server.get_dynamic_search_rule("rule-b").await;
snapshot!(code, @"200 OK");
let (_, code) = server.delete_dynamic_search_rule("rule-b").await;
snapshot!(code, @"204 No Content");
let (value, code) = server.list_dynamic_search_rules().await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"results": [],
"offset": 0,
"limit": 20,
"total": 0
}
"#);
}
#[actix_web::test]
async fn patch_rule() {
let server = dynamic_search_rules_server().await;
let (_, code) = server
.create_dynamic_search_rule("updatable", json!({
"actions": [{ "selector": { "id": "42" }, "action": { "type": "pin", "position": 1 } }]
}))
.await;
snapshot!(code, @"201 Created");
let (value, code) = server
.patch_dynamic_search_rule("updatable", json!({ "description": "Updated", "priority": 10 }))
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "priority");
let (value, code) =
server.patch_dynamic_search_rule("updatable", json!({ "active": true })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "active");
let (value, code) = server
.patch_dynamic_search_rule(
"updatable",
json!({ "conditions": [{ "scope": "query", "isEmpty": true }] }),
)
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "conditions");
let (value, code) = server
.patch_dynamic_search_rule(
"updatable",
json!({ "actions": [{ "selector": { "id": "99" }, "action": { "type": "pin", "position": 7 } }] }),
)
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "actions");
}
#[actix_web::test]
async fn get_not_found() {
let server = dynamic_search_rules_server().await;
let (value, code) = server.get_dynamic_search_rule("no-such-rule").await;
snapshot!(code, @"404 Not Found");
snapshot!(json_string!(value), @r#"
{
"message": "Dynamic search rule `no-such-rule` not found.",
"code": "dynamic_search_rule_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#dynamic_search_rule_not_found"
}
"#);
}
#[actix_web::test]
async fn patch_creates_rule_when_missing() {
let server = dynamic_search_rules_server().await;
let (value, code) = server
.patch_dynamic_search_rule(
"foobar",
json!({
"actions": [{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } }]
}),
)
.await;
snapshot!(code, @"201 Created");
snapshot!(json_string!(value), @r#"
{
"uid": "foobar",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "1"
},
"action": {
"type": "pin",
"position": 0
}
}
]
}
"#);
let (value, code) = server.get_dynamic_search_rule("foobar").await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"uid": "foobar",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "1"
},
"action": {
"type": "pin",
"position": 0
}
}
]
}
"#);
}
#[actix_web::test]
async fn delete_not_found() {
let server = dynamic_search_rules_server().await;
let (value, code) = server.delete_dynamic_search_rule("phantom").await;
snapshot!(code, @"404 Not Found");
snapshot!(json_string!(value), @r#"
{
"message": "Dynamic search rule `phantom` not found.",
"code": "dynamic_search_rule_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#dynamic_search_rule_not_found"
}
"#);
}
#[actix_web::test]
async fn create_unknown_field() {
let server = dynamic_search_rules_server().await;
let (value, code) = server
.create_dynamic_search_rule("rule-x", json!({
"actions": [{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } }],
"unknownField": true
}))
.await;
snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(value));
}
#[actix_web::test]
async fn patch_unknown_field() {
let server = dynamic_search_rules_server().await;
let (_, code) = server
.create_dynamic_search_rule("rule-y", json!({
"actions": [{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } }]
}))
.await;
snapshot!(code, @"201 Created");
let (value, code) =
server.patch_dynamic_search_rule("rule-y", json!({ "bogusField": 42 })).await;
snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(value));
}
#[actix_web::test]
async fn create_missing_actions() {
let server = dynamic_search_rules_server().await;
let (value, code) = server.create_dynamic_search_rule("no-actions", json!({})).await;
snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(value));
}
#[actix_web::test]
async fn create_empty_body() {
let server = dynamic_search_rules_server().await;
let (_, code) = server.create_dynamic_search_rule("empty", json!({})).await;
snapshot!(code, @"400 Bad Request");
}
#[actix_web::test]
async fn patch_preserves_fields() {
let server = dynamic_search_rules_server().await;
let (_, code) = server
.create_dynamic_search_rule("preserve", json!({
"description": "original",
"priority": 5,
"active": true,
"conditions": [{ "scope": "query", "isEmpty": true }],
"actions": [{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } }]
}))
.await;
snapshot!(code, @"201 Created");
let (value, code) =
server.patch_dynamic_search_rule("preserve", json!({ "description": "updated" })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value));
}
#[actix_web::test]
async fn patch_replaces_arrays() {
let server = dynamic_search_rules_server().await;
let (_, code) = server
.create_dynamic_search_rule(
"arrays",
json!({
"conditions": [{ "scope": "query", "isEmpty": true }],
"actions": [
{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } },
{ "selector": { "id": "2" }, "action": { "type": "pin", "position": 2 } }
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) =
server.patch_dynamic_search_rule("arrays", json!({ "conditions": [] })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "clear_conditions");
let (value, code) = server
.patch_dynamic_search_rule(
"arrays",
json!({ "actions": [{ "selector": { "id": "3" }, "action": { "type": "pin", "position": 4 } }] }),
)
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), name: "replace_actions");
}
#[actix_web::test]
async fn patch_empty_body() {
let server = dynamic_search_rules_server().await;
let (_, code) = server
.create_dynamic_search_rule("no-change", json!({
"active": true,
"actions": [{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } }]
}))
.await;
snapshot!(code, @"201 Created");
let (value, code) = server.patch_dynamic_search_rule("no-change", json!({})).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value), @r#"
{
"uid": "no-change",
"active": true,
"conditions": [],
"actions": [
{
"selector": {
"id": "1"
},
"action": {
"type": "pin",
"position": 0
}
}
]
}
"#);
}
#[actix_web::test]
async fn defaults_on_create() {
let server = dynamic_search_rules_server().await;
let (value, code) = server
.create_dynamic_search_rule("minimal", json!({
"actions": [{ "selector": { "id": "1" }, "action": { "type": "pin", "position": 0 } }]
}))
.await;
snapshot!(code, @"201 Created");
snapshot!(json_string!(value));
}
#[actix_web::test]
async fn disabling_the_feature_stops_applying_rules_to_search() {
let server = dynamic_search_rules_server().await;
let index = server.index("movies");
let (task, code) = index
.add_documents(
json!([
{ "id": "local", "title": "Batman Returns" },
{ "id": "remote", "title": "Batman" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-remote",
json!({
"active": true,
"actions": [
{
"selector": { "id": "remote" },
"action": { "type": "pin", "position": 0 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) = index.search_post(json!({ "q": "batman returns" })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value["hits"]), name: "results_when_dsr_enabled");
let (value, code) = server.set_features(json!({ "dynamicSearchRules": false })).await;
snapshot!(code, @"200 OK");
snapshot!(value["dynamicSearchRules"], @"false");
let (value, code) = index.search_post(json!({ "q": "batman returns" })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value["hits"]), name: "results_when_dsr_disabled");
}
#[actix_web::test]
async fn search_applies_pins_when_query_contains_value() {
let server = dynamic_search_rules_server().await;
let index = server.index("movies");
let (task, code) = index
.add_documents(
json!([
{ "id": "local", "title": "Batman Returns" },
{ "id": "remote", "title": "Batman" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-when-query-contains-returns",
json!({
"active": true,
"conditions": [
{ "scope": "query", "contains": "returns" }
],
"actions": [
{
"selector": { "id": "remote" },
"action": { "type": "pin", "position": 0 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) = index.search_post(json!({ "q": "Batman Returns" })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value["hits"]), @r#"
[
{
"id": "remote",
"title": "Batman"
},
{
"id": "local",
"title": "Batman Returns"
}
]
"#);
}
#[actix_web::test]
async fn search_filters_out_pinned_documents_excluded_by_filters() {
let server = dynamic_search_rules_server().await;
let index = server.index("products");
let (task, code) = index.update_settings(json!({ "filterableAttributes": ["kind"] })).await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (task, code) = index
.add_documents(
json!([
{ "id": "organic-1", "kind": "keep" },
{ "id": "filtered-pin", "kind": "drop" },
{ "id": "organic-2", "kind": "keep" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-filtered",
json!({
"active": true,
"actions": [
{
"selector": { "id": "filtered-pin" },
"action": { "type": "pin", "position": 0 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) = index.search_post(json!({ "filter": "kind = keep", "limit": 10 })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value["hits"]));
}
#[actix_web::test]
async fn search_keeps_pins_that_miss_query_but_not_filters() {
let server = dynamic_search_rules_server().await;
let index = server.index("movies");
let (task, code) = index.update_settings(json!({ "filterableAttributes": ["kind"] })).await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (task, code) = index
.add_documents(
json!([
{ "id": "organic-match", "title": "Batman Returns", "kind": "keep" },
{ "id": "pinned-query-miss", "title": "The Matrix", "kind": "keep" },
{ "id": "filtered-pin", "title": "Batman Returns", "kind": "drop" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-query-miss-but-filtered",
json!({
"active": true,
"conditions": [
{ "scope": "query", "contains": "returns" }
],
"actions": [
{
"selector": { "id": "pinned-query-miss" },
"action": { "type": "pin", "position": 0 }
},
{
"selector": { "id": "filtered-pin" },
"action": { "type": "pin", "position": 1 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) =
index.search_post(json!({ "q": "Batman Returns", "filter": "kind = keep" })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value["hits"]), @r#"
[
{
"id": "pinned-query-miss",
"title": "The Matrix",
"kind": "keep"
},
{
"id": "organic-match",
"title": "Batman Returns",
"kind": "keep"
}
]
"#);
}
#[actix_web::test]
async fn search_keeps_hybrid_pins_that_miss_query_but_not_filters() {
let server = dynamic_search_rules_server().await;
let index = server.index("movies");
let (task, code) = index
.update_settings(json!({
"filterableAttributes": ["kind"],
"embedders": {
"default": {
"source": "userProvided",
"dimensions": 2
}
}
}))
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (task, code) = index
.add_documents(
json!([
{
"id": "organic-match",
"title": "Batman Returns",
"kind": "keep",
"_vectors": { "default": [1.0, 1.0] }
},
{
"id": "pinned-query-miss",
"title": "The Matrix",
"kind": "keep",
"_vectors": { "default": [-1.0, -1.0] }
},
{
"id": "filtered-pin",
"title": "Batman Returns",
"kind": "drop",
"_vectors": { "default": [1.0, 1.0] }
}
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-query-miss-but-filtered-hybrid",
json!({
"active": true,
"conditions": [
{ "scope": "query", "contains": "returns" }
],
"actions": [
{
"selector": { "id": "pinned-query-miss" },
"action": { "type": "pin", "position": 0 }
},
{
"selector": { "id": "filtered-pin" },
"action": { "type": "pin", "position": 1 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) = index
.search_post(json!({
"q": "Batman Returns",
"filter": "kind = keep",
"vector": [1.0, 1.0],
"hybrid": {
"embedder": "default",
"semanticRatio": 0.5
}
}))
.await;
snapshot!(code, @"200 OK");
snapshot!(
json_string!(value, { ".requestUid" => "[uuid]", ".processingTimeMs" => "[duration]" }),
@r#"
{
"hits": [
{
"id": "pinned-query-miss",
"title": "The Matrix",
"kind": "keep"
},
{
"id": "organic-match",
"title": "Batman Returns",
"kind": "keep"
}
],
"query": "Batman Returns",
"processingTimeMs": "[duration]",
"limit": 20,
"offset": 0,
"estimatedTotalHits": 2,
"requestUid": "[uuid]",
"semanticHitCount": 1
}
"#
);
}
#[actix_web::test]
async fn search_counts_pins_that_miss_query() {
let server = dynamic_search_rules_server().await;
let index = server.index("movies");
let (task, code) = index.update_settings(json!({ "filterableAttributes": ["kind"] })).await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (task, code) = index
.add_documents(
json!([
{ "id": "organic-match", "title": "Batman Returns", "kind": "keep" },
{ "id": "pinned-query-miss", "title": "The Matrix", "kind": "keep" },
{ "id": "filtered-pin", "title": "Batman Returns", "kind": "drop" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-query-miss-but-filtered",
json!({
"active": true,
"conditions": [
{ "scope": "query", "contains": "returns" }
],
"actions": [
{
"selector": { "id": "pinned-query-miss" },
"action": { "type": "pin", "position": 0 }
},
{
"selector": { "id": "filtered-pin" },
"action": { "type": "pin", "position": 1 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) =
index.search_post(json!({ "q": "Batman Returns", "filter": "kind = keep" })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value, { ".requestUid" => "[uuid]", ".processingTimeMs" => "[duration]" }), @r#"
{
"hits": [
{
"id": "pinned-query-miss",
"title": "The Matrix",
"kind": "keep"
},
{
"id": "organic-match",
"title": "Batman Returns",
"kind": "keep"
}
],
"query": "Batman Returns",
"processingTimeMs": "[duration]",
"limit": 20,
"offset": 0,
"estimatedTotalHits": 2,
"requestUid": "[uuid]"
}
"#);
let (value, code) = index
.search_post(json!({
"q": "Batman Returns",
"filter": "kind = keep",
"page": 2,
"hitsPerPage": 1
}))
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value, { ".requestUid" => "[uuid]", ".processingTimeMs" => "[duration]" }), @r#"
{
"hits": [
{
"id": "organic-match",
"title": "Batman Returns",
"kind": "keep"
}
],
"query": "Batman Returns",
"processingTimeMs": "[duration]",
"hitsPerPage": 1,
"page": 2,
"totalPages": 2,
"totalHits": 2,
"requestUid": "[uuid]"
}
"#);
}
#[actix_web::test]
#[ignore = "distinct/pinning semantics to revisit"]
async fn search_distinct_deduplicates_pinned_documents() {
let server = dynamic_search_rules_server().await;
let index = server.index("movies");
let (task, code) = index.update_settings(json!({ "filterableAttributes": ["series"] })).await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (task, code) = index
.add_documents(
json!([
{ "id": "organic-duplicate", "title": "Batman Returns", "series": "batman" },
{ "id": "pinned-duplicate", "title": "The Matrix", "series": "batman" },
{ "id": "organic-unique", "title": "Batman Forever", "series": "forever" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-duplicate-series",
json!({
"active": true,
"actions": [
{
"selector": { "id": "pinned-duplicate" },
"action": { "type": "pin", "position": 0 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) = index.search_post(json!({ "q": "Batman", "distinct": "series" })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value, { ".requestUid" => "[uuid]", ".processingTimeMs" => "[duration]" }), @r#"
{
"hits": [
{
"id": "pinned-duplicate",
"title": "The Matrix",
"series": "batman"
},
{
"id": "organic-unique",
"title": "Batman Forever",
"series": "forever"
}
],
"query": "Batman",
"processingTimeMs": "[duration]",
"limit": 20,
"offset": 0,
"estimatedTotalHits": 2,
"requestUid": "[uuid]"
}
"#);
}
#[actix_web::test]
async fn search_facet_distribution_counts_pins_that_miss_query() {
let server = dynamic_search_rules_server().await;
let index = server.index("movies");
let (task, code) =
index.update_settings(json!({ "filterableAttributes": ["kind", "color"] })).await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (task, code) = index
.add_documents(
json!([
{ "id": "organic-match", "title": "Batman Returns", "kind": "keep", "color": "red" },
{ "id": "pinned-query-miss", "title": "The Matrix", "kind": "keep", "color": "blue" },
{ "id": "filtered-pin", "title": "Batman Returns", "kind": "drop", "color": "green" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pin-query-miss-and-filtered",
json!({
"active": true,
"conditions": [
{ "scope": "query", "contains": "returns" }
],
"actions": [
{
"selector": { "id": "pinned-query-miss" },
"action": { "type": "pin", "position": 0 }
},
{
"selector": { "id": "filtered-pin" },
"action": { "type": "pin", "position": 1 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) = index
.search_post(json!({
"q": "Batman Returns",
"filter": "kind = keep",
"facets": ["color"]
}))
.await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value, { ".requestUid" => "[uuid]", ".processingTimeMs" => "[duration]" }), @r#"
{
"hits": [
{
"id": "pinned-query-miss",
"title": "The Matrix",
"kind": "keep",
"color": "blue"
},
{
"id": "organic-match",
"title": "Batman Returns",
"kind": "keep",
"color": "red"
}
],
"query": "Batman Returns",
"processingTimeMs": "[duration]",
"limit": 20,
"offset": 0,
"estimatedTotalHits": 2,
"facetDistribution": {
"color": {
"blue": 1,
"red": 1
}
},
"facetStats": {},
"requestUid": "[uuid]"
}
"#);
}
#[actix_web::test]
async fn search_pumps_pins_when_organic_results_run_out() {
let server = dynamic_search_rules_server().await;
let index = server.index("products");
let (task, code) = index
.add_documents(
json!([
{ "id": "organic-1" },
{ "id": "late-pin-1" },
{ "id": "organic-2" },
{ "id": "late-pin-2" }
]),
None,
)
.await;
snapshot!(code, @"202 Accepted");
server.wait_task(task.uid()).await.succeeded();
let (_, code) = server
.create_dynamic_search_rule(
"pump-pins",
json!({
"active": true,
"actions": [
{
"selector": { "id": "late-pin-1" },
"action": { "type": "pin", "position": 10 }
},
{
"selector": { "id": "late-pin-2" },
"action": { "type": "pin", "position": 20 }
}
]
}),
)
.await;
snapshot!(code, @"201 Created");
let (value, code) = index.search_post(json!({ "limit": 10 })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value["hits"]), name: "limit_10");
let (value, code) = index.search_post(json!({ "offset": 2, "limit": 2 })).await;
snapshot!(code, @"200 OK");
snapshot!(json_string!(value["hits"]), name: "offset_2_limit_2");
}