mirror of
https://github.com/hanzoai/index.git
synced 2026-10-30 18:34:27 +00:00
1358 lines
38 KiB
Rust
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");
|
|
}
|