mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-12-04 11:45:44 +00:00
Compare commits
3 Commits
v1.0.0-rc.
...
with_rate_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6678491212 | ||
|
|
a82f8aacde | ||
|
|
5cf71c6014 |
@@ -44,5 +44,5 @@ jobs:
|
||||
--title "Update version for the next release ($NEW_VERSION) in Cargo.toml files" \
|
||||
--body '⚠️ This PR is automatically generated. Check the new version is the expected one before merging.' \
|
||||
--label 'skip changelog' \
|
||||
--milestone $NEW_VERSION \
|
||||
--milestone $NEW_VERSION
|
||||
--base $GITHUB_REF_NAME
|
||||
|
||||
1160
Cargo.lock
generated
1160
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
86
config.toml
86
config.toml
@@ -28,9 +28,17 @@ http_payload_size_limit = "100 MB"
|
||||
|
||||
log_level = "INFO"
|
||||
# Defines how much detail should be present in Meilisearch's logs.
|
||||
# Meilisearch currently supports six log levels, listed in order of increasing verbosity: `OFF`, `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`
|
||||
# Meilisearch currently supports five log levels, listed in order of increasing verbosity: `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE`
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#log-level
|
||||
|
||||
max_index_size = "100 GiB"
|
||||
# Sets the maximum size of the index.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#max-index-size
|
||||
|
||||
max_task_db_size = "100 GiB"
|
||||
# Sets the maximum size of the task database.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#max-task-db-size
|
||||
|
||||
# max_indexing_memory = "2 GiB"
|
||||
# Sets the maximum amount of RAM Meilisearch can use when indexing.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#max-indexing-memory
|
||||
@@ -39,6 +47,11 @@ log_level = "INFO"
|
||||
# Sets the maximum number of threads Meilisearch can use during indexing.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#max-indexing-threads
|
||||
|
||||
disable_auto_batching = false
|
||||
# Deactivates auto-batching when provided.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#disable-auto-batching
|
||||
|
||||
|
||||
#############
|
||||
### DUMPS ###
|
||||
#############
|
||||
@@ -60,20 +73,85 @@ ignore_dump_if_db_exists = false
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#ignore-dump-if-db-exists
|
||||
|
||||
|
||||
#####################
|
||||
### RATE LIMITING ###
|
||||
#####################
|
||||
|
||||
rate_limiting_disable_all = false
|
||||
# Prevents a Meilisearch instance from performing any rate limiting.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-all
|
||||
|
||||
rate_limiting_disable_global = false
|
||||
# Prevents a Meilisearch instance from performing rate limiting global to all queries.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-global
|
||||
|
||||
rate_limiting_global_pool = 100000
|
||||
# The maximum pool of search requests that can be performed before they are rejected.
|
||||
#
|
||||
# The pool starts full at the provided value, then each search request diminishes the pool by 1.
|
||||
# When the pool is empty the search request is rejected.
|
||||
# The pool is replenished by 1 depending on the cooldown period.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-global-pool
|
||||
|
||||
rate_limiting_global_cooldown_ns = 50000
|
||||
# The amount of time, in nanoseconds, before the pool of available search requests is replenished by 1 again.
|
||||
#
|
||||
# The maximum number of available search requests is given by `rate_limiting_global_pool`.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-global-cooldown-ns
|
||||
|
||||
rate_limiting_disable_ip = false
|
||||
# Prevents a Meilisearch instance from performing rate limiting per IP address.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-ip
|
||||
|
||||
rate_limiting_ip_pool = 200
|
||||
# The maximum pool of search requests that can be performed from a specific IP before they are rejected.
|
||||
#
|
||||
# The pool starts full at the provided value, then each search request from the same IP address diminishes the pool by 1.
|
||||
# When the pool is empty the search request is rejected.
|
||||
# The pool is replenished by 1 depending on the cooldown period.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-ip-pool
|
||||
|
||||
rate_limiting_ip_cooldown_ns = 50000000
|
||||
# The amount of time, in nanoseconds, before the pool of available search requests for a specific IP address is replenished by 1 again.
|
||||
#
|
||||
# The maximum number of available search requests for a specific IP address is given by `rate_limiting_ip_pool`.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-ip-cooldown-ns
|
||||
|
||||
rate_limiting_disable_api_key = false
|
||||
# Prevents a Meilisearch instance from performing rate limiting per API key.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-disable-api-key
|
||||
|
||||
rate_limiting_api_key_pool = 10000
|
||||
# The maximum pool of search requests that can be performed using a specific API key before they are rejected.
|
||||
#
|
||||
# The pool starts full at the provided value, then each search request using the same API key diminishes the pool by 1.
|
||||
# When the pool is empty the search request is rejected.
|
||||
# The pool is replenished by 1 depending on the cooldown period.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-api-key-pool
|
||||
|
||||
rate_limiting_api_key_cooldown_ns = 500000
|
||||
# The amount of time, in nanoseconds, before the pool of available search requests using a specific API key is replenished by 1 again.
|
||||
#
|
||||
# The maximum number of available search requests using a specific API key is given by `rate_limiting_api_key_pool`.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#rate-limiting-api-key-cooldown-ns
|
||||
|
||||
|
||||
#################
|
||||
### SNAPSHOTS ###
|
||||
#################
|
||||
|
||||
schedule_snapshot = false
|
||||
# Enables scheduled snapshots when true, disable when false (the default).
|
||||
# If the value is given as an integer, then enables the scheduled snapshot with the passed value as the interval
|
||||
# between each snapshot, in seconds.
|
||||
# Activates scheduled snapshots when provided.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#schedule-snapshot-creation
|
||||
|
||||
snapshot_dir = "snapshots/"
|
||||
# Sets the directory where Meilisearch will store snapshots.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#snapshot-destination
|
||||
|
||||
snapshot_interval_sec = 86400
|
||||
# Defines the interval between each snapshot. Value must be given in seconds.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#snapshot-interval
|
||||
|
||||
# import_snapshot = "./path/to/my/snapshot"
|
||||
# Launches Meilisearch after importing a previously-generated snapshot at the given filepath.
|
||||
# https://docs.meilisearch.com/learn/configuration/instance_options.html#import-snapshot
|
||||
|
||||
@@ -249,17 +249,17 @@ pub(crate) mod test {
|
||||
|
||||
pub fn create_test_settings() -> Settings<Checked> {
|
||||
let settings = Settings {
|
||||
displayed_attributes: Setting::Set(vec![S("race"), S("name")]).into(),
|
||||
searchable_attributes: Setting::Set(vec![S("name"), S("race")]).into(),
|
||||
filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }).into(),
|
||||
sortable_attributes: Setting::Set(btreeset! { S("age") }).into(),
|
||||
ranking_rules: Setting::NotSet.into(),
|
||||
stop_words: Setting::NotSet.into(),
|
||||
synonyms: Setting::NotSet.into(),
|
||||
distinct_attribute: Setting::NotSet.into(),
|
||||
typo_tolerance: Setting::NotSet.into(),
|
||||
faceting: Setting::NotSet.into(),
|
||||
pagination: Setting::NotSet.into(),
|
||||
displayed_attributes: Setting::Set(vec![S("race"), S("name")]),
|
||||
searchable_attributes: Setting::Set(vec![S("name"), S("race")]),
|
||||
filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }),
|
||||
sortable_attributes: Setting::Set(btreeset! { S("age") }),
|
||||
ranking_rules: Setting::NotSet,
|
||||
stop_words: Setting::NotSet,
|
||||
synonyms: Setting::NotSet,
|
||||
distinct_attribute: Setting::NotSet,
|
||||
typo_tolerance: Setting::NotSet,
|
||||
faceting: Setting::NotSet,
|
||||
pagination: Setting::NotSet,
|
||||
_kind: std::marker::PhantomData,
|
||||
};
|
||||
settings.check()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: dump/src/reader/compat/v1_to_v2.rs
|
||||
expression: spells.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"filterableAttributes": [],
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"exactness"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"distinctAttribute": null
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: dump/src/reader/compat/v1_to_v2.rs
|
||||
expression: movies.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"filterableAttributes": [
|
||||
"genres",
|
||||
"id"
|
||||
],
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"exactness",
|
||||
"asc(release_date)"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"distinctAttribute": null
|
||||
}
|
||||
@@ -260,7 +260,7 @@ impl From<v5::ResponseError> for v6::ResponseError {
|
||||
"invalid_index_uid" => v6::Code::InvalidIndexUid,
|
||||
"invalid_min_word_length_for_typo" => v6::Code::InvalidMinWordLengthForTypo,
|
||||
"invalid_state" => v6::Code::InvalidState,
|
||||
"primary_key_inference_failed" => v6::Code::NoPrimaryKeyCandidateFound,
|
||||
"primary_key_inference_failed" => v6::Code::MissingPrimaryKey,
|
||||
"index_primary_key_already_exists" => v6::Code::PrimaryKeyAlreadyPresent,
|
||||
"max_fields_limit_exceeded" => v6::Code::MaxFieldsLimitExceeded,
|
||||
"missing_document_id" => v6::Code::MissingDocumentId,
|
||||
@@ -272,7 +272,7 @@ impl From<v5::ResponseError> for v6::ResponseError {
|
||||
"database_size_limit_reached" => v6::Code::DatabaseSizeLimitReached,
|
||||
"document_not_found" => v6::Code::DocumentNotFound,
|
||||
"internal" => v6::Code::Internal,
|
||||
"invalid_geo_field" => v6::Code::InvalidDocumentGeoField,
|
||||
"invalid_geo_field" => v6::Code::InvalidGeoField,
|
||||
"invalid_ranking_rule" => v6::Code::InvalidRankingRule,
|
||||
"invalid_store_file" => v6::Code::InvalidStore,
|
||||
"invalid_api_key" => v6::Code::InvalidToken,
|
||||
@@ -291,7 +291,7 @@ impl From<v5::ResponseError> for v6::ResponseError {
|
||||
"malformed_payload" => v6::Code::MalformedPayload,
|
||||
"missing_payload" => v6::Code::MissingPayload,
|
||||
"api_key_not_found" => v6::Code::ApiKeyNotFound,
|
||||
"missing_parameter" => v6::Code::UnretrievableErrorCode,
|
||||
"missing_parameter" => v6::Code::MissingParameter,
|
||||
"invalid_api_key_actions" => v6::Code::InvalidApiKeyActions,
|
||||
"invalid_api_key_indexes" => v6::Code::InvalidApiKeyIndexes,
|
||||
"invalid_api_key_expires_at" => v6::Code::InvalidApiKeyExpiresAt,
|
||||
@@ -419,7 +419,7 @@ pub(crate) mod test {
|
||||
// tasks
|
||||
let tasks = dump.tasks().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||
let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip();
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"10c673c97f053830aa659876d7aa0b53");
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"6519f7064c45d2196dd59b71350a9bf5");
|
||||
assert_eq!(update_files.len(), 22);
|
||||
assert!(update_files[0].is_none()); // the dump creation
|
||||
assert!(update_files[1].is_some()); // the enqueued document addition
|
||||
|
||||
@@ -201,7 +201,7 @@ pub(crate) mod test {
|
||||
// tasks
|
||||
let tasks = dump.tasks().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||
let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip();
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"10c673c97f053830aa659876d7aa0b53");
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"6519f7064c45d2196dd59b71350a9bf5");
|
||||
assert_eq!(update_files.len(), 22);
|
||||
assert!(update_files[0].is_none()); // the dump creation
|
||||
assert!(update_files[1].is_some()); // the enqueued document addition
|
||||
@@ -222,12 +222,12 @@ pub(crate) mod test {
|
||||
assert!(indexes.is_empty());
|
||||
|
||||
// products
|
||||
insta::assert_json_snapshot!(products.metadata(), @r###"
|
||||
insta::assert_json_snapshot!(products.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###"
|
||||
{
|
||||
"uid": "products",
|
||||
"primaryKey": "sku",
|
||||
"createdAt": "2022-10-04T15:51:35.939396731Z",
|
||||
"updatedAt": "2022-10-04T15:55:01.897325373Z"
|
||||
"createdAt": "[now]",
|
||||
"updatedAt": "[now]"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -237,12 +237,12 @@ pub(crate) mod test {
|
||||
meili_snap::snapshot_hash!(format!("{:#?}", documents), @"b01c8371aea4c7171af0d4d846a2bdca");
|
||||
|
||||
// movies
|
||||
insta::assert_json_snapshot!(movies.metadata(), @r###"
|
||||
insta::assert_json_snapshot!(movies.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###"
|
||||
{
|
||||
"uid": "movies",
|
||||
"primaryKey": "id",
|
||||
"createdAt": "2022-10-04T15:51:35.291992167Z",
|
||||
"updatedAt": "2022-10-04T15:55:10.33561842Z"
|
||||
"createdAt": "[now]",
|
||||
"updatedAt": "[now]"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -252,12 +252,12 @@ pub(crate) mod test {
|
||||
meili_snap::snapshot_hash!(format!("{:#?}", documents), @"e962baafd2fbae4cdd14e876053b0c5a");
|
||||
|
||||
// spells
|
||||
insta::assert_json_snapshot!(spells.metadata(), @r###"
|
||||
insta::assert_json_snapshot!(spells.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###"
|
||||
{
|
||||
"uid": "dnd_spells",
|
||||
"primaryKey": "index",
|
||||
"createdAt": "2022-10-04T15:51:37.381094632Z",
|
||||
"updatedAt": "2022-10-04T15:55:02.394503431Z"
|
||||
"createdAt": "[now]",
|
||||
"updatedAt": "[now]"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -279,7 +279,7 @@ pub(crate) mod test {
|
||||
// tasks
|
||||
let tasks = dump.tasks().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||
let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip();
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"12eca43d5d1e1f334200eb4df653b0c9");
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"491e244a80a19fe2a900b809d310c24a");
|
||||
assert_eq!(update_files.len(), 10);
|
||||
assert!(update_files[0].is_some()); // the enqueued document addition
|
||||
assert!(update_files[1..].iter().all(|u| u.is_none())); // everything already processed
|
||||
@@ -356,7 +356,7 @@ pub(crate) mod test {
|
||||
// tasks
|
||||
let tasks = dump.tasks().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||
let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip();
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"2f51c6345fabccf47b18c82bad618ffe");
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"7cacce2e21702be696b866808c726946");
|
||||
assert_eq!(update_files.len(), 10);
|
||||
assert!(update_files[0].is_some()); // the enqueued document addition
|
||||
assert!(update_files[1..].iter().all(|u| u.is_none())); // everything already processed
|
||||
@@ -449,7 +449,7 @@ pub(crate) mod test {
|
||||
// tasks
|
||||
let tasks = dump.tasks().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||
let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip();
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"b27292d0bb86d4b4dd1b375a46b33890");
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"6cabec4e252b74c8f3a2c8517622e85f");
|
||||
assert_eq!(update_files.len(), 9);
|
||||
assert!(update_files[0].is_some()); // the enqueued document addition
|
||||
assert!(update_files[1..].iter().all(|u| u.is_none())); // everything already processed
|
||||
@@ -542,7 +542,7 @@ pub(crate) mod test {
|
||||
// tasks
|
||||
let tasks = dump.tasks().unwrap().collect::<Result<Vec<_>>>().unwrap();
|
||||
let (tasks, update_files): (Vec<_>, Vec<_>) = tasks.into_iter().unzip();
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"9725ccfceea3f8d5846c44006c9e1e7b");
|
||||
meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"b3e3652bfc10a76670be157d2507d761");
|
||||
assert_eq!(update_files.len(), 9);
|
||||
assert!(update_files[..].iter().all(|u| u.is_none())); // no update file in dump v1
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: dump/src/reader/mod.rs
|
||||
expression: movies.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"filterableAttributes": [
|
||||
"genres",
|
||||
"id"
|
||||
],
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"exactness",
|
||||
"release_date:asc"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"distinctAttribute": null
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: dump/src/reader/mod.rs
|
||||
expression: spells.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"filterableAttributes": [],
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"exactness"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"distinctAttribute": null
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: dump/src/reader/mod.rs
|
||||
expression: products.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"filterableAttributes": [],
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"exactness"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {
|
||||
"android": [
|
||||
"phone",
|
||||
"smartphone"
|
||||
],
|
||||
"iphone": [
|
||||
"phone",
|
||||
"smartphone"
|
||||
],
|
||||
"phone": [
|
||||
"android",
|
||||
"iphone",
|
||||
"smartphone"
|
||||
]
|
||||
},
|
||||
"distinctAttribute": null
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: dump/src/reader/v1/mod.rs
|
||||
expression: dnd_spells.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"wordsPosition",
|
||||
"exactness"
|
||||
],
|
||||
"distinctAttribute": null,
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"attributesForFaceting": []
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: dump/src/reader/v1/mod.rs
|
||||
expression: movies.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"wordsPosition",
|
||||
"exactness",
|
||||
"asc(release_date)"
|
||||
],
|
||||
"distinctAttribute": null,
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"attributesForFaceting": [
|
||||
"id",
|
||||
"genres"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: dump/src/reader/v1/mod.rs
|
||||
expression: movies.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"wordsPosition",
|
||||
"exactness",
|
||||
"asc(release_date)"
|
||||
],
|
||||
"distinctAttribute": null,
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"attributesForFaceting": [
|
||||
"id",
|
||||
"genres"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: dump/src/reader/v1/mod.rs
|
||||
expression: dnd_spells.settings().unwrap()
|
||||
---
|
||||
{
|
||||
"rankingRules": [
|
||||
"typo",
|
||||
"words",
|
||||
"proximity",
|
||||
"attribute",
|
||||
"wordsPosition",
|
||||
"exactness"
|
||||
],
|
||||
"distinctAttribute": null,
|
||||
"searchableAttributes": [
|
||||
"*"
|
||||
],
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
],
|
||||
"stopWords": [],
|
||||
"synonyms": {},
|
||||
"attributesForFaceting": []
|
||||
}
|
||||
@@ -141,10 +141,6 @@ impl V5Reader {
|
||||
V5IndexReader::new(
|
||||
index.uid.clone(),
|
||||
&self.dump.path().join("indexes").join(index.index_meta.uuid.to_string()),
|
||||
&index.index_meta,
|
||||
BufReader::new(
|
||||
File::open(self.dump.path().join("updates").join("data.jsonl")).unwrap(),
|
||||
),
|
||||
)
|
||||
}))
|
||||
}
|
||||
@@ -193,39 +189,16 @@ pub struct V5IndexReader {
|
||||
}
|
||||
|
||||
impl V5IndexReader {
|
||||
pub fn new(
|
||||
name: String,
|
||||
path: &Path,
|
||||
index_metadata: &meta::IndexMeta,
|
||||
tasks: BufReader<File>,
|
||||
) -> Result<Self> {
|
||||
pub fn new(name: String, path: &Path) -> Result<Self> {
|
||||
let meta = File::open(path.join("meta.json"))?;
|
||||
let meta: meta::DumpMeta = serde_json::from_reader(meta)?;
|
||||
|
||||
let mut created_at = None;
|
||||
let mut updated_at = None;
|
||||
|
||||
for line in tasks.lines() {
|
||||
let task: Task = serde_json::from_str(&line?)?;
|
||||
|
||||
if *task.index_uid().unwrap_or_default().to_string() == name {
|
||||
if updated_at.is_none() {
|
||||
updated_at = task.processed_at()
|
||||
}
|
||||
|
||||
if task.id as usize == index_metadata.creation_task_id {
|
||||
created_at = task.created_at();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = IndexMetadata {
|
||||
uid: name,
|
||||
primary_key: meta.primary_key,
|
||||
created_at: created_at.unwrap_or_else(OffsetDateTime::now_utc),
|
||||
updated_at: updated_at.unwrap_or_else(OffsetDateTime::now_utc),
|
||||
// FIXME: Iterate over the whole task queue to find the creation and last update date.
|
||||
created_at: OffsetDateTime::now_utc(),
|
||||
updated_at: OffsetDateTime::now_utc(),
|
||||
};
|
||||
|
||||
let ret = V5IndexReader {
|
||||
@@ -329,12 +302,12 @@ pub(crate) mod test {
|
||||
assert!(indexes.is_empty());
|
||||
|
||||
// products
|
||||
insta::assert_json_snapshot!(products.metadata(), @r###"
|
||||
insta::assert_json_snapshot!(products.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###"
|
||||
{
|
||||
"uid": "products",
|
||||
"primaryKey": "sku",
|
||||
"createdAt": "2022-10-04T15:51:35.939396731Z",
|
||||
"updatedAt": "2022-10-04T15:55:01.897325373Z"
|
||||
"createdAt": "[now]",
|
||||
"updatedAt": "[now]"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -344,12 +317,12 @@ pub(crate) mod test {
|
||||
meili_snap::snapshot_hash!(format!("{:#?}", documents), @"b01c8371aea4c7171af0d4d846a2bdca");
|
||||
|
||||
// movies
|
||||
insta::assert_json_snapshot!(movies.metadata(), @r###"
|
||||
insta::assert_json_snapshot!(movies.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###"
|
||||
{
|
||||
"uid": "movies",
|
||||
"primaryKey": "id",
|
||||
"createdAt": "2022-10-04T15:51:35.291992167Z",
|
||||
"updatedAt": "2022-10-04T15:55:10.33561842Z"
|
||||
"createdAt": "[now]",
|
||||
"updatedAt": "[now]"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -359,12 +332,12 @@ pub(crate) mod test {
|
||||
meili_snap::snapshot_hash!(format!("{:#?}", documents), @"e962baafd2fbae4cdd14e876053b0c5a");
|
||||
|
||||
// spells
|
||||
insta::assert_json_snapshot!(spells.metadata(), @r###"
|
||||
insta::assert_json_snapshot!(spells.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###"
|
||||
{
|
||||
"uid": "dnd_spells",
|
||||
"primaryKey": "index",
|
||||
"createdAt": "2022-10-04T15:51:37.381094632Z",
|
||||
"updatedAt": "2022-10-04T15:55:02.394503431Z"
|
||||
"createdAt": "[now]",
|
||||
"updatedAt": "[now]"
|
||||
}
|
||||
"###);
|
||||
|
||||
|
||||
@@ -140,45 +140,6 @@ impl Task {
|
||||
TaskContent::Dump { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn processed_at(&self) -> Option<OffsetDateTime> {
|
||||
match self.events.last() {
|
||||
Some(TaskEvent::Succeeded { result: _, timestamp }) => Some(*timestamp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> Option<OffsetDateTime> {
|
||||
match &self.content {
|
||||
TaskContent::IndexCreation { index_uid: _, primary_key: _ } => {
|
||||
match self.events.first() {
|
||||
Some(TaskEvent::Created(ts)) => Some(*ts),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
TaskContent::DocumentAddition {
|
||||
index_uid: _,
|
||||
content_uuid: _,
|
||||
merge_strategy: _,
|
||||
primary_key: _,
|
||||
documents_count: _,
|
||||
allow_index_creation: _,
|
||||
} => match self.events.first() {
|
||||
Some(TaskEvent::Created(ts)) => Some(*ts),
|
||||
_ => None,
|
||||
},
|
||||
TaskContent::SettingsUpdate {
|
||||
index_uid: _,
|
||||
settings: _,
|
||||
is_deletion: _,
|
||||
allow_index_creation: _,
|
||||
} => match self.events.first() {
|
||||
Some(TaskEvent::Created(ts)) => Some(*ts),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexUid {
|
||||
|
||||
@@ -26,7 +26,7 @@ pub type Kind = crate::KindDump;
|
||||
pub type Details = meilisearch_types::tasks::Details;
|
||||
|
||||
// everything related to the settings
|
||||
pub type Setting<T> = meilisearch_types::settings::Setting<T>;
|
||||
pub type Setting<T> = meilisearch_types::milli::update::Setting<T>;
|
||||
pub type TypoTolerance = meilisearch_types::settings::TypoSettings;
|
||||
pub type MinWordSizeForTypos = meilisearch_types::settings::MinWordSizeTyposSetting;
|
||||
pub type FacetingSettings = meilisearch_types::settings::FacetingSettings;
|
||||
|
||||
@@ -882,11 +882,11 @@ impl IndexScheduler {
|
||||
}
|
||||
if !not_found_indexes.is_empty() {
|
||||
if not_found_indexes.len() == 1 {
|
||||
return Err(Error::SwapIndexNotFound(
|
||||
return Err(Error::IndexNotFound(
|
||||
not_found_indexes.into_iter().next().unwrap().clone(),
|
||||
));
|
||||
} else {
|
||||
return Err(Error::SwapIndexesNotFound(
|
||||
return Err(Error::IndexesNotFound(
|
||||
not_found_indexes.into_iter().cloned().collect(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use meilisearch_types::error::{Code, ErrorCode};
|
||||
use meilisearch_types::tasks::{Kind, Status};
|
||||
use meilisearch_types::{heed, milli};
|
||||
@@ -7,47 +5,16 @@ use thiserror::Error;
|
||||
|
||||
use crate::TaskId;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DateField {
|
||||
BeforeEnqueuedAt,
|
||||
AfterEnqueuedAt,
|
||||
BeforeStartedAt,
|
||||
AfterStartedAt,
|
||||
BeforeFinishedAt,
|
||||
AfterFinishedAt,
|
||||
}
|
||||
|
||||
impl Display for DateField {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DateField::BeforeEnqueuedAt => write!(f, "beforeEnqueuedAt"),
|
||||
DateField::AfterEnqueuedAt => write!(f, "afterEnqueuedAt"),
|
||||
DateField::BeforeStartedAt => write!(f, "beforeStartedAt"),
|
||||
DateField::AfterStartedAt => write!(f, "afterStartedAt"),
|
||||
DateField::BeforeFinishedAt => write!(f, "beforeFinishedAt"),
|
||||
DateField::AfterFinishedAt => write!(f, "afterFinishedAt"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateField> for Code {
|
||||
fn from(date: DateField) -> Self {
|
||||
match date {
|
||||
DateField::BeforeEnqueuedAt => Code::InvalidTaskBeforeEnqueuedAt,
|
||||
DateField::AfterEnqueuedAt => Code::InvalidTaskAfterEnqueuedAt,
|
||||
DateField::BeforeStartedAt => Code::InvalidTaskBeforeStartedAt,
|
||||
DateField::AfterStartedAt => Code::InvalidTaskAfterStartedAt,
|
||||
DateField::BeforeFinishedAt => Code::InvalidTaskBeforeFinishedAt,
|
||||
DateField::AfterFinishedAt => Code::InvalidTaskAfterFinishedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Index `{0}` not found.")]
|
||||
IndexNotFound(String),
|
||||
#[error(
|
||||
"Indexes {} not found.",
|
||||
.0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
|
||||
)]
|
||||
IndexesNotFound(Vec<String>),
|
||||
#[error("Index `{0}` already exists.")]
|
||||
IndexAlreadyExists(String),
|
||||
#[error(
|
||||
@@ -59,19 +26,12 @@ pub enum Error {
|
||||
.0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
|
||||
)]
|
||||
SwapDuplicateIndexesFound(Vec<String>),
|
||||
#[error("Index `{0}` not found.")]
|
||||
SwapIndexNotFound(String),
|
||||
#[error(
|
||||
"Indexes {} not found.",
|
||||
.0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
|
||||
)]
|
||||
SwapIndexesNotFound(Vec<String>),
|
||||
#[error("Corrupted dump.")]
|
||||
CorruptedDump,
|
||||
#[error(
|
||||
"Task `{field}` `{date}` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."
|
||||
)]
|
||||
InvalidTaskDate { field: DateField, date: String },
|
||||
InvalidTaskDate { field: String, date: String },
|
||||
#[error("Task uid `{task_uid}` is invalid. It should only contain numeric characters.")]
|
||||
InvalidTaskUids { task_uid: String },
|
||||
#[error(
|
||||
@@ -138,16 +98,15 @@ impl ErrorCode for Error {
|
||||
fn error_code(&self) -> Code {
|
||||
match self {
|
||||
Error::IndexNotFound(_) => Code::IndexNotFound,
|
||||
Error::IndexesNotFound(_) => Code::IndexNotFound,
|
||||
Error::IndexAlreadyExists(_) => Code::IndexAlreadyExists,
|
||||
Error::SwapDuplicateIndexesFound(_) => Code::InvalidDuplicateIndexesFound,
|
||||
Error::SwapDuplicateIndexFound(_) => Code::InvalidDuplicateIndexesFound,
|
||||
Error::SwapIndexNotFound(_) => Code::InvalidSwapIndexes,
|
||||
Error::SwapIndexesNotFound(_) => Code::InvalidSwapIndexes,
|
||||
Error::InvalidTaskDate { field, .. } => (*field).into(),
|
||||
Error::InvalidTaskUids { .. } => Code::InvalidTaskUids,
|
||||
Error::InvalidTaskStatuses { .. } => Code::InvalidTaskStatuses,
|
||||
Error::InvalidTaskTypes { .. } => Code::InvalidTaskTypes,
|
||||
Error::InvalidTaskCanceledBy { .. } => Code::InvalidTaskCanceledBy,
|
||||
Error::SwapDuplicateIndexesFound(_) => Code::DuplicateIndexFound,
|
||||
Error::SwapDuplicateIndexFound(_) => Code::DuplicateIndexFound,
|
||||
Error::InvalidTaskDate { .. } => Code::InvalidTaskDateFilter,
|
||||
Error::InvalidTaskUids { .. } => Code::InvalidTaskUidsFilter,
|
||||
Error::InvalidTaskStatuses { .. } => Code::InvalidTaskStatusesFilter,
|
||||
Error::InvalidTaskTypes { .. } => Code::InvalidTaskTypesFilter,
|
||||
Error::InvalidTaskCanceledBy { .. } => Code::InvalidTaskCanceledByFilter,
|
||||
Error::InvalidIndexUid { .. } => Code::InvalidIndexUid,
|
||||
Error::TaskNotFound(_) => Code::TaskNotFound,
|
||||
Error::TaskDeletionWithEmptyQuery => Code::TaskDeletionWithEmptyQuery,
|
||||
@@ -160,7 +119,6 @@ impl ErrorCode for Error {
|
||||
Error::FileStore(e) => e.error_code(),
|
||||
Error::IoError(e) => e.error_code(),
|
||||
Error::Persist(e) => e.error_code(),
|
||||
|
||||
// Irrecoverable errors
|
||||
Error::Anyhow(_) => Code::Internal,
|
||||
Error::CorruptedTaskQueue => Code::Internal,
|
||||
|
||||
@@ -227,9 +227,9 @@ pub struct IndexSchedulerOptions {
|
||||
pub snapshots_path: PathBuf,
|
||||
/// The path to the folder containing the dumps.
|
||||
pub dumps_path: PathBuf,
|
||||
/// The maximum size, in bytes, of the task index.
|
||||
pub task_db_size: usize,
|
||||
/// The maximum size, in bytes, of each meilisearch index.
|
||||
pub task_db_size: usize,
|
||||
/// The maximum size, in bytes, of the tasks index.
|
||||
pub index_size: usize,
|
||||
/// Configuration used during indexing for each meilisearch index.
|
||||
pub indexer_config: IndexerConfig,
|
||||
|
||||
@@ -10,7 +10,7 @@ source: index-scheduler/src/lib.rs
|
||||
1 {uid: 1, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "b", primary_key: Some("id") }}
|
||||
2 {uid: 2, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "c", primary_key: Some("id") }}
|
||||
3 {uid: 3, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "d", primary_key: Some("id") }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Indexes `e`, `f` not found.", error_code: "invalid_swap_indexes", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-swap-indexes" }, details: { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }, kind: IndexSwap { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Indexes `e`, `f` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }, kind: IndexSwap { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }}
|
||||
----------------------------------------------------------------------
|
||||
### Status:
|
||||
enqueued []
|
||||
|
||||
@@ -6,16 +6,16 @@ source: index-scheduler/src/lib.rs
|
||||
[]
|
||||
----------------------------------------------------------------------
|
||||
### All Tasks:
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }}
|
||||
2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }}
|
||||
5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }}
|
||||
6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }}
|
||||
7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }}
|
||||
8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }}
|
||||
9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }}
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }}
|
||||
2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }}
|
||||
5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }}
|
||||
6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }}
|
||||
7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }}
|
||||
8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }}
|
||||
9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }}
|
||||
----------------------------------------------------------------------
|
||||
### Status:
|
||||
enqueued []
|
||||
|
||||
@@ -6,16 +6,16 @@ source: index-scheduler/src/lib.rs
|
||||
[]
|
||||
----------------------------------------------------------------------
|
||||
### All Tasks:
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }}
|
||||
2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }}
|
||||
5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }}
|
||||
6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }}
|
||||
7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }}
|
||||
8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }}
|
||||
9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }}
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }}
|
||||
2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }}
|
||||
5 {uid: 5, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }}
|
||||
6 {uid: 6, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }}
|
||||
7 {uid: 7, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }}
|
||||
8 {uid: 8, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000008, documents_count: 1, allow_index_creation: false }}
|
||||
9 {uid: 9, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000009, documents_count: 1, allow_index_creation: false }}
|
||||
----------------------------------------------------------------------
|
||||
### Status:
|
||||
enqueued []
|
||||
|
||||
@@ -6,11 +6,11 @@ source: index-scheduler/src/lib.rs
|
||||
[]
|
||||
----------------------------------------------------------------------
|
||||
### All Tasks:
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }}
|
||||
2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }}
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: false }}
|
||||
2 {uid: 2, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: false }}
|
||||
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000004, documents_count: 1, allow_index_creation: false }}
|
||||
5 {uid: 5, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000005, documents_count: 1, allow_index_creation: false }}
|
||||
6 {uid: 6, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000006, documents_count: 1, allow_index_creation: false }}
|
||||
7 {uid: 7, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000007, documents_count: 1, allow_index_creation: false }}
|
||||
|
||||
@@ -6,7 +6,7 @@ source: index-scheduler/src/lib.rs
|
||||
[]
|
||||
----------------------------------------------------------------------
|
||||
### All Tasks:
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }}
|
||||
2 {uid: 2, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: succeeded, details: { received_documents: 1, indexed_documents: Some(1) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }}
|
||||
|
||||
@@ -6,7 +6,7 @@ source: index-scheduler/src/lib.rs
|
||||
[]
|
||||
----------------------------------------------------------------------
|
||||
### All Tasks:
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
0 {uid: 0, status: failed, error: ResponseError { code: 200, message: "Index `doggos` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index_not_found" }, details: { received_documents: 1, indexed_documents: Some(0) }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000000, documents_count: 1, allow_index_creation: false }}
|
||||
1 {uid: 1, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000001, documents_count: 1, allow_index_creation: true }}
|
||||
2 {uid: 2, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000002, documents_count: 1, allow_index_creation: false }}
|
||||
3 {uid: 3, status: enqueued, details: { received_documents: 1, indexed_documents: None }, kind: DocumentAdditionOrUpdate { index_uid: "doggos", primary_key: Some("id"), method: ReplaceDocuments, content_file: 00000000-0000-0000-0000-000000000003, documents_count: 1, allow_index_creation: true }}
|
||||
|
||||
@@ -7,16 +7,14 @@ edition = "2021"
|
||||
[dependencies]
|
||||
actix-web = { version = "4.2.1", default-features = false }
|
||||
anyhow = "1.0.65"
|
||||
convert_case = "0.6.0"
|
||||
csv = "1.1.6"
|
||||
deserr = { version = "0.1.2", features = ["serde-json"] }
|
||||
either = { version = "1.6.1", features = ["serde"] }
|
||||
enum-iterator = "1.1.3"
|
||||
file-store = { path = "../file-store" }
|
||||
flate2 = "1.0.24"
|
||||
fst = "0.4.7"
|
||||
memmap2 = "0.5.7"
|
||||
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.38.0", default-features = false }
|
||||
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.37.3", default-features = false }
|
||||
proptest = { version = "1.0.0", optional = true }
|
||||
proptest-derive = { version = "0.3.0", optional = true }
|
||||
roaring = { version = "0.10.0", features = ["serde"] }
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::fs::File;
|
||||
use std::io::{self, Seek, Write};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use either::Either;
|
||||
use memmap2::MmapOptions;
|
||||
use milli::documents::{DocumentsBatchBuilder, Error};
|
||||
use milli::Object;
|
||||
@@ -119,6 +120,20 @@ pub fn read_csv(file: &File, writer: impl Write + Seek) -> Result<u64> {
|
||||
|
||||
/// Reads JSON from temporary file and write an obkv batch to writer.
|
||||
pub fn read_json(file: &File, writer: impl Write + Seek) -> Result<u64> {
|
||||
read_json_inner(file, writer, PayloadType::Json)
|
||||
}
|
||||
|
||||
/// Reads JSON from temporary file and write an obkv batch to writer.
|
||||
pub fn read_ndjson(file: &File, writer: impl Write + Seek) -> Result<u64> {
|
||||
read_json_inner(file, writer, PayloadType::Ndjson)
|
||||
}
|
||||
|
||||
/// Reads JSON from temporary file and write an obkv batch to writer.
|
||||
fn read_json_inner(
|
||||
file: &File,
|
||||
writer: impl Write + Seek,
|
||||
payload_type: PayloadType,
|
||||
) -> Result<u64> {
|
||||
let mut builder = DocumentsBatchBuilder::new(writer);
|
||||
let mmap = unsafe { MmapOptions::new().map(file)? };
|
||||
let mut deserializer = serde_json::Deserializer::from_slice(&mmap);
|
||||
@@ -128,20 +143,23 @@ pub fn read_json(file: &File, writer: impl Write + Seek) -> Result<u64> {
|
||||
// The data has been transferred to the writer during the deserialization process.
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(e)) => return Err(DocumentFormatError::Io(e)),
|
||||
Err(e) => {
|
||||
// Attempt to deserialize a single json string when the cause of the exception is not Category.data
|
||||
// Other types of deserialisation exceptions are returned directly to the front-end
|
||||
if e.classify() != serde_json::error::Category::Data {
|
||||
return Err(DocumentFormatError::MalformedPayload(
|
||||
Error::Json(e),
|
||||
PayloadType::Json,
|
||||
));
|
||||
Err(_e) => {
|
||||
// If we cannot deserialize the content as an array of object then we try
|
||||
// to deserialize it with the original method to keep correct error messages.
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(transparent)]
|
||||
struct ArrayOrSingleObject {
|
||||
#[serde(with = "either::serde_untagged")]
|
||||
inner: Either<Vec<Object>, Object>,
|
||||
}
|
||||
|
||||
let content: Object = serde_json::from_slice(&mmap)
|
||||
let content: ArrayOrSingleObject = serde_json::from_reader(file)
|
||||
.map_err(Error::Json)
|
||||
.map_err(|e| (PayloadType::Json, e))?;
|
||||
builder.append_json_object(&content).map_err(DocumentFormatError::Io)?;
|
||||
.map_err(|e| (payload_type, e))?;
|
||||
|
||||
for object in content.inner.map_right(|o| vec![o]).into_inner() {
|
||||
builder.append_json_object(&object).map_err(DocumentFormatError::Io)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,22 +169,6 @@ pub fn read_json(file: &File, writer: impl Write + Seek) -> Result<u64> {
|
||||
Ok(count as u64)
|
||||
}
|
||||
|
||||
/// Reads JSON from temporary file and write an obkv batch to writer.
|
||||
pub fn read_ndjson(file: &File, writer: impl Write + Seek) -> Result<u64> {
|
||||
let mut builder = DocumentsBatchBuilder::new(writer);
|
||||
let mmap = unsafe { MmapOptions::new().map(file)? };
|
||||
|
||||
for result in serde_json::Deserializer::from_slice(&mmap).into_iter() {
|
||||
let object = result.map_err(Error::Json).map_err(|e| (PayloadType::Ndjson, e))?;
|
||||
builder.append_json_object(&object).map_err(Into::into).map_err(DocumentFormatError::Io)?;
|
||||
}
|
||||
|
||||
let count = builder.documents_count();
|
||||
let _ = builder.into_inner().map_err(Into::into).map_err(DocumentFormatError::Io)?;
|
||||
|
||||
Ok(count as u64)
|
||||
}
|
||||
|
||||
/// The actual handling of the deserialization process in serde
|
||||
/// avoids storing the deserialized object in memory.
|
||||
///
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::{fmt, io};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{self as aweb, HttpResponseBuilder};
|
||||
use aweb::rt::task::JoinError;
|
||||
use convert_case::Casing;
|
||||
use milli::heed::{Error as HeedError, MdbError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -95,7 +94,6 @@ enum ErrorType {
|
||||
InternalError,
|
||||
InvalidRequestError,
|
||||
AuthenticationError,
|
||||
System,
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorType {
|
||||
@@ -106,12 +104,11 @@ impl fmt::Display for ErrorType {
|
||||
InternalError => write!(f, "internal"),
|
||||
InvalidRequestError => write!(f, "invalid_request"),
|
||||
AuthenticationError => write!(f, "auth"),
|
||||
System => write!(f, "system"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Code {
|
||||
// error related to your setup
|
||||
IoError,
|
||||
@@ -121,20 +118,15 @@ pub enum Code {
|
||||
// index related error
|
||||
CreateIndex,
|
||||
IndexAlreadyExists,
|
||||
InvalidIndexPrimaryKey,
|
||||
IndexNotFound,
|
||||
InvalidIndexUid,
|
||||
MissingIndexUid,
|
||||
InvalidMinWordLengthForTypo,
|
||||
InvalidIndexLimit,
|
||||
InvalidIndexOffset,
|
||||
|
||||
DuplicateIndexFound,
|
||||
|
||||
// invalid state error
|
||||
InvalidState,
|
||||
NoPrimaryKeyCandidateFound,
|
||||
MultiplePrimaryKeyCandidatesFound,
|
||||
MissingPrimaryKey,
|
||||
PrimaryKeyAlreadyPresent,
|
||||
|
||||
MaxFieldsLimitExceeded,
|
||||
@@ -144,73 +136,23 @@ pub enum Code {
|
||||
Filter,
|
||||
Sort,
|
||||
|
||||
// Invalid swap-indexes
|
||||
InvalidSwapIndexes,
|
||||
InvalidDuplicateIndexesFound,
|
||||
|
||||
// Invalid settings update request
|
||||
InvalidSettingsDisplayedAttributes,
|
||||
InvalidSettingsSearchableAttributes,
|
||||
InvalidSettingsFilterableAttributes,
|
||||
InvalidSettingsSortableAttributes,
|
||||
InvalidSettingsRankingRules,
|
||||
InvalidSettingsStopWords,
|
||||
InvalidSettingsSynonyms,
|
||||
InvalidSettingsDistinctAttribute,
|
||||
InvalidSettingsTypoTolerance,
|
||||
InvalidSettingsFaceting,
|
||||
InvalidSettingsPagination,
|
||||
|
||||
// Invalid search request
|
||||
InvalidSearchQ,
|
||||
InvalidSearchOffset,
|
||||
InvalidSearchLimit,
|
||||
InvalidSearchPage,
|
||||
InvalidSearchHitsPerPage,
|
||||
InvalidSearchAttributesToRetrieve,
|
||||
InvalidSearchAttributesToCrop,
|
||||
InvalidSearchCropLength,
|
||||
InvalidSearchAttributesToHighlight,
|
||||
InvalidSearchShowMatchesPosition,
|
||||
InvalidSearchFilter,
|
||||
InvalidSearchSort,
|
||||
InvalidSearchFacets,
|
||||
InvalidSearchHighlightPreTag,
|
||||
InvalidSearchHighlightPostTag,
|
||||
InvalidSearchCropMarker,
|
||||
InvalidSearchMatchingStrategy,
|
||||
|
||||
// Related to the tasks
|
||||
InvalidTaskUids,
|
||||
InvalidTaskTypes,
|
||||
InvalidTaskStatuses,
|
||||
InvalidTaskCanceledBy,
|
||||
InvalidTaskLimit,
|
||||
InvalidTaskFrom,
|
||||
InvalidTaskBeforeEnqueuedAt,
|
||||
InvalidTaskAfterEnqueuedAt,
|
||||
InvalidTaskBeforeStartedAt,
|
||||
InvalidTaskAfterStartedAt,
|
||||
InvalidTaskBeforeFinishedAt,
|
||||
InvalidTaskAfterFinishedAt,
|
||||
|
||||
// Documents API
|
||||
InvalidDocumentFields,
|
||||
InvalidDocumentLimit,
|
||||
InvalidDocumentOffset,
|
||||
|
||||
BadParameter,
|
||||
BadRequest,
|
||||
DatabaseSizeLimitReached,
|
||||
DocumentNotFound,
|
||||
Internal,
|
||||
InvalidDocumentGeoField,
|
||||
InvalidGeoField,
|
||||
InvalidRankingRule,
|
||||
InvalidStore,
|
||||
InvalidToken,
|
||||
MissingAuthorizationHeader,
|
||||
MissingMasterKey,
|
||||
DumpNotFound,
|
||||
InvalidTaskDateFilter,
|
||||
InvalidTaskStatusesFilter,
|
||||
InvalidTaskTypesFilter,
|
||||
InvalidTaskCanceledByFilter,
|
||||
InvalidTaskUidsFilter,
|
||||
TaskNotFound,
|
||||
TaskDeletionWithEmptyQuery,
|
||||
TaskCancelationWithEmptyQuery,
|
||||
@@ -230,13 +172,7 @@ pub enum Code {
|
||||
MissingPayload,
|
||||
|
||||
ApiKeyNotFound,
|
||||
|
||||
MissingApiKeyActions,
|
||||
MissingApiKeyExpiresAt,
|
||||
MissingApiKeyIndexes,
|
||||
|
||||
InvalidApiKeyOffset,
|
||||
InvalidApiKeyLimit,
|
||||
MissingParameter,
|
||||
InvalidApiKeyActions,
|
||||
InvalidApiKeyIndexes,
|
||||
InvalidApiKeyExpiresAt,
|
||||
@@ -254,12 +190,12 @@ impl Code {
|
||||
|
||||
match self {
|
||||
// related to the setup
|
||||
IoError => ErrCode::system("io_error", StatusCode::UNPROCESSABLE_ENTITY),
|
||||
IoError => ErrCode::invalid("io_error", StatusCode::UNPROCESSABLE_ENTITY),
|
||||
TooManyOpenFiles => {
|
||||
ErrCode::system("too_many_open_files", StatusCode::UNPROCESSABLE_ENTITY)
|
||||
ErrCode::invalid("too_many_open_files", StatusCode::UNPROCESSABLE_ENTITY)
|
||||
}
|
||||
NoSpaceLeftOnDevice => {
|
||||
ErrCode::system("no_space_left_on_device", StatusCode::UNPROCESSABLE_ENTITY)
|
||||
ErrCode::invalid("no_space_left_on_device", StatusCode::UNPROCESSABLE_ENTITY)
|
||||
}
|
||||
|
||||
// index related errors
|
||||
@@ -271,23 +207,13 @@ impl Code {
|
||||
// thrown when requesting an unexisting index
|
||||
IndexNotFound => ErrCode::invalid("index_not_found", StatusCode::NOT_FOUND),
|
||||
InvalidIndexUid => ErrCode::invalid("invalid_index_uid", StatusCode::BAD_REQUEST),
|
||||
MissingIndexUid => ErrCode::invalid("missing_index_uid", StatusCode::BAD_REQUEST),
|
||||
InvalidIndexPrimaryKey => {
|
||||
ErrCode::invalid("invalid_index_primary_key", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidIndexLimit => ErrCode::invalid("invalid_index_limit", StatusCode::BAD_REQUEST),
|
||||
InvalidIndexOffset => ErrCode::invalid("invalid_index_offset", StatusCode::BAD_REQUEST),
|
||||
|
||||
// invalid state error
|
||||
InvalidState => ErrCode::internal("invalid_state", StatusCode::INTERNAL_SERVER_ERROR),
|
||||
// thrown when no primary key has been set
|
||||
NoPrimaryKeyCandidateFound => {
|
||||
ErrCode::invalid("index_primary_key_no_candidate_found", StatusCode::BAD_REQUEST)
|
||||
MissingPrimaryKey => {
|
||||
ErrCode::invalid("primary_key_inference_failed", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
MultiplePrimaryKeyCandidatesFound => ErrCode::invalid(
|
||||
"index_primary_key_multiple_candidates_found",
|
||||
StatusCode::BAD_REQUEST,
|
||||
),
|
||||
// error thrown when trying to set an already existing primary key
|
||||
PrimaryKeyAlreadyPresent => {
|
||||
ErrCode::invalid("index_primary_key_already_exists", StatusCode::BAD_REQUEST)
|
||||
@@ -319,9 +245,7 @@ impl Code {
|
||||
}
|
||||
DocumentNotFound => ErrCode::invalid("document_not_found", StatusCode::NOT_FOUND),
|
||||
Internal => ErrCode::internal("internal", StatusCode::INTERNAL_SERVER_ERROR),
|
||||
InvalidDocumentGeoField => {
|
||||
ErrCode::invalid("invalid_document_geo_field", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidGeoField => ErrCode::invalid("invalid_geo_field", StatusCode::BAD_REQUEST),
|
||||
InvalidToken => ErrCode::authentication("invalid_api_key", StatusCode::FORBIDDEN),
|
||||
MissingAuthorizationHeader => {
|
||||
ErrCode::authentication("missing_authorization_header", StatusCode::UNAUTHORIZED)
|
||||
@@ -329,6 +253,21 @@ impl Code {
|
||||
MissingMasterKey => {
|
||||
ErrCode::authentication("missing_master_key", StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
InvalidTaskDateFilter => {
|
||||
ErrCode::invalid("invalid_task_date_filter", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskUidsFilter => {
|
||||
ErrCode::invalid("invalid_task_uids_filter", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskStatusesFilter => {
|
||||
ErrCode::invalid("invalid_task_statuses_filter", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskTypesFilter => {
|
||||
ErrCode::invalid("invalid_task_types_filter", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskCanceledByFilter => {
|
||||
ErrCode::invalid("invalid_task_canceled_by_filter", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
TaskNotFound => ErrCode::invalid("task_not_found", StatusCode::NOT_FOUND),
|
||||
TaskDeletionWithEmptyQuery => {
|
||||
ErrCode::invalid("missing_task_filters", StatusCode::BAD_REQUEST)
|
||||
@@ -368,25 +307,7 @@ impl Code {
|
||||
|
||||
// error related to keys
|
||||
ApiKeyNotFound => ErrCode::invalid("api_key_not_found", StatusCode::NOT_FOUND),
|
||||
|
||||
MissingApiKeyExpiresAt => {
|
||||
ErrCode::invalid("missing_api_key_expires_at", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
MissingApiKeyActions => {
|
||||
ErrCode::invalid("missing_api_key_actions", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
MissingApiKeyIndexes => {
|
||||
ErrCode::invalid("missing_api_key_indexes", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
InvalidApiKeyOffset => {
|
||||
ErrCode::invalid("invalid_api_key_offset", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidApiKeyLimit => {
|
||||
ErrCode::invalid("invalid_api_key_limit", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
MissingParameter => ErrCode::invalid("missing_parameter", StatusCode::BAD_REQUEST),
|
||||
InvalidApiKeyActions => {
|
||||
ErrCode::invalid("invalid_api_key_actions", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
@@ -409,132 +330,6 @@ impl Code {
|
||||
DuplicateIndexFound => {
|
||||
ErrCode::invalid("duplicate_index_found", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
// Swap indexes error
|
||||
InvalidSwapIndexes => ErrCode::invalid("invalid_swap_indexes", StatusCode::BAD_REQUEST),
|
||||
InvalidDuplicateIndexesFound => {
|
||||
ErrCode::invalid("invalid_swap_duplicate_index_found", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
// Invalid settings
|
||||
InvalidSettingsDisplayedAttributes => {
|
||||
ErrCode::invalid("invalid_settings_displayed_attributes", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsSearchableAttributes => {
|
||||
ErrCode::invalid("invalid_settings_searchable_attributes", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsFilterableAttributes => {
|
||||
ErrCode::invalid("invalid_settings_filterable_attributes", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsSortableAttributes => {
|
||||
ErrCode::invalid("invalid_settings_sortable_attributes", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsRankingRules => {
|
||||
ErrCode::invalid("invalid_settings_ranking_rules", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsStopWords => {
|
||||
ErrCode::invalid("invalid_settings_stop_words", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsSynonyms => {
|
||||
ErrCode::invalid("invalid_settings_synonyms", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsDistinctAttribute => {
|
||||
ErrCode::invalid("invalid_settings_distinct_attribute", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsTypoTolerance => {
|
||||
ErrCode::invalid("invalid_settings_typo_tolerance", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsFaceting => {
|
||||
ErrCode::invalid("invalid_settings_faceting", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSettingsPagination => {
|
||||
ErrCode::invalid("invalid_settings_pagination", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
// Invalid search
|
||||
InvalidSearchQ => ErrCode::invalid("invalid_search_q", StatusCode::BAD_REQUEST),
|
||||
InvalidSearchOffset => {
|
||||
ErrCode::invalid("invalid_search_offset", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchLimit => ErrCode::invalid("invalid_search_limit", StatusCode::BAD_REQUEST),
|
||||
InvalidSearchPage => ErrCode::invalid("invalid_search_page", StatusCode::BAD_REQUEST),
|
||||
InvalidSearchHitsPerPage => {
|
||||
ErrCode::invalid("invalid_search_hits_per_page", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchAttributesToRetrieve => {
|
||||
ErrCode::invalid("invalid_search_attributes_to_retrieve", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchAttributesToCrop => {
|
||||
ErrCode::invalid("invalid_search_attributes_to_crop", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchCropLength => {
|
||||
ErrCode::invalid("invalid_search_crop_length", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchAttributesToHighlight => {
|
||||
ErrCode::invalid("invalid_search_attributes_to_highlight", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchShowMatchesPosition => {
|
||||
ErrCode::invalid("invalid_search_show_matches_position", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchFilter => {
|
||||
ErrCode::invalid("invalid_search_filter", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchSort => ErrCode::invalid("invalid_search_sort", StatusCode::BAD_REQUEST),
|
||||
InvalidSearchFacets => {
|
||||
ErrCode::invalid("invalid_search_facets", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchHighlightPreTag => {
|
||||
ErrCode::invalid("invalid_search_highlight_pre_tag", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchHighlightPostTag => {
|
||||
ErrCode::invalid("invalid_search_highlight_post_tag", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchCropMarker => {
|
||||
ErrCode::invalid("invalid_search_crop_marker", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidSearchMatchingStrategy => {
|
||||
ErrCode::invalid("invalid_search_matching_strategy", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
// Related to the tasks
|
||||
InvalidTaskUids => ErrCode::invalid("invalid_task_uids", StatusCode::BAD_REQUEST),
|
||||
InvalidTaskTypes => ErrCode::invalid("invalid_task_types", StatusCode::BAD_REQUEST),
|
||||
InvalidTaskStatuses => {
|
||||
ErrCode::invalid("invalid_task_statuses", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskCanceledBy => {
|
||||
ErrCode::invalid("invalid_task_canceled_by", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskLimit => ErrCode::invalid("invalid_task_limit", StatusCode::BAD_REQUEST),
|
||||
InvalidTaskFrom => ErrCode::invalid("invalid_task_from", StatusCode::BAD_REQUEST),
|
||||
InvalidTaskBeforeEnqueuedAt => {
|
||||
ErrCode::invalid("invalid_task_before_enqueued_at", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskAfterEnqueuedAt => {
|
||||
ErrCode::invalid("invalid_task_after_enqueued_at", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskBeforeStartedAt => {
|
||||
ErrCode::invalid("invalid_task_before_started_at", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskAfterStartedAt => {
|
||||
ErrCode::invalid("invalid_task_after_started_at", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskBeforeFinishedAt => {
|
||||
ErrCode::invalid("invalid_task_before_finished_at", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidTaskAfterFinishedAt => {
|
||||
ErrCode::invalid("invalid_task_after_finished_at", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
InvalidDocumentFields => {
|
||||
ErrCode::invalid("invalid_document_fields", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidDocumentLimit => {
|
||||
ErrCode::invalid("invalid_document_limit", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
InvalidDocumentOffset => {
|
||||
ErrCode::invalid("invalid_document_offset", StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,10 +350,7 @@ impl Code {
|
||||
|
||||
/// return the doc url associated with the error
|
||||
fn url(&self) -> String {
|
||||
format!(
|
||||
"https://docs.meilisearch.com/errors#{}",
|
||||
self.name().to_case(convert_case::Case::Kebab)
|
||||
)
|
||||
format!("https://docs.meilisearch.com/errors#{}", self.name())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,10 +373,6 @@ impl ErrCode {
|
||||
fn invalid(error_name: &'static str, status_code: StatusCode) -> ErrCode {
|
||||
ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError }
|
||||
}
|
||||
|
||||
fn system(error_name: &'static str, status_code: StatusCode) -> ErrCode {
|
||||
ErrCode { status_code, error_name, error_type: ErrorType::System }
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorCode for JoinError {
|
||||
@@ -617,16 +405,13 @@ impl ErrorCode for milli::Error {
|
||||
UserError::InvalidDocumentId { .. } | UserError::TooManyDocumentIds { .. } => {
|
||||
Code::InvalidDocumentId
|
||||
}
|
||||
UserError::NoPrimaryKeyCandidateFound => Code::NoPrimaryKeyCandidateFound,
|
||||
UserError::MultiplePrimaryKeyCandidatesFound { .. } => {
|
||||
Code::MultiplePrimaryKeyCandidatesFound
|
||||
}
|
||||
UserError::MissingPrimaryKey => Code::MissingPrimaryKey,
|
||||
UserError::PrimaryKeyCannotBeChanged(_) => Code::PrimaryKeyAlreadyPresent,
|
||||
UserError::SortRankingRuleMissing => Code::Sort,
|
||||
UserError::InvalidFacetsDistribution { .. } => Code::BadRequest,
|
||||
UserError::InvalidSortableAttribute { .. } => Code::Sort,
|
||||
UserError::CriterionError(_) => Code::InvalidRankingRule,
|
||||
UserError::InvalidGeoField { .. } => Code::InvalidDocumentGeoField,
|
||||
UserError::InvalidGeoField { .. } => Code::InvalidGeoField,
|
||||
UserError::SortError(_) => Code::Sort,
|
||||
UserError::InvalidMinTypoWordLenSetting(_, _) => {
|
||||
Code::InvalidMinWordLengthForTypo
|
||||
@@ -679,13 +464,6 @@ impl ErrorCode for io::Error {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_any<T>(any: Result<T, T>) -> T {
|
||||
match any {
|
||||
Ok(any) => any,
|
||||
Err(any) => any,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-traits")]
|
||||
mod strategy {
|
||||
use proptest::strategy::Strategy;
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Key {
|
||||
.map(|act| {
|
||||
from_value(act.clone()).map_err(|_| Error::InvalidApiKeyActions(act.clone()))
|
||||
})
|
||||
.ok_or(Error::MissingApiKeyActions)??;
|
||||
.ok_or(Error::MissingParameter("actions"))??;
|
||||
|
||||
let indexes = value
|
||||
.get("indexes")
|
||||
@@ -75,12 +75,12 @@ impl Key {
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
.ok_or(Error::MissingApiKeyIndexes)??;
|
||||
.ok_or(Error::MissingParameter("indexes"))??;
|
||||
|
||||
let expires_at = value
|
||||
.get("expiresAt")
|
||||
.map(parse_expiration_date)
|
||||
.ok_or(Error::MissingApiKeyExpiresAt)??;
|
||||
.ok_or(Error::MissingParameter("expiresAt"))??;
|
||||
|
||||
let created_at = OffsetDateTime::now_utc();
|
||||
let updated_at = created_at;
|
||||
@@ -344,12 +344,8 @@ pub mod actions {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("`expiresAt` field is mandatory.")]
|
||||
MissingApiKeyExpiresAt,
|
||||
#[error("`indexes` field is mandatory.")]
|
||||
MissingApiKeyIndexes,
|
||||
#[error("`actions` field is mandatory.")]
|
||||
MissingApiKeyActions,
|
||||
#[error("`{0}` field is mandatory.")]
|
||||
MissingParameter(&'static str),
|
||||
#[error("`actions` field value `{0}` is invalid. It should be an array of string representing action names.")]
|
||||
InvalidApiKeyActions(Value),
|
||||
#[error("`indexes` field value `{0}` is invalid. It should be an array of string representing index names.")]
|
||||
@@ -379,9 +375,7 @@ impl From<IndexUidFormatError> for Error {
|
||||
impl ErrorCode for Error {
|
||||
fn error_code(&self) -> Code {
|
||||
match self {
|
||||
Self::MissingApiKeyExpiresAt => Code::MissingApiKeyExpiresAt,
|
||||
Self::MissingApiKeyIndexes => Code::MissingApiKeyIndexes,
|
||||
Self::MissingApiKeyActions => Code::MissingApiKeyActions,
|
||||
Self::MissingParameter(_) => Code::MissingParameter,
|
||||
Self::InvalidApiKeyActions(_) => Code::InvalidApiKeyActions,
|
||||
Self::InvalidApiKeyIndexes(_) | Self::InvalidApiKeyIndexUid(_) => {
|
||||
Code::InvalidApiKeyIndexes
|
||||
|
||||
@@ -2,10 +2,10 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::marker::PhantomData;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use deserr::{DeserializeError, DeserializeFromValue};
|
||||
use fst::IntoStreamer;
|
||||
use milli::update::Setting;
|
||||
use milli::{Index, DEFAULT_VALUES_PER_FACET};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
/// The maximimum number of results that the engine
|
||||
/// will be able to return in one search call.
|
||||
@@ -27,135 +27,16 @@ where
|
||||
.serialize(s)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub enum Setting<T> {
|
||||
Set(T),
|
||||
Reset,
|
||||
NotSet,
|
||||
}
|
||||
|
||||
impl<T> Default for Setting<T> {
|
||||
fn default() -> Self {
|
||||
Self::NotSet
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Setting<T>> for milli::update::Setting<T> {
|
||||
fn from(value: Setting<T>) -> Self {
|
||||
match value {
|
||||
Setting::Set(x) => milli::update::Setting::Set(x),
|
||||
Setting::Reset => milli::update::Setting::Reset,
|
||||
Setting::NotSet => milli::update::Setting::NotSet,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> From<milli::update::Setting<T>> for Setting<T> {
|
||||
fn from(value: milli::update::Setting<T>) -> Self {
|
||||
match value {
|
||||
milli::update::Setting::Set(x) => Setting::Set(x),
|
||||
milli::update::Setting::Reset => Setting::Reset,
|
||||
milli::update::Setting::NotSet => Setting::NotSet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Setting<T> {
|
||||
pub fn set(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Set(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_ref(&self) -> Setting<&T> {
|
||||
match *self {
|
||||
Self::Set(ref value) => Setting::Set(value),
|
||||
Self::Reset => Setting::Reset,
|
||||
Self::NotSet => Setting::NotSet,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_not_set(&self) -> bool {
|
||||
matches!(self, Self::NotSet)
|
||||
}
|
||||
|
||||
/// If `Self` is `Reset`, then map self to `Set` with the provided `val`.
|
||||
pub fn or_reset(self, val: T) -> Self {
|
||||
match self {
|
||||
Self::Reset => Self::Set(val),
|
||||
otherwise => otherwise,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for Setting<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Set(value) => Some(value),
|
||||
// Usually not_set isn't serialized by setting skip_serializing_if field attribute
|
||||
Self::NotSet | Self::Reset => None,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Setting<T> {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).map(|x| match x {
|
||||
Some(x) => Self::Set(x),
|
||||
None => Self::Reset, // Reset is forced by sending null value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> DeserializeFromValue<E> for Setting<T>
|
||||
where
|
||||
T: DeserializeFromValue<E>,
|
||||
E: DeserializeError,
|
||||
{
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
value: deserr::Value<V>,
|
||||
location: deserr::ValuePointerRef,
|
||||
) -> Result<Self, E> {
|
||||
match value {
|
||||
deserr::Value::Null => Ok(Setting::Reset),
|
||||
_ => T::deserialize_from_value(value, location).map(Setting::Set),
|
||||
}
|
||||
}
|
||||
fn default() -> Option<Self> {
|
||||
Some(Self::NotSet)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct Checked;
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Unchecked;
|
||||
|
||||
impl<E> DeserializeFromValue<E> for Unchecked
|
||||
where
|
||||
E: DeserializeError,
|
||||
{
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
_value: deserr::Value<V>,
|
||||
_location: deserr::ValuePointerRef,
|
||||
) -> Result<Self, E> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct MinWordSizeTyposSetting {
|
||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
@@ -166,10 +47,9 @@ pub struct MinWordSizeTyposSetting {
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct TypoSettings {
|
||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
@@ -186,10 +66,9 @@ pub struct TypoSettings {
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct FacetingSettings {
|
||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
@@ -197,10 +76,9 @@ pub struct FacetingSettings {
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct PaginationSettings {
|
||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
@@ -210,11 +88,10 @@ pub struct PaginationSettings {
|
||||
/// Holds all the settings for an index. `T` can either be `Checked` if they represents settings
|
||||
/// whose validity is guaranteed, or `Unchecked` if they need to be validated. In the later case, a
|
||||
/// call to `check` will return a `Settings<Checked>` from a `Settings<Unchecked>`.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>"))]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
||||
pub struct Settings<T> {
|
||||
#[serde(
|
||||
|
||||
@@ -4,10 +4,11 @@ description = "Meilisearch HTTP server"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "meilisearch"
|
||||
version = "1.0.0"
|
||||
version = "0.30.1"
|
||||
|
||||
[dependencies]
|
||||
actix-cors = "0.6.3"
|
||||
actix-governor = "0.3.2"
|
||||
actix-http = { version = "3.2.2", default-features = false, features = ["compress-brotli", "compress-gzip", "rustls"] }
|
||||
actix-web = { version = "4.2.1", default-features = false, features = ["macros", "compress-brotli", "compress-gzip", "cookies", "rustls"] }
|
||||
actix-web-static-files = { git = "https://github.com/kilork/actix-web-static-files.git", rev = "2d3b6160", optional = true }
|
||||
@@ -19,7 +20,6 @@ byte-unit = { version = "4.0.14", default-features = false, features = ["std", "
|
||||
bytes = "1.2.1"
|
||||
clap = { version = "4.0.9", features = ["derive", "env"] }
|
||||
crossbeam-channel = "0.5.6"
|
||||
deserr = { version = "0.1.2", features = ["serde-json"] }
|
||||
dump = { path = "../dump" }
|
||||
either = "1.8.0"
|
||||
env_logger = "0.9.1"
|
||||
@@ -72,14 +72,11 @@ toml = "0.5.9"
|
||||
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
||||
walkdir = "2.3.2"
|
||||
yaup = "0.2.0"
|
||||
serde_urlencoded = "0.7.1"
|
||||
actix-utils = "3.0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.7.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
brotli = "3.3.4"
|
||||
insta = "1.19.1"
|
||||
manifest-dir-macros = "0.1.16"
|
||||
maplit = "1.0.2"
|
||||
meili-snap = {path = "../meili-snap"}
|
||||
|
||||
@@ -25,7 +25,9 @@ use uuid::Uuid;
|
||||
|
||||
use super::{config_user_id_path, DocumentDeletionKind, MEILISEARCH_CONFIG_PATH};
|
||||
use crate::analytics::Analytics;
|
||||
use crate::option::{default_http_addr, IndexerOpts, MaxMemory, MaxThreads, ScheduleSnapshot};
|
||||
use crate::option::{
|
||||
default_http_addr, IndexerOpts, MaxMemory, MaxThreads, RateLimiterConfig, SchedulerConfig,
|
||||
};
|
||||
use crate::routes::indexes::documents::UpdateDocumentsQuery;
|
||||
use crate::routes::tasks::TasksFilterQueryRaw;
|
||||
use crate::routes::{create_all_stats, Stats};
|
||||
@@ -220,12 +222,16 @@ struct Infos {
|
||||
ignore_missing_dump: bool,
|
||||
ignore_dump_if_db_exists: bool,
|
||||
import_snapshot: bool,
|
||||
schedule_snapshot: Option<u64>,
|
||||
schedule_snapshot: bool,
|
||||
snapshot_dir: bool,
|
||||
snapshot_interval_sec: u64,
|
||||
ignore_missing_snapshot: bool,
|
||||
ignore_snapshot_if_db_exists: bool,
|
||||
http_addr: bool,
|
||||
max_index_size: Byte,
|
||||
max_task_db_size: Byte,
|
||||
http_payload_size_limit: Byte,
|
||||
disable_auto_batching: bool,
|
||||
log_level: String,
|
||||
max_indexing_memory: MaxMemory,
|
||||
max_indexing_threads: MaxThreads,
|
||||
@@ -237,6 +243,16 @@ struct Infos {
|
||||
ssl_require_auth: bool,
|
||||
ssl_resumption: bool,
|
||||
ssl_tickets: bool,
|
||||
rate_limiting_disable_all: bool,
|
||||
rate_limiting_disable_global: bool,
|
||||
rate_limiting_global_pool: u32,
|
||||
rate_limiting_global_cooldown_ns: u64,
|
||||
rate_limiting_disable_ip: bool,
|
||||
rate_limiting_ip_pool: u32,
|
||||
rate_limiting_ip_cooldown_ns: u64,
|
||||
rate_limiting_disable_api_key: bool,
|
||||
rate_limiting_api_key_pool: u32,
|
||||
rate_limiting_api_key_cooldown_ns: u64,
|
||||
}
|
||||
|
||||
impl From<Opt> for Infos {
|
||||
@@ -249,8 +265,8 @@ impl From<Opt> for Infos {
|
||||
http_addr,
|
||||
master_key: _,
|
||||
env,
|
||||
max_index_size: _,
|
||||
max_task_db_size: _,
|
||||
max_index_size,
|
||||
max_task_db_size,
|
||||
http_payload_size_limit,
|
||||
ssl_cert_path,
|
||||
ssl_key_path,
|
||||
@@ -264,23 +280,40 @@ impl From<Opt> for Infos {
|
||||
ignore_snapshot_if_db_exists,
|
||||
snapshot_dir,
|
||||
schedule_snapshot,
|
||||
snapshot_interval_sec,
|
||||
import_dump,
|
||||
ignore_missing_dump,
|
||||
ignore_dump_if_db_exists,
|
||||
dump_dir,
|
||||
log_level,
|
||||
indexer_options,
|
||||
scheduler_options,
|
||||
config_file_path,
|
||||
generate_master_key: _,
|
||||
rate_limiter_options,
|
||||
#[cfg(all(not(debug_assertions), feature = "analytics"))]
|
||||
no_analytics: _,
|
||||
} = options;
|
||||
|
||||
let schedule_snapshot = match schedule_snapshot {
|
||||
ScheduleSnapshot::Disabled => None,
|
||||
ScheduleSnapshot::Enabled(interval) => Some(interval),
|
||||
};
|
||||
|
||||
let IndexerOpts { max_indexing_memory, max_indexing_threads } = indexer_options;
|
||||
let SchedulerConfig { disable_auto_batching } = scheduler_options;
|
||||
let IndexerOpts {
|
||||
log_every_n: _,
|
||||
max_nb_chunks: _,
|
||||
max_indexing_memory,
|
||||
max_indexing_threads,
|
||||
} = indexer_options;
|
||||
let RateLimiterConfig {
|
||||
rate_limiting_disable_all,
|
||||
rate_limiting_disable_global,
|
||||
rate_limiting_global_pool,
|
||||
rate_limiting_global_cooldown_ns,
|
||||
rate_limiting_disable_ip,
|
||||
rate_limiting_ip_pool,
|
||||
rate_limiting_ip_cooldown_ns,
|
||||
rate_limiting_disable_api_key,
|
||||
rate_limiting_api_key_pool,
|
||||
rate_limiting_api_key_cooldown_ns,
|
||||
} = rate_limiter_options;
|
||||
|
||||
// We're going to override every sensible information.
|
||||
// We consider information sensible if it contains a path, an address, or a key.
|
||||
@@ -294,11 +327,15 @@ impl From<Opt> for Infos {
|
||||
import_snapshot: import_snapshot.is_some(),
|
||||
schedule_snapshot,
|
||||
snapshot_dir: snapshot_dir != PathBuf::from("snapshots/"),
|
||||
snapshot_interval_sec,
|
||||
ignore_missing_snapshot,
|
||||
ignore_snapshot_if_db_exists,
|
||||
http_addr: http_addr != default_http_addr(),
|
||||
max_index_size,
|
||||
max_task_db_size,
|
||||
http_payload_size_limit,
|
||||
log_level: log_level.to_string(),
|
||||
disable_auto_batching,
|
||||
log_level,
|
||||
max_indexing_memory,
|
||||
max_indexing_threads,
|
||||
with_configuration_file: config_file_path.is_some(),
|
||||
@@ -309,6 +346,16 @@ impl From<Opt> for Infos {
|
||||
ssl_require_auth,
|
||||
ssl_resumption,
|
||||
ssl_tickets,
|
||||
rate_limiting_disable_all,
|
||||
rate_limiting_disable_global,
|
||||
rate_limiting_global_pool,
|
||||
rate_limiting_global_cooldown_ns,
|
||||
rate_limiting_disable_ip,
|
||||
rate_limiting_ip_pool,
|
||||
rate_limiting_ip_cooldown_ns,
|
||||
rate_limiting_disable_api_key,
|
||||
rate_limiting_api_key_pool,
|
||||
rate_limiting_api_key_cooldown_ns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ impl ErrorCode for MeilisearchHttpError {
|
||||
MeilisearchHttpError::DocumentNotFound(_) => Code::DocumentNotFound,
|
||||
MeilisearchHttpError::InvalidExpression(_, _) => Code::Filter,
|
||||
MeilisearchHttpError::PayloadTooLarge => Code::PayloadTooLarge,
|
||||
MeilisearchHttpError::SwapIndexPayloadWrongLength(_) => Code::InvalidSwapIndexes,
|
||||
MeilisearchHttpError::SwapIndexPayloadWrongLength(_) => Code::BadRequest,
|
||||
MeilisearchHttpError::IndexUid(e) => e.error_code(),
|
||||
MeilisearchHttpError::SerdeJson(_) => Code::Internal,
|
||||
MeilisearchHttpError::HeedError(_) => Code::Internal,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use deserr::{DeserializeError, DeserializeFromValue};
|
||||
use futures::ready;
|
||||
use meilisearch_types::error::{ErrorCode, ResponseError};
|
||||
|
||||
/// Extractor for typed data from Json request payloads
|
||||
/// deserialised by deserr.
|
||||
///
|
||||
/// # Extractor
|
||||
/// To extract typed data from a request body, the inner type `T` must implement the
|
||||
/// [`deserr::DeserializeFromError<E>`] trait. The inner type `E` must implement the
|
||||
/// [`ErrorCode`](meilisearch_error::ErrorCode) trait.
|
||||
#[derive(Debug)]
|
||||
pub struct ValidatedJson<T, E>(pub T, PhantomData<*const E>);
|
||||
|
||||
impl<T, E> ValidatedJson<T, E> {
|
||||
pub fn new(data: T) -> Self {
|
||||
ValidatedJson(data, PhantomData)
|
||||
}
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> FromRequest for ValidatedJson<T, E>
|
||||
where
|
||||
E: DeserializeError + ErrorCode + 'static,
|
||||
T: DeserializeFromValue<E>,
|
||||
{
|
||||
type Error = actix_web::Error;
|
||||
type Future = ValidatedJsonExtractFut<T, E>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
ValidatedJsonExtractFut {
|
||||
fut: Json::<serde_json::Value>::from_request(req, payload),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ValidatedJsonExtractFut<T, E> {
|
||||
fut: <Json<serde_json::Value> as FromRequest>::Future,
|
||||
_phantom: PhantomData<*const (T, E)>,
|
||||
}
|
||||
|
||||
impl<T, E> Future for ValidatedJsonExtractFut<T, E>
|
||||
where
|
||||
T: DeserializeFromValue<E>,
|
||||
E: DeserializeError + ErrorCode + 'static,
|
||||
{
|
||||
type Output = Result<ValidatedJson<T, E>, actix_web::Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let ValidatedJsonExtractFut { fut, .. } = self.get_mut();
|
||||
let fut = Pin::new(fut);
|
||||
|
||||
let res = ready!(fut.poll(cx));
|
||||
|
||||
let res = match res {
|
||||
Err(err) => Err(err),
|
||||
Ok(data) => match deserr::deserialize::<_, _, E>(data.into_inner()) {
|
||||
Ok(data) => Ok(ValidatedJson::new(data)),
|
||||
Err(e) => Err(ResponseError::from(e).into()),
|
||||
},
|
||||
};
|
||||
|
||||
Poll::Ready(res)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
pub mod payload;
|
||||
#[macro_use]
|
||||
pub mod authentication;
|
||||
pub mod json;
|
||||
pub mod query_parameters;
|
||||
pub mod sequential_extractor;
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
//! A module to parse query parameter with deserr
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use std::{fmt, ops};
|
||||
|
||||
use actix_http::Payload;
|
||||
use actix_utils::future::{err, ok, Ready};
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use deserr::{DeserializeError, DeserializeFromValue};
|
||||
use meilisearch_types::error::{Code, ErrorCode, ResponseError};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct QueryParameter<T, E>(pub T, PhantomData<*const E>);
|
||||
|
||||
impl<T, E> QueryParameter<T, E> {
|
||||
/// Unwrap into inner `T` value.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> QueryParameter<T, E>
|
||||
where
|
||||
T: DeserializeFromValue<E>,
|
||||
E: DeserializeError + ErrorCode + 'static,
|
||||
{
|
||||
pub fn from_query(query_str: &str) -> Result<Self, actix_web::Error> {
|
||||
let value = serde_urlencoded::from_str::<serde_json::Value>(query_str)
|
||||
.map_err(|e| ResponseError::from_msg(e.to_string(), Code::BadRequest))?;
|
||||
|
||||
match deserr::deserialize::<_, _, E>(value) {
|
||||
Ok(data) => Ok(QueryParameter(data, PhantomData)),
|
||||
Err(e) => Err(ResponseError::from(e).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ops::Deref for QueryParameter<T, E> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ops::DerefMut for QueryParameter<T, E> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Display, E> fmt::Display for QueryParameter<T, E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> FromRequest for QueryParameter<T, E>
|
||||
where
|
||||
T: DeserializeFromValue<E>,
|
||||
E: DeserializeError + ErrorCode + 'static,
|
||||
{
|
||||
type Error = actix_web::Error;
|
||||
type Future = Ready<Result<Self, actix_web::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
QueryParameter::from_query(req.query_string()).map(ok).unwrap_or_else(err)
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,19 @@ pub mod route_metrics;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_governor::{
|
||||
GlobalKeyExtractor, Governor, GovernorConfigBuilder, KeyExtractor, PeerIpKeyExtractor,
|
||||
};
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_web::dev::{ServiceFactory, ServiceResponse};
|
||||
use actix_web::error::JsonPayloadError;
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{middleware, web, HttpRequest};
|
||||
use analytics::Analytics;
|
||||
@@ -41,10 +46,12 @@ use meilisearch_types::tasks::KindWithContent;
|
||||
use meilisearch_types::versioning::{check_version_file, create_version_file};
|
||||
use meilisearch_types::{compression, milli, VERSION_FILE_NAME};
|
||||
pub use option::Opt;
|
||||
use option::ScheduleSnapshot;
|
||||
use option::RateLimiterConfig;
|
||||
|
||||
use crate::error::MeilisearchHttpError;
|
||||
|
||||
pub static AUTOBATCHING_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Check if a db is empty. It does not provide any information on the
|
||||
/// validity of the data in it.
|
||||
/// We consider a database as non empty when it's a non empty directory.
|
||||
@@ -76,6 +83,7 @@ pub fn create_app(
|
||||
InitError = (),
|
||||
>,
|
||||
> {
|
||||
let rate_limiters = configure_rate_limiters(&opt.rate_limiter_options);
|
||||
let app = actix_web::App::new()
|
||||
.configure(|s| {
|
||||
configure_data(
|
||||
@@ -86,7 +94,7 @@ pub fn create_app(
|
||||
analytics.clone(),
|
||||
)
|
||||
})
|
||||
.configure(routes::configure)
|
||||
.configure(|cfg| routes::configure(cfg, rate_limiters))
|
||||
.configure(|s| dashboard(s, enable_dashboard));
|
||||
#[cfg(feature = "metrics")]
|
||||
let app = app.configure(|s| configure_metrics_route(s, opt.enable_metrics_route));
|
||||
@@ -170,8 +178,8 @@ pub fn setup_meilisearch(opt: &Opt) -> anyhow::Result<(Arc<IndexScheduler>, Auth
|
||||
|
||||
// We create a loop in a thread that registers snapshotCreation tasks
|
||||
let index_scheduler = Arc::new(index_scheduler);
|
||||
if let ScheduleSnapshot::Enabled(snapshot_delay) = opt.schedule_snapshot {
|
||||
let snapshot_delay = Duration::from_secs(snapshot_delay);
|
||||
if opt.schedule_snapshot {
|
||||
let snapshot_delay = Duration::from_secs(opt.snapshot_interval_sec);
|
||||
let index_scheduler = index_scheduler.clone();
|
||||
thread::Builder::new()
|
||||
.name(String::from("register-snapshot-tasks"))
|
||||
@@ -207,7 +215,7 @@ fn open_or_create_database_unchecked(
|
||||
task_db_size: opt.max_task_db_size.get_bytes() as usize,
|
||||
index_size: opt.max_index_size.get_bytes() as usize,
|
||||
indexer_config: (&opt.indexer_options).try_into()?,
|
||||
autobatching_enabled: true,
|
||||
autobatching_enabled: !opt.scheduler_options.disable_auto_batching,
|
||||
})?)
|
||||
};
|
||||
|
||||
@@ -384,6 +392,123 @@ pub fn configure_data(
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper struct to implement rate-limiting depending on the API key.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ApiKeyExtractor;
|
||||
|
||||
impl KeyExtractor for ApiKeyExtractor {
|
||||
/// `Some(api_key)` for requests containing an API key, `None` otherwise
|
||||
type Key = Option<String>;
|
||||
|
||||
/// Error indicating that the request header could not be converted to a `String` representation.
|
||||
type KeyExtractionError = actix_http::header::ToStrError;
|
||||
|
||||
/// Extracts an API key from a request header, if one is present.
|
||||
///
|
||||
/// Returns Ok(None) if there is no authorization header.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - `Self::KeyExtractionError`: if an authorization header is present, but not representable as a `String` (e.g. non-UTF8)
|
||||
fn extract(
|
||||
&self,
|
||||
req: &actix_web::dev::ServiceRequest,
|
||||
) -> Result<Self::Key, Self::KeyExtractionError> {
|
||||
let key = req.headers().get("Authorization").map(|token| token.to_str()).transpose()?;
|
||||
Ok(key.and_then(|token| token.strip_prefix("Bearer ")).map(|key| key.trim().to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates a conditionally enabled rate-limiter.
|
||||
///
|
||||
/// This struct can be turned into an Actix middleware using [`Self::into_middleware`],
|
||||
/// allowing to add it to some routes.
|
||||
pub struct RateLimiter<K: KeyExtractor> {
|
||||
enabled: bool,
|
||||
governor: Governor<K>,
|
||||
}
|
||||
|
||||
/// The available rate limiters.
|
||||
pub struct RateLimiters {
|
||||
/// Limits globally regardless of the origin of the query.
|
||||
pub global: RateLimiter<GlobalKeyExtractor>,
|
||||
/// Limits depending on the IP address of origin.
|
||||
pub ip: RateLimiter<PeerIpKeyExtractor>,
|
||||
/// Limits depending on the API Key in the Authorization header.
|
||||
pub api_key: RateLimiter<ApiKeyExtractor>,
|
||||
}
|
||||
|
||||
impl<K: KeyExtractor> RateLimiter<K> {
|
||||
fn disabled(key_extractor: K) -> Self {
|
||||
let governor = Governor::new(
|
||||
&GovernorConfigBuilder::default()
|
||||
.methods(vec![])
|
||||
.key_extractor(key_extractor)
|
||||
.finish()
|
||||
.unwrap(),
|
||||
);
|
||||
Self { enabled: false, governor }
|
||||
}
|
||||
|
||||
fn enabled(key_extractor: K, pool_size: u32, cooldown_ns: u64) -> Self {
|
||||
let governor = Governor::new(
|
||||
&GovernorConfigBuilder::default()
|
||||
.key_extractor(key_extractor)
|
||||
.burst_size(pool_size)
|
||||
.per_nanosecond(cooldown_ns)
|
||||
.use_headers()
|
||||
.finish()
|
||||
.unwrap(),
|
||||
);
|
||||
Self { enabled: true, governor }
|
||||
}
|
||||
|
||||
/// Turns this into a middleware that is enabled only if the rate limiter was enabled.
|
||||
pub fn into_middleware(self) -> Condition<Governor<K>> {
|
||||
Condition::new(self.enabled, self.governor)
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_rate_limiters(rate_limiter_options: &RateLimiterConfig) -> RateLimiters {
|
||||
if rate_limiter_options.rate_limiting_disable_all {
|
||||
return RateLimiters {
|
||||
global: RateLimiter::disabled(GlobalKeyExtractor),
|
||||
ip: RateLimiter::disabled(PeerIpKeyExtractor),
|
||||
api_key: RateLimiter::disabled(ApiKeyExtractor),
|
||||
};
|
||||
}
|
||||
let global = if rate_limiter_options.rate_limiting_disable_global {
|
||||
RateLimiter::disabled(GlobalKeyExtractor)
|
||||
} else {
|
||||
RateLimiter::enabled(
|
||||
GlobalKeyExtractor,
|
||||
rate_limiter_options.rate_limiting_global_pool,
|
||||
rate_limiter_options.rate_limiting_global_cooldown_ns,
|
||||
)
|
||||
};
|
||||
|
||||
let ip = if rate_limiter_options.rate_limiting_disable_ip {
|
||||
RateLimiter::disabled(PeerIpKeyExtractor)
|
||||
} else {
|
||||
RateLimiter::enabled(
|
||||
PeerIpKeyExtractor,
|
||||
rate_limiter_options.rate_limiting_ip_pool,
|
||||
rate_limiter_options.rate_limiting_ip_cooldown_ns,
|
||||
)
|
||||
};
|
||||
|
||||
let api_key = if rate_limiter_options.rate_limiting_disable_api_key {
|
||||
RateLimiter::disabled(ApiKeyExtractor)
|
||||
} else {
|
||||
RateLimiter::enabled(
|
||||
ApiKeyExtractor,
|
||||
rate_limiter_options.rate_limiting_api_key_pool,
|
||||
rate_limiter_options.rate_limiting_api_key_cooldown_ns,
|
||||
)
|
||||
};
|
||||
RateLimiters { global, ip, api_key }
|
||||
}
|
||||
|
||||
#[cfg(feature = "mini-dashboard")]
|
||||
pub fn dashboard(config: &mut web::ServiceConfig, enable_frontend: bool) {
|
||||
use actix_web::HttpResponse;
|
||||
|
||||
@@ -16,7 +16,11 @@ static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
/// does all the setup before meilisearch is launched
|
||||
fn setup(opt: &Opt) -> anyhow::Result<()> {
|
||||
let mut log_builder = env_logger::Builder::new();
|
||||
log_builder.parse_filters(&opt.log_level.to_string());
|
||||
log_builder.parse_filters(&opt.log_level);
|
||||
if opt.log_level == "info" {
|
||||
// if we are in info we only allow the warn log_level for milli
|
||||
log_builder.filter_module("milli", log::LevelFilter::Warn);
|
||||
}
|
||||
|
||||
log_builder.init();
|
||||
|
||||
@@ -29,12 +33,17 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
setup(&opt)?;
|
||||
|
||||
if opt.generate_master_key {
|
||||
println!("{}", generate_master_key());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match (opt.env.as_ref(), &opt.master_key) {
|
||||
("production", Some(master_key)) if master_key.len() < MASTER_KEY_MIN_SIZE => {
|
||||
anyhow::bail!(
|
||||
"In production mode, the master key must be of at least {MASTER_KEY_MIN_SIZE} bytes, but the provided key is only {} bytes long
|
||||
"In production mode, the master key must be of at least {MASTER_KEY_MIN_SIZE} characters, but the provided key is only {} characters long
|
||||
|
||||
We generated a secure master key for you (you can safely copy this token):
|
||||
We generated a secure Master Key for you (you can safely copy this token):
|
||||
|
||||
>> export MEILI_MASTER_KEY={} <<",
|
||||
master_key.len(),
|
||||
@@ -45,7 +54,7 @@ We generated a secure master key for you (you can safely copy this token):
|
||||
anyhow::bail!(
|
||||
"In production mode, you must provide a master key to secure your instance. It can be specified via the MEILI_MASTER_KEY environment variable or the --master-key launch option.
|
||||
|
||||
We generated a secure master key for you (you can safely copy this token):
|
||||
We generated a secure Master Key for you (you can safely copy this token):
|
||||
|
||||
>> export MEILI_MASTER_KEY={} <<
|
||||
",
|
||||
@@ -164,21 +173,24 @@ Anonymous telemetry:\t\"Enabled\""
|
||||
|
||||
match (opt.env.as_ref(), &opt.master_key) {
|
||||
("production", Some(_)) => {
|
||||
eprintln!("A master key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key.");
|
||||
eprintln!("A Master Key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key.");
|
||||
}
|
||||
("development", Some(master_key)) => {
|
||||
eprintln!("A master key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key.");
|
||||
eprintln!("A Master Key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key.");
|
||||
|
||||
if master_key.len() < MASTER_KEY_MIN_SIZE {
|
||||
eprintln!();
|
||||
log::warn!("The provided master key is too short (< {MASTER_KEY_MIN_SIZE} bytes)");
|
||||
eprintln!("A master key of at least {MASTER_KEY_MIN_SIZE} bytes will be required when switching to the production environment.");
|
||||
log::warn!(
|
||||
"The provided Master Key is too short (< {MASTER_KEY_MIN_SIZE} characters)"
|
||||
);
|
||||
eprintln!("A Master Key of at least {MASTER_KEY_MIN_SIZE} characters will be required when switching to the production environment.");
|
||||
eprintln!("Restart Meilisearch with the `--generate-master-key` flag to generate a secure Master Key you can use");
|
||||
}
|
||||
}
|
||||
("development", None) => {
|
||||
log::warn!("No master key found; The server will accept unidentified requests");
|
||||
eprintln!("If you need some protection in development mode, please export a key:\n\nexport MEILI_MASTER_KEY={}", generate_master_key());
|
||||
eprintln!("\nA master key of at least {MASTER_KEY_MIN_SIZE} bytes will be required when switching to the production environment.");
|
||||
eprintln!("\nA Master Key of at least {MASTER_KEY_MIN_SIZE} characters will be required when switching to the production environment.");
|
||||
}
|
||||
// unreachable because Opt::try_build above would have failed already if any other value had been produced
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::env::VarError;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Display;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::num::ParseIntError;
|
||||
use std::ops::Deref;
|
||||
@@ -29,6 +28,8 @@ const MEILI_MASTER_KEY: &str = "MEILI_MASTER_KEY";
|
||||
const MEILI_ENV: &str = "MEILI_ENV";
|
||||
#[cfg(all(not(debug_assertions), feature = "analytics"))]
|
||||
const MEILI_NO_ANALYTICS: &str = "MEILI_NO_ANALYTICS";
|
||||
const MEILI_MAX_INDEX_SIZE: &str = "MEILI_MAX_INDEX_SIZE";
|
||||
const MEILI_MAX_TASK_DB_SIZE: &str = "MEILI_MAX_TASK_DB_SIZE";
|
||||
const MEILI_HTTP_PAYLOAD_SIZE_LIMIT: &str = "MEILI_HTTP_PAYLOAD_SIZE_LIMIT";
|
||||
const MEILI_SSL_CERT_PATH: &str = "MEILI_SSL_CERT_PATH";
|
||||
const MEILI_SSL_KEY_PATH: &str = "MEILI_SSL_KEY_PATH";
|
||||
@@ -42,11 +43,29 @@ const MEILI_IGNORE_MISSING_SNAPSHOT: &str = "MEILI_IGNORE_MISSING_SNAPSHOT";
|
||||
const MEILI_IGNORE_SNAPSHOT_IF_DB_EXISTS: &str = "MEILI_IGNORE_SNAPSHOT_IF_DB_EXISTS";
|
||||
const MEILI_SNAPSHOT_DIR: &str = "MEILI_SNAPSHOT_DIR";
|
||||
const MEILI_SCHEDULE_SNAPSHOT: &str = "MEILI_SCHEDULE_SNAPSHOT";
|
||||
const MEILI_SNAPSHOT_INTERVAL_SEC: &str = "MEILI_SNAPSHOT_INTERVAL_SEC";
|
||||
const MEILI_IMPORT_DUMP: &str = "MEILI_IMPORT_DUMP";
|
||||
const MEILI_IGNORE_MISSING_DUMP: &str = "MEILI_IGNORE_MISSING_DUMP";
|
||||
const MEILI_IGNORE_DUMP_IF_DB_EXISTS: &str = "MEILI_IGNORE_DUMP_IF_DB_EXISTS";
|
||||
const MEILI_DUMP_DIR: &str = "MEILI_DUMP_DIR";
|
||||
const MEILI_LOG_LEVEL: &str = "MEILI_LOG_LEVEL";
|
||||
const MEILI_GENERATE_MASTER_KEY: &str = "MEILI_GENERATE_MASTER_KEY";
|
||||
|
||||
// rate limiting
|
||||
|
||||
const MEILI_RATE_LIMITING_DISABLE_ALL: &str = "MEILI_RATE_LIMITING_DISABLE_ALL";
|
||||
const MEILI_RATE_LIMITING_DISABLE_GLOBAL: &str = "MEILI_RATE_LIMITING_DISABLE_GLOBAL";
|
||||
const MEILI_RATE_LIMITING_DISABLE_IP: &str = "MEILI_RATE_LIMITING_DISABLE_IP";
|
||||
const MEILI_RATE_LIMITING_DISABLE_API_KEY: &str = "MEILI_RATE_LIMITING_DISABLE_API_KEY";
|
||||
|
||||
const MEILI_RATE_LIMITING_GLOBAL_POOL: &str = "MEILI_RATE_LIMITING_GLOBAL_POOL";
|
||||
const MEILI_RATE_LIMITING_IP_POOL: &str = "MEILI_RATE_LIMITING_IP_POOL";
|
||||
const MEILI_RATE_LIMITING_API_KEY_POOL: &str = "MEILI_RATE_LIMITING_API_KEY_POOL";
|
||||
|
||||
const MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS";
|
||||
const MEILI_RATE_LIMITING_IP_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_IP_COOLDOWN_NS";
|
||||
const MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS: &str = "MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS";
|
||||
|
||||
#[cfg(feature = "metrics")]
|
||||
const MEILI_ENABLE_METRICS_ROUTE: &str = "MEILI_ENABLE_METRICS_ROUTE";
|
||||
|
||||
@@ -54,80 +73,27 @@ const DEFAULT_CONFIG_FILE_PATH: &str = "./config.toml";
|
||||
const DEFAULT_DB_PATH: &str = "./data.ms";
|
||||
const DEFAULT_HTTP_ADDR: &str = "localhost:7700";
|
||||
const DEFAULT_ENV: &str = "development";
|
||||
const DEFAULT_MAX_INDEX_SIZE: &str = "100 GiB";
|
||||
const DEFAULT_MAX_TASK_DB_SIZE: &str = "100 GiB";
|
||||
const DEFAULT_HTTP_PAYLOAD_SIZE_LIMIT: &str = "100 MB";
|
||||
const DEFAULT_SNAPSHOT_DIR: &str = "snapshots/";
|
||||
const DEFAULT_SNAPSHOT_INTERVAL_SEC: u64 = 86400;
|
||||
const DEFAULT_SNAPSHOT_INTERVAL_SEC_STR: &str = "86400";
|
||||
const DEFAULT_DUMP_DIR: &str = "dumps/";
|
||||
const DEFAULT_LOG_LEVEL: &str = "INFO";
|
||||
|
||||
const MEILI_MAX_INDEXING_MEMORY: &str = "MEILI_MAX_INDEXING_MEMORY";
|
||||
const MEILI_MAX_INDEXING_THREADS: &str = "MEILI_MAX_INDEXING_THREADS";
|
||||
const DEFAULT_LOG_EVERY_N: usize = 100_000;
|
||||
const DISABLE_AUTO_BATCHING: &str = "DISABLE_AUTO_BATCHING";
|
||||
const DEFAULT_LOG_EVERY_N: usize = 100000;
|
||||
|
||||
// Each environment (index and task-db) is taking space in the virtual address space.
|
||||
//
|
||||
// The size of the virtual address space is limited by the OS. About 100TB for Linux and about 10TB for Windows.
|
||||
// This means that the number of indexes is limited to about 200 for Linux and about 20 for Windows.
|
||||
pub const INDEX_SIZE: u64 = 536_870_912_000; // 500 GiB
|
||||
pub const TASK_DB_SIZE: u64 = 10_737_418_240; // 10 GiB
|
||||
const DEFAULT_GLOBAL_RATE_LIMITING_POOL: u32 = 100_000;
|
||||
const DEFAULT_GLOBAL_RATE_LIMITING_COOLDOWN_NS: u64 = 50_000; // pool replenishes in 5s
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum LogLevel {
|
||||
Off,
|
||||
Error,
|
||||
Warn,
|
||||
#[default]
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
const DEFAULT_IP_RATE_LIMITING_POOL: u32 = 200;
|
||||
const DEFAULT_IP_RATE_LIMITING_COOLDOWN_NS: u64 = 50_000_000; // pool replenishes in 10s
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LogLevelError {
|
||||
pub given_log_level: String,
|
||||
}
|
||||
|
||||
impl Display for LogLevelError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(
|
||||
f,
|
||||
"Log level '{}' is invalid. Accepted values are 'OFF', 'ERROR', 'WARN', 'INFO', 'DEBUG', and 'TRACE'.",
|
||||
self.given_log_level
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LogLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
LogLevel::Off => Display::fmt("OFF", f),
|
||||
LogLevel::Error => Display::fmt("ERROR", f),
|
||||
LogLevel::Warn => Display::fmt("WARN", f),
|
||||
LogLevel::Info => Display::fmt("INFO", f),
|
||||
LogLevel::Debug => Display::fmt("DEBUG", f),
|
||||
LogLevel::Trace => Display::fmt("TRACE", f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LogLevelError {}
|
||||
|
||||
impl FromStr for LogLevel {
|
||||
type Err = LogLevelError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"off" => Ok(LogLevel::Off),
|
||||
"error" => Ok(LogLevel::Error),
|
||||
"warn" => Ok(LogLevel::Warn),
|
||||
"info" => Ok(LogLevel::Info),
|
||||
"debug" => Ok(LogLevel::Debug),
|
||||
"trace" => Ok(LogLevel::Trace),
|
||||
_ => Err(LogLevelError { given_log_level: s.to_owned() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
const DEFAULT_API_KEY_RATE_LIMITING_POOL: u32 = 10_000;
|
||||
const DEFAULT_API_KEY_RATE_LIMITING_COOLDOWN_NS: u64 = 500_000; // pool replenishes in 10s
|
||||
|
||||
#[derive(Debug, Clone, Parser, Deserialize)]
|
||||
#[clap(version, next_display_order = None)]
|
||||
@@ -163,14 +129,14 @@ pub struct Opt {
|
||||
pub no_analytics: bool,
|
||||
|
||||
/// Sets the maximum size of the index. Value must be given in bytes or explicitly stating a base unit (for instance: 107374182400, '107.7Gb', or '107374 Mb').
|
||||
#[clap(skip = default_max_index_size())]
|
||||
#[serde(skip, default = "default_max_index_size")]
|
||||
#[clap(long, env = MEILI_MAX_INDEX_SIZE, default_value_t = default_max_index_size())]
|
||||
#[serde(default = "default_max_index_size")]
|
||||
pub max_index_size: Byte,
|
||||
|
||||
/// Sets the maximum size of the task database. Value must be given in bytes or explicitly stating a
|
||||
/// base unit (for instance: 107374182400, '107.7Gb', or '107374 Mb').
|
||||
#[clap(skip = default_max_task_db_size())]
|
||||
#[serde(skip, default = "default_max_task_db_size")]
|
||||
#[clap(long, env = MEILI_MAX_TASK_DB_SIZE, default_value_t = default_max_task_db_size())]
|
||||
#[serde(default = "default_max_task_db_size")]
|
||||
pub max_task_db_size: Byte,
|
||||
|
||||
/// Sets the maximum size of accepted payloads. Value must be given in bytes or explicitly stating a
|
||||
@@ -247,11 +213,14 @@ pub struct Opt {
|
||||
pub snapshot_dir: PathBuf,
|
||||
|
||||
/// Activates scheduled snapshots when provided. Snapshots are disabled by default.
|
||||
///
|
||||
/// When provided with a value, defines the interval between each snapshot, in seconds.
|
||||
#[clap(long,env = MEILI_SCHEDULE_SNAPSHOT, num_args(0..=1), value_parser=parse_schedule_snapshot, default_value_t, default_missing_value=default_snapshot_interval_sec(), value_name = "SNAPSHOT_INTERVAL_SEC")]
|
||||
#[serde(default, deserialize_with = "schedule_snapshot_deserialize")]
|
||||
pub schedule_snapshot: ScheduleSnapshot,
|
||||
#[clap(long, env = MEILI_SCHEDULE_SNAPSHOT)]
|
||||
#[serde(default)]
|
||||
pub schedule_snapshot: bool,
|
||||
|
||||
/// Defines the interval between each snapshot. Value must be given in seconds.
|
||||
#[clap(long, env = MEILI_SNAPSHOT_INTERVAL_SEC, default_value_t = default_snapshot_interval_sec())]
|
||||
#[serde(default = "default_snapshot_interval_sec")]
|
||||
pub snapshot_interval_sec: u64,
|
||||
|
||||
/// Imports the dump file located at the specified path. Path must point to a `.dump` file.
|
||||
/// If a database already exists, Meilisearch will throw an error and abort launch.
|
||||
@@ -282,10 +251,17 @@ pub struct Opt {
|
||||
|
||||
/// Defines how much detail should be present in Meilisearch's logs.
|
||||
///
|
||||
/// Meilisearch currently supports six log levels, listed in order of increasing verbosity: OFF, ERROR, WARN, INFO, DEBUG, TRACE.
|
||||
#[clap(long, env = MEILI_LOG_LEVEL, default_value_t)]
|
||||
/// Meilisearch currently supports five log levels, listed in order of increasing verbosity: ERROR, WARN, INFO, DEBUG, TRACE.
|
||||
#[clap(long, env = MEILI_LOG_LEVEL, default_value_t = default_log_level())]
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: String,
|
||||
|
||||
/// Generates a string of characters that can be used as a master key and exits.
|
||||
///
|
||||
/// Pass the generated master key using the `--master-key` argument or the `MEILI_MASTER_KEY` environment variable in a subsequent Meilisearch invocation.
|
||||
#[clap(long, env = MEILI_GENERATE_MASTER_KEY)]
|
||||
#[serde(default)]
|
||||
pub log_level: LogLevel,
|
||||
pub generate_master_key: bool,
|
||||
|
||||
/// Enables Prometheus metrics and /metrics route.
|
||||
#[cfg(feature = "metrics")]
|
||||
@@ -297,6 +273,14 @@ pub struct Opt {
|
||||
#[clap(flatten)]
|
||||
pub indexer_options: IndexerOpts,
|
||||
|
||||
#[serde(flatten)]
|
||||
#[clap(flatten)]
|
||||
pub scheduler_options: SchedulerConfig,
|
||||
|
||||
#[serde(flatten)]
|
||||
#[clap(flatten)]
|
||||
pub rate_limiter_options: RateLimiterConfig,
|
||||
|
||||
/// Set the path to a configuration file that should be used to setup the engine.
|
||||
/// Format must be TOML.
|
||||
#[clap(long)]
|
||||
@@ -360,8 +344,8 @@ impl Opt {
|
||||
http_addr,
|
||||
master_key,
|
||||
env,
|
||||
max_index_size: _,
|
||||
max_task_db_size: _,
|
||||
max_index_size,
|
||||
max_task_db_size,
|
||||
http_payload_size_limit,
|
||||
ssl_cert_path,
|
||||
ssl_key_path,
|
||||
@@ -372,16 +356,20 @@ impl Opt {
|
||||
ssl_tickets,
|
||||
snapshot_dir,
|
||||
schedule_snapshot,
|
||||
snapshot_interval_sec,
|
||||
dump_dir,
|
||||
log_level,
|
||||
indexer_options,
|
||||
scheduler_options,
|
||||
import_snapshot: _,
|
||||
ignore_missing_snapshot: _,
|
||||
ignore_snapshot_if_db_exists: _,
|
||||
import_dump: _,
|
||||
generate_master_key: _,
|
||||
ignore_missing_dump: _,
|
||||
ignore_dump_if_db_exists: _,
|
||||
config_file_path: _,
|
||||
rate_limiter_options,
|
||||
#[cfg(all(not(debug_assertions), feature = "analytics"))]
|
||||
no_analytics,
|
||||
#[cfg(feature = "metrics")]
|
||||
@@ -397,6 +385,8 @@ impl Opt {
|
||||
{
|
||||
export_to_env_if_not_present(MEILI_NO_ANALYTICS, no_analytics.to_string());
|
||||
}
|
||||
export_to_env_if_not_present(MEILI_MAX_INDEX_SIZE, max_index_size.to_string());
|
||||
export_to_env_if_not_present(MEILI_MAX_TASK_DB_SIZE, max_task_db_size.to_string());
|
||||
export_to_env_if_not_present(
|
||||
MEILI_HTTP_PAYLOAD_SIZE_LIMIT,
|
||||
http_payload_size_limit.to_string(),
|
||||
@@ -417,12 +407,13 @@ impl Opt {
|
||||
export_to_env_if_not_present(MEILI_SSL_RESUMPTION, ssl_resumption.to_string());
|
||||
export_to_env_if_not_present(MEILI_SSL_TICKETS, ssl_tickets.to_string());
|
||||
export_to_env_if_not_present(MEILI_SNAPSHOT_DIR, snapshot_dir);
|
||||
if let Some(snapshot_interval) = schedule_snapshot_to_env(schedule_snapshot) {
|
||||
export_to_env_if_not_present(MEILI_SCHEDULE_SNAPSHOT, snapshot_interval)
|
||||
}
|
||||
|
||||
export_to_env_if_not_present(MEILI_SCHEDULE_SNAPSHOT, schedule_snapshot.to_string());
|
||||
export_to_env_if_not_present(
|
||||
MEILI_SNAPSHOT_INTERVAL_SEC,
|
||||
snapshot_interval_sec.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(MEILI_DUMP_DIR, dump_dir);
|
||||
export_to_env_if_not_present(MEILI_LOG_LEVEL, log_level.to_string());
|
||||
export_to_env_if_not_present(MEILI_LOG_LEVEL, log_level);
|
||||
#[cfg(feature = "metrics")]
|
||||
{
|
||||
export_to_env_if_not_present(
|
||||
@@ -431,6 +422,8 @@ impl Opt {
|
||||
);
|
||||
}
|
||||
indexer_options.export_to_env();
|
||||
scheduler_options.export_to_env();
|
||||
rate_limiter_options.export_to_env();
|
||||
}
|
||||
|
||||
pub fn get_ssl_config(&self) -> anyhow::Result<Option<rustls::ServerConfig>> {
|
||||
@@ -480,8 +473,18 @@ impl Opt {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Parser, Deserialize)]
|
||||
#[derive(Debug, Clone, Parser, Deserialize)]
|
||||
pub struct IndexerOpts {
|
||||
/// Sets the amount of documents to skip before printing
|
||||
/// a log regarding the indexing advancement.
|
||||
#[serde(default = "default_log_every_n")]
|
||||
#[clap(long, default_value_t = default_log_every_n(), hide = true)] // 100k
|
||||
pub log_every_n: usize,
|
||||
|
||||
/// Grenad max number of chunks in bytes.
|
||||
#[clap(long, hide = true)]
|
||||
pub max_nb_chunks: Option<usize>,
|
||||
|
||||
/// Sets the maximum amount of RAM Meilisearch can use when indexing. By default, Meilisearch
|
||||
/// uses no more than two thirds of available memory.
|
||||
#[clap(long, env = MEILI_MAX_INDEXING_MEMORY, default_value_t)]
|
||||
@@ -499,7 +502,12 @@ pub struct IndexerOpts {
|
||||
impl IndexerOpts {
|
||||
/// Exports the values to their corresponding env vars if they are not set.
|
||||
pub fn export_to_env(self) {
|
||||
let IndexerOpts { max_indexing_memory, max_indexing_threads } = self;
|
||||
let IndexerOpts {
|
||||
max_indexing_memory,
|
||||
max_indexing_threads,
|
||||
log_every_n: _,
|
||||
max_nb_chunks: _,
|
||||
} = self;
|
||||
if let Some(max_indexing_memory) = max_indexing_memory.0 {
|
||||
export_to_env_if_not_present(
|
||||
MEILI_MAX_INDEXING_MEMORY,
|
||||
@@ -513,6 +521,22 @@ impl IndexerOpts {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Parser, Default, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub struct SchedulerConfig {
|
||||
/// Deactivates auto-batching when provided.
|
||||
#[clap(long, env = DISABLE_AUTO_BATCHING)]
|
||||
#[serde(default)]
|
||||
pub disable_auto_batching: bool,
|
||||
}
|
||||
|
||||
impl SchedulerConfig {
|
||||
pub fn export_to_env(self) {
|
||||
let SchedulerConfig { disable_auto_batching } = self;
|
||||
export_to_env_if_not_present(DISABLE_AUTO_BATCHING, disable_auto_batching.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&IndexerOpts> for IndexerConfig {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
@@ -523,7 +547,8 @@ impl TryFrom<&IndexerOpts> for IndexerConfig {
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
log_every_n: Some(DEFAULT_LOG_EVERY_N),
|
||||
log_every_n: Some(other.log_every_n),
|
||||
max_nb_chunks: other.max_nb_chunks,
|
||||
max_memory: other.max_indexing_memory.map(|b| b.get_bytes() as usize),
|
||||
thread_pool: Some(thread_pool),
|
||||
max_positions_per_attributes: None,
|
||||
@@ -532,6 +557,153 @@ impl TryFrom<&IndexerOpts> for IndexerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IndexerOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_every_n: 100_000,
|
||||
max_nb_chunks: None,
|
||||
max_indexing_memory: MaxMemory::default(),
|
||||
max_indexing_threads: MaxThreads::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options related to the configuration of the rate limiters.
|
||||
#[derive(Debug, Clone, Parser, Default, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub struct RateLimiterConfig {
|
||||
/// When provided, completely disables all rate limiting.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_ALL)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_all: bool,
|
||||
|
||||
/// When provided, disables the global rate limiting that applies to all search requests.
|
||||
///
|
||||
/// Disabling the global rate limiting does not disable IP-based and API-key-based rate limitings.
|
||||
/// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_GLOBAL)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_global: bool,
|
||||
/// The maximum pool of search requests that can be performed before they are rejected.
|
||||
///
|
||||
/// The pool starts full at the provided value, then each search request diminishes the pool by 1.
|
||||
/// When the pool is empty the search request is rejected.
|
||||
/// The pool is replenished by 1 depending on the cooldown period.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_GLOBAL_POOL, default_value_t = default_rate_limiting_global_pool())]
|
||||
#[serde(default = "default_rate_limiting_global_pool")]
|
||||
pub rate_limiting_global_pool: u32,
|
||||
/// The amount of time, in nanoseconds, before the pool of available search requests is replenished by 1 again.
|
||||
///
|
||||
/// The maximum number of available search requests is given by `--rate-limiting-global-pool`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS, default_value_t = default_rate_limiting_global_cooldown_ns())]
|
||||
#[serde(default = "default_rate_limiting_global_cooldown_ns")]
|
||||
pub rate_limiting_global_cooldown_ns: u64,
|
||||
|
||||
/// When provided, disables the rate limiting that applies to all search requests originating with a specific IP address.
|
||||
///
|
||||
/// Disabling the IP rate limiting does not disable the rate limiting that applies to all requests ("global") nor the API-key-based rate limiting.
|
||||
/// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_IP)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_ip: bool,
|
||||
/// The maximum pool of search requests that can be performed from a specific IP before they are rejected.
|
||||
///
|
||||
/// The pool starts full at the provided value, then each search request from the same IP address diminishes the pool by 1.
|
||||
/// When the pool is empty the search request is rejected.
|
||||
/// The pool is replenished by 1 depending on the cooldown period.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_IP_POOL, default_value_t = default_rate_limiting_ip_pool())]
|
||||
#[serde(default = "default_rate_limiting_ip_pool")]
|
||||
pub rate_limiting_ip_pool: u32,
|
||||
/// The amount of time, in nanoseconds, before the pool of available search requests for a specific IP address is replenished by 1 again.
|
||||
///
|
||||
/// The maximum number of available search requests for a specific IP address is given by `--rate-limiting-ip-pool`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_IP_COOLDOWN_NS, default_value_t = default_rate_limiting_ip_cooldown_ns())]
|
||||
#[serde(default = "default_rate_limiting_ip_cooldown_ns")]
|
||||
pub rate_limiting_ip_cooldown_ns: u64,
|
||||
|
||||
/// When provided, disables the rate limiting that applies to all search requests originating with a specific API key.
|
||||
///
|
||||
/// Disabling the API key limiting does not disable the rate limiting that applies to all requests ("global") nor the IP-based rate limiting.
|
||||
/// To disable all rate limiting regardless of the origin use `--rate-limiting-disable-all`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_DISABLE_API_KEY)]
|
||||
#[serde(default)]
|
||||
pub rate_limiting_disable_api_key: bool,
|
||||
/// The maximum pool of search requests that can be performed using a specific API key before they are rejected.
|
||||
///
|
||||
/// The pool starts full at the provided value, then each search request using the same API key diminishes the pool by 1.
|
||||
/// When the pool is empty the search request is rejected.
|
||||
/// The pool is replenished by 1 depending on the cooldown period.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_API_KEY_POOL, default_value_t = default_rate_limiting_api_key_pool())]
|
||||
#[serde(default = "default_rate_limiting_api_key_pool")]
|
||||
pub rate_limiting_api_key_pool: u32,
|
||||
/// The amount of time, in nanoseconds, before the pool of available search requests using a specific API key is replenished by 1 again.
|
||||
///
|
||||
/// The maximum number of available search requests using a specific API key is given by `--rate-limiting-api-key-pool`.
|
||||
#[clap(long, env = MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS, default_value_t = default_rate_limiting_api_key_cooldown_ns())]
|
||||
#[serde(default = "default_rate_limiting_api_key_cooldown_ns")]
|
||||
pub rate_limiting_api_key_cooldown_ns: u64,
|
||||
}
|
||||
|
||||
impl RateLimiterConfig {
|
||||
/// Exports the values to their corresponding env vars if they are not set.
|
||||
pub fn export_to_env(self) {
|
||||
let RateLimiterConfig {
|
||||
rate_limiting_disable_all: disable_rate_limiting,
|
||||
rate_limiting_disable_global: disable_global_rate_limiting,
|
||||
rate_limiting_global_pool: global_rate_limiting_pool,
|
||||
rate_limiting_global_cooldown_ns: global_rate_limiting_cooldown_ns,
|
||||
rate_limiting_disable_ip: disable_ip_rate_limiting,
|
||||
rate_limiting_ip_pool: ip_rate_limiting_pool,
|
||||
rate_limiting_ip_cooldown_ns: ip_rate_limiting_cooldown_ns,
|
||||
rate_limiting_disable_api_key: disable_api_key_rate_limiting,
|
||||
rate_limiting_api_key_pool: api_key_rate_limiting_pool,
|
||||
rate_limiting_api_key_cooldown_ns: api_key_rate_limiting_cooldown_ns,
|
||||
} = self;
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_ALL,
|
||||
disable_rate_limiting.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_GLOBAL,
|
||||
disable_global_rate_limiting.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_IP,
|
||||
disable_ip_rate_limiting.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_DISABLE_API_KEY,
|
||||
disable_api_key_rate_limiting.to_string(),
|
||||
);
|
||||
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_GLOBAL_POOL,
|
||||
global_rate_limiting_pool.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_IP_POOL,
|
||||
ip_rate_limiting_pool.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_API_KEY_POOL,
|
||||
api_key_rate_limiting_pool.to_string(),
|
||||
);
|
||||
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_GLOBAL_COOLDOWN_NS,
|
||||
global_rate_limiting_cooldown_ns.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_IP_COOLDOWN_NS,
|
||||
ip_rate_limiting_cooldown_ns.to_string(),
|
||||
);
|
||||
export_to_env_if_not_present(
|
||||
MEILI_RATE_LIMITING_API_KEY_COOLDOWN_NS,
|
||||
api_key_rate_limiting_cooldown_ns.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A type used to detect the max memory available and use 2/3 of it.
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct MaxMemory(Option<Byte>);
|
||||
@@ -693,11 +865,11 @@ fn default_env() -> String {
|
||||
}
|
||||
|
||||
fn default_max_index_size() -> Byte {
|
||||
Byte::from_bytes(INDEX_SIZE)
|
||||
Byte::from_str(DEFAULT_MAX_INDEX_SIZE).unwrap()
|
||||
}
|
||||
|
||||
fn default_max_task_db_size() -> Byte {
|
||||
Byte::from_bytes(TASK_DB_SIZE)
|
||||
Byte::from_str(DEFAULT_MAX_TASK_DB_SIZE).unwrap()
|
||||
}
|
||||
|
||||
fn default_http_payload_size_limit() -> Byte {
|
||||
@@ -708,108 +880,44 @@ fn default_snapshot_dir() -> PathBuf {
|
||||
PathBuf::from(DEFAULT_SNAPSHOT_DIR)
|
||||
}
|
||||
|
||||
fn default_snapshot_interval_sec() -> &'static str {
|
||||
DEFAULT_SNAPSHOT_INTERVAL_SEC_STR
|
||||
fn default_snapshot_interval_sec() -> u64 {
|
||||
DEFAULT_SNAPSHOT_INTERVAL_SEC
|
||||
}
|
||||
|
||||
fn default_dump_dir() -> PathBuf {
|
||||
PathBuf::from(DEFAULT_DUMP_DIR)
|
||||
}
|
||||
|
||||
/// Indicates if a snapshot was scheduled, and if yes with which interval.
|
||||
#[derive(Debug, Default, Copy, Clone, Deserialize, Serialize)]
|
||||
pub enum ScheduleSnapshot {
|
||||
/// Scheduled snapshots are disabled.
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Snapshots are scheduled at the specified interval, in seconds.
|
||||
Enabled(u64),
|
||||
fn default_log_level() -> String {
|
||||
DEFAULT_LOG_LEVEL.to_string()
|
||||
}
|
||||
|
||||
impl Display for ScheduleSnapshot {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ScheduleSnapshot::Disabled => write!(f, ""),
|
||||
ScheduleSnapshot::Enabled(value) => write!(f, "{}", value),
|
||||
}
|
||||
}
|
||||
fn default_log_every_n() -> usize {
|
||||
DEFAULT_LOG_EVERY_N
|
||||
}
|
||||
|
||||
impl FromStr for ScheduleSnapshot {
|
||||
type Err = ParseIntError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"" => ScheduleSnapshot::Disabled,
|
||||
s => ScheduleSnapshot::Enabled(s.parse()?),
|
||||
})
|
||||
}
|
||||
fn default_rate_limiting_global_pool() -> u32 {
|
||||
DEFAULT_GLOBAL_RATE_LIMITING_POOL
|
||||
}
|
||||
|
||||
fn parse_schedule_snapshot(s: &str) -> Result<ScheduleSnapshot, ParseIntError> {
|
||||
Ok(if s.is_empty() { ScheduleSnapshot::Disabled } else { ScheduleSnapshot::from_str(s)? })
|
||||
fn default_rate_limiting_ip_pool() -> u32 {
|
||||
DEFAULT_IP_RATE_LIMITING_POOL
|
||||
}
|
||||
|
||||
fn schedule_snapshot_to_env(schedule_snapshot: ScheduleSnapshot) -> Option<String> {
|
||||
match schedule_snapshot {
|
||||
ScheduleSnapshot::Enabled(snapshot_delay) => Some(snapshot_delay.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
fn default_rate_limiting_api_key_pool() -> u32 {
|
||||
DEFAULT_API_KEY_RATE_LIMITING_POOL
|
||||
}
|
||||
|
||||
fn schedule_snapshot_deserialize<'de, D>(deserializer: D) -> Result<ScheduleSnapshot, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct BoolOrInt;
|
||||
fn default_rate_limiting_global_cooldown_ns() -> u64 {
|
||||
DEFAULT_GLOBAL_RATE_LIMITING_COOLDOWN_NS
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for BoolOrInt {
|
||||
type Value = ScheduleSnapshot;
|
||||
fn default_rate_limiting_ip_cooldown_ns() -> u64 {
|
||||
DEFAULT_IP_RATE_LIMITING_COOLDOWN_NS
|
||||
}
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("integer or boolean")
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(if value {
|
||||
ScheduleSnapshot::Enabled(DEFAULT_SNAPSHOT_INTERVAL_SEC)
|
||||
} else {
|
||||
ScheduleSnapshot::Disabled
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(ScheduleSnapshot::Enabled(v as u64))
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(ScheduleSnapshot::Enabled(v))
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(ScheduleSnapshot::Disabled)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(ScheduleSnapshot::Disabled)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(BoolOrInt)
|
||||
fn default_rate_limiting_api_key_cooldown_ns() -> u64 {
|
||||
DEFAULT_API_KEY_RATE_LIMITING_COOLDOWN_NS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use std::convert::Infallible;
|
||||
use std::num::ParseIntError;
|
||||
use std::{fmt, str};
|
||||
use std::str;
|
||||
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef};
|
||||
use meilisearch_auth::error::AuthControllerError;
|
||||
use meilisearch_auth::AuthController;
|
||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
||||
use meilisearch_types::error::{Code, ResponseError};
|
||||
use meilisearch_types::keys::{Action, Key};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -15,7 +12,6 @@ use uuid::Uuid;
|
||||
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::query_parameters::QueryParameter;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::routes::Pagination;
|
||||
|
||||
@@ -49,72 +45,10 @@ pub async fn create_api_key(
|
||||
Ok(HttpResponse::Created().json(res))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PaginationDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PaginationDeserrError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PaginationDeserrError {}
|
||||
impl ErrorCode for PaginationDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<PaginationDeserrError> for PaginationDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: PaginationDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeError for PaginationDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
let code = match location.last_field() {
|
||||
Some("offset") => Code::InvalidApiKeyLimit,
|
||||
Some("limit") => Code::InvalidApiKeyOffset,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
|
||||
Err(PaginationDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<ParseIntError> for PaginationDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: ParseIntError,
|
||||
merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
PaginationDeserrError::error::<Infallible>(
|
||||
None,
|
||||
deserr::ErrorKind::Unexpected { msg: other.to_string() },
|
||||
merge_location,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_api_keys(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, AuthController>,
|
||||
paginate: QueryParameter<Pagination, PaginationDeserrError>,
|
||||
paginate: web::Query<Pagination>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
let paginate = paginate.into_inner();
|
||||
let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
|
||||
let keys = auth_controller.list_keys()?;
|
||||
let page_view = paginate
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::io::ErrorKind;
|
||||
use std::num::ParseIntError;
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_web::http::header::CONTENT_TYPE;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use bstr::ByteSlice;
|
||||
use deserr::{DeserializeError, DeserializeFromValue, IntoValue, MergeWithError, ValuePointerRef};
|
||||
use futures::StreamExt;
|
||||
use index_scheduler::IndexScheduler;
|
||||
use log::debug;
|
||||
use meilisearch_types::document_formats::{read_csv, read_json, read_ndjson, PayloadType};
|
||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::heed::RoTxn;
|
||||
use meilisearch_types::index_uid::IndexUid;
|
||||
use meilisearch_types::milli::update::IndexDocumentsMethod;
|
||||
@@ -35,7 +30,6 @@ use crate::error::PayloadError::ReceivePayload;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::payload::Payload;
|
||||
use crate::extractors::query_parameters::QueryParameter;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::routes::{fold_star_or, PaginationView, SummarizedTaskView};
|
||||
|
||||
@@ -82,62 +76,16 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct GetDocument {
|
||||
fields: Option<CS<StarOr<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GetDocumentDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GetDocumentDeserrError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for GetDocumentDeserrError {}
|
||||
impl ErrorCode for GetDocumentDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<GetDocumentDeserrError> for GetDocumentDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: GetDocumentDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeError for GetDocumentDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
let code = match location.last_field() {
|
||||
Some("fields") => Code::InvalidDocumentFields,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
|
||||
Err(GetDocumentDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_document(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
|
||||
path: web::Path<DocumentParam>,
|
||||
params: QueryParameter<GetDocument, GetDocumentDeserrError>,
|
||||
params: web::Query<GetDocument>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
let GetDocument { fields } = params.into_inner();
|
||||
let attributes_to_retrieve = fields.and_then(fold_star_or);
|
||||
@@ -164,82 +112,20 @@ pub async fn delete_document(
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct BrowseQuery {
|
||||
#[deserr(default, from(&String) = FromStr::from_str -> ParseIntError)]
|
||||
#[serde(default)]
|
||||
offset: usize,
|
||||
#[deserr(default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> ParseIntError)]
|
||||
#[serde(default = "crate::routes::PAGINATION_DEFAULT_LIMIT")]
|
||||
limit: usize,
|
||||
fields: Option<CS<StarOr<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BrowseQueryDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BrowseQueryDeserrError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for BrowseQueryDeserrError {}
|
||||
impl ErrorCode for BrowseQueryDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<BrowseQueryDeserrError> for BrowseQueryDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: BrowseQueryDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeError for BrowseQueryDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
let code = match location.last_field() {
|
||||
Some("fields") => Code::InvalidDocumentFields,
|
||||
Some("offset") => Code::InvalidDocumentOffset,
|
||||
Some("limit") => Code::InvalidDocumentLimit,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
|
||||
Err(BrowseQueryDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<ParseIntError> for BrowseQueryDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: ParseIntError,
|
||||
merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
BrowseQueryDeserrError::error::<Infallible>(
|
||||
None,
|
||||
deserr::ErrorKind::Unexpected { msg: other.to_string() },
|
||||
merge_location,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all_documents(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
params: QueryParameter<BrowseQuery, BrowseQueryDeserrError>,
|
||||
params: web::Query<BrowseQuery>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
debug!("called with params: {:?}", params);
|
||||
let BrowseQuery { limit, offset, fields } = params.into_inner();
|
||||
@@ -254,62 +140,16 @@ pub async fn get_all_documents(
|
||||
Ok(HttpResponse::Ok().json(ret))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct UpdateDocumentsQuery {
|
||||
pub primary_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateDocumentsQueryDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UpdateDocumentsQueryDeserrError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for UpdateDocumentsQueryDeserrError {}
|
||||
impl ErrorCode for UpdateDocumentsQueryDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<UpdateDocumentsQueryDeserrError> for UpdateDocumentsQueryDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: UpdateDocumentsQueryDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeError for UpdateDocumentsQueryDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
let code = match location.last_field() {
|
||||
Some("primaryKey") => Code::InvalidIndexPrimaryKey,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
|
||||
Err(UpdateDocumentsQueryDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_documents(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
params: QueryParameter<UpdateDocumentsQuery, UpdateDocumentsQueryDeserrError>,
|
||||
params: web::Query<UpdateDocumentsQuery>,
|
||||
body: Payload,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
@@ -337,7 +177,7 @@ pub async fn add_documents(
|
||||
pub async fn update_documents(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
|
||||
path: web::Path<String>,
|
||||
params: QueryParameter<UpdateDocumentsQuery, UpdateDocumentsQueryDeserrError>,
|
||||
params: web::Query<UpdateDocumentsQuery>,
|
||||
body: Payload,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
use std::convert::Infallible;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use deserr::{
|
||||
DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef,
|
||||
};
|
||||
use index_scheduler::IndexScheduler;
|
||||
use log::debug;
|
||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::index_uid::IndexUid;
|
||||
use meilisearch_types::milli::{self, FieldDistribution, Index};
|
||||
use meilisearch_types::tasks::KindWithContent;
|
||||
@@ -20,15 +14,14 @@ use super::{Pagination, SummarizedTaskView};
|
||||
use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
use crate::extractors::json::ValidatedJson;
|
||||
use crate::extractors::query_parameters::QueryParameter;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::RateLimiters;
|
||||
|
||||
pub mod documents;
|
||||
pub mod search;
|
||||
pub mod settings;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
pub fn configure(cfg: &mut web::ServiceConfig, rate_limiters: RateLimiters) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
.route(web::get().to(list_indexes))
|
||||
@@ -44,7 +37,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
)
|
||||
.service(web::resource("/stats").route(web::get().to(SeqHandler(get_index_stats))))
|
||||
.service(web::scope("/documents").configure(documents::configure))
|
||||
.service(web::scope("/search").configure(search::configure))
|
||||
.service(web::scope("/search").configure(|cfg| search::configure(cfg, rate_limiters)))
|
||||
.service(web::scope("/settings").configure(settings::configure)),
|
||||
);
|
||||
}
|
||||
@@ -74,7 +67,7 @@ impl IndexView {
|
||||
|
||||
pub async fn list_indexes(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
|
||||
paginate: QueryParameter<Pagination, ListIndexesDeserrError>,
|
||||
paginate: web::Query<Pagination>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
let search_rules = &index_scheduler.filters().search_rules;
|
||||
let indexes: Vec<_> = index_scheduler.indexes()?;
|
||||
@@ -90,68 +83,8 @@ pub async fn list_indexes(
|
||||
Ok(HttpResponse::Ok().json(ret))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ListIndexesDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListIndexesDeserrError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ListIndexesDeserrError {}
|
||||
impl ErrorCode for ListIndexesDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<ListIndexesDeserrError> for ListIndexesDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: ListIndexesDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl deserr::DeserializeError for ListIndexesDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let code = match location.last_field() {
|
||||
Some("offset") => Code::InvalidIndexLimit,
|
||||
Some("limit") => Code::InvalidIndexOffset,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
Err(ListIndexesDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<ParseIntError> for ListIndexesDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: ParseIntError,
|
||||
merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
ListIndexesDeserrError::error::<Infallible>(
|
||||
None,
|
||||
ErrorKind::Unexpected { msg: other.to_string() },
|
||||
merge_location,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeserializeFromValue, Debug)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct IndexCreateRequest {
|
||||
uid: String,
|
||||
primary_key: Option<String>,
|
||||
@@ -159,7 +92,7 @@ pub struct IndexCreateRequest {
|
||||
|
||||
pub async fn create_index(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>,
|
||||
body: ValidatedJson<IndexCreateRequest, CreateIndexesDeserrError>,
|
||||
body: web::Json<IndexCreateRequest>,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
@@ -184,58 +117,11 @@ pub async fn create_index(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateIndexesDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CreateIndexesDeserrError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CreateIndexesDeserrError {}
|
||||
impl ErrorCode for CreateIndexesDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<CreateIndexesDeserrError> for CreateIndexesDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: CreateIndexesDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl deserr::DeserializeError for CreateIndexesDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let code = match location.last_field() {
|
||||
Some("uid") => Code::InvalidIndexUid,
|
||||
Some("primaryKey") => Code::InvalidIndexPrimaryKey,
|
||||
None if matches!(error, ErrorKind::MissingField { field } if field == "uid") => {
|
||||
Code::MissingIndexUid
|
||||
}
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
Err(CreateIndexesDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeserializeFromValue, Debug)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[allow(dead_code)]
|
||||
pub struct UpdateIndexRequest {
|
||||
uid: Option<String>,
|
||||
primary_key: Option<String>,
|
||||
}
|
||||
|
||||
@@ -254,7 +140,7 @@ pub async fn get_index(
|
||||
pub async fn update_index(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_UPDATE }>, Data<IndexScheduler>>,
|
||||
path: web::Path<String>,
|
||||
body: ValidatedJson<UpdateIndexRequest, UpdateIndexesDeserrError>,
|
||||
body: web::Json<UpdateIndexRequest>,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
@@ -262,7 +148,7 @@ pub async fn update_index(
|
||||
let body = body.into_inner();
|
||||
analytics.publish(
|
||||
"Index Updated".to_string(),
|
||||
json!({ "primary_key": body.primary_key }),
|
||||
json!({ "primary_key": body.primary_key}),
|
||||
Some(&req),
|
||||
);
|
||||
|
||||
@@ -278,51 +164,6 @@ pub async fn update_index(
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateIndexesDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UpdateIndexesDeserrError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for UpdateIndexesDeserrError {}
|
||||
impl ErrorCode for UpdateIndexesDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<UpdateIndexesDeserrError> for UpdateIndexesDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: UpdateIndexesDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl deserr::DeserializeError for UpdateIndexesDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let code = match location.last_field() {
|
||||
Some("primaryKey") => Code::InvalidIndexPrimaryKey,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
Err(UpdateIndexesDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_index(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_DELETE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
||||
@@ -1,64 +1,62 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use index_scheduler::IndexScheduler;
|
||||
use log::debug;
|
||||
use meilisearch_auth::IndexSearchRules;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use serde::Deserialize;
|
||||
use serde_cs::vec::CS;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::analytics::{Analytics, SearchAggregator};
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::json::ValidatedJson;
|
||||
use crate::extractors::query_parameters::QueryParameter;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::routes::from_string_to_option;
|
||||
use crate::search::{
|
||||
perform_search, MatchingStrategy, SearchDeserError, SearchQuery, DEFAULT_CROP_LENGTH,
|
||||
DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG,
|
||||
DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET,
|
||||
perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER,
|
||||
DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
|
||||
DEFAULT_SEARCH_OFFSET,
|
||||
};
|
||||
use crate::RateLimiters;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
pub fn configure(cfg: &mut web::ServiceConfig, rate_limiters: RateLimiters) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
.wrap(rate_limiters.global.into_middleware())
|
||||
.wrap(rate_limiters.ip.into_middleware())
|
||||
.wrap(rate_limiters.api_key.into_middleware())
|
||||
.route(web::get().to(SeqHandler(search_with_url_query)))
|
||||
.route(web::post().to(SeqHandler(search_with_post))),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, deserr::DeserializeFromValue)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SearchQueryGet {
|
||||
q: Option<String>,
|
||||
#[deserr(default = DEFAULT_SEARCH_OFFSET(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
||||
#[serde(default = "DEFAULT_SEARCH_OFFSET")]
|
||||
offset: usize,
|
||||
#[deserr(default = DEFAULT_SEARCH_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
||||
#[serde(default = "DEFAULT_SEARCH_LIMIT")]
|
||||
limit: usize,
|
||||
#[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)]
|
||||
page: Option<usize>,
|
||||
#[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)]
|
||||
hits_per_page: Option<usize>,
|
||||
attributes_to_retrieve: Option<CS<String>>,
|
||||
attributes_to_crop: Option<CS<String>>,
|
||||
#[deserr(default = DEFAULT_CROP_LENGTH(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
||||
#[serde(default = "DEFAULT_CROP_LENGTH")]
|
||||
crop_length: usize,
|
||||
attributes_to_highlight: Option<CS<String>>,
|
||||
filter: Option<String>,
|
||||
sort: Option<String>,
|
||||
#[deserr(default, from(&String) = FromStr::from_str -> std::str::ParseBoolError)]
|
||||
#[serde(default = "Default::default")]
|
||||
show_matches_position: bool,
|
||||
facets: Option<CS<String>>,
|
||||
#[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
||||
#[serde(default = "DEFAULT_HIGHLIGHT_PRE_TAG")]
|
||||
highlight_pre_tag: String,
|
||||
#[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
||||
#[serde(default = "DEFAULT_HIGHLIGHT_POST_TAG")]
|
||||
highlight_post_tag: String,
|
||||
#[deserr(default = DEFAULT_CROP_MARKER())]
|
||||
#[serde(default = "DEFAULT_CROP_MARKER")]
|
||||
crop_marker: String,
|
||||
#[deserr(default)]
|
||||
#[serde(default)]
|
||||
matching_strategy: MatchingStrategy,
|
||||
}
|
||||
|
||||
@@ -142,7 +140,7 @@ fn fix_sort_query_parameters(sort_query: &str) -> Vec<String> {
|
||||
pub async fn search_with_url_query(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
params: QueryParameter<SearchQueryGet, SearchDeserError>,
|
||||
params: web::Query<SearchQueryGet>,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
@@ -174,7 +172,7 @@ pub async fn search_with_url_query(
|
||||
pub async fn search_with_post(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
params: ValidatedJson<SearchQuery, SearchDeserError>,
|
||||
params: web::Json<SearchQuery>,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use deserr::{IntoValue, ValuePointerRef};
|
||||
use index_scheduler::IndexScheduler;
|
||||
use log::debug;
|
||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::index_uid::IndexUid;
|
||||
use meilisearch_types::settings::{settings, Settings, Unchecked};
|
||||
use meilisearch_types::tasks::KindWithContent;
|
||||
@@ -14,7 +11,6 @@ use serde_json::json;
|
||||
use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::json::ValidatedJson;
|
||||
use crate::routes::SummarizedTaskView;
|
||||
|
||||
#[macro_export]
|
||||
@@ -43,7 +39,7 @@ macro_rules! make_setting_route {
|
||||
>,
|
||||
index_uid: web::Path<String>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
let new_settings = Settings { $attr: Setting::Reset.into(), ..Default::default() };
|
||||
let new_settings = Settings { $attr: Setting::Reset, ..Default::default() };
|
||||
|
||||
let allow_index_creation = index_scheduler.filters().allow_index_creation;
|
||||
let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner();
|
||||
@@ -78,8 +74,8 @@ macro_rules! make_setting_route {
|
||||
|
||||
let new_settings = Settings {
|
||||
$attr: match body {
|
||||
Some(inner_body) => Setting::Set(inner_body).into(),
|
||||
None => Setting::Reset.into(),
|
||||
Some(inner_body) => Setting::Set(inner_body),
|
||||
None => Setting::Reset,
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
@@ -212,7 +208,7 @@ make_setting_route!(
|
||||
"TypoTolerance Updated".to_string(),
|
||||
json!({
|
||||
"typo_tolerance": {
|
||||
"enabled": setting.as_ref().map(|s| !matches!(s.enabled.into(), Setting::Set(false))),
|
||||
"enabled": setting.as_ref().map(|s| !matches!(s.enabled, Setting::Set(false))),
|
||||
"disable_on_attributes": setting
|
||||
.as_ref()
|
||||
.and_then(|s| s.disable_on_attributes.as_ref().set().map(|m| !m.is_empty())),
|
||||
@@ -428,66 +424,10 @@ generate_configure!(
|
||||
faceting
|
||||
);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SettingsDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SettingsDeserrError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SettingsDeserrError {}
|
||||
impl ErrorCode for SettingsDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl deserr::MergeWithError<SettingsDeserrError> for SettingsDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: SettingsDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl deserr::DeserializeError for SettingsDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
let code = match location.first_field() {
|
||||
Some("displayedAttributes") => Code::InvalidSettingsDisplayedAttributes,
|
||||
Some("searchableAttributes") => Code::InvalidSettingsSearchableAttributes,
|
||||
Some("filterableAttributes") => Code::InvalidSettingsFilterableAttributes,
|
||||
Some("sortableAttributes") => Code::InvalidSettingsSortableAttributes,
|
||||
Some("rankingRules") => Code::InvalidSettingsRankingRules,
|
||||
Some("stopWords") => Code::InvalidSettingsStopWords,
|
||||
Some("synonyms") => Code::InvalidSettingsSynonyms,
|
||||
Some("distinctAttribute") => Code::InvalidSettingsDistinctAttribute,
|
||||
Some("typoTolerance") => Code::InvalidSettingsTypoTolerance,
|
||||
Some("faceting") => Code::InvalidSettingsFaceting,
|
||||
Some("pagination") => Code::InvalidSettingsPagination,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
|
||||
Err(SettingsDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_all(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
body: ValidatedJson<Settings<Unchecked>, SettingsDeserrError>,
|
||||
body: web::Json<Settings<Unchecked>>,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use deserr::DeserializeFromValue;
|
||||
use index_scheduler::{IndexScheduler, Query};
|
||||
use log::debug;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
@@ -18,6 +16,7 @@ use self::indexes::IndexStats;
|
||||
use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::RateLimiters;
|
||||
|
||||
mod api_key;
|
||||
mod dump;
|
||||
@@ -25,14 +24,14 @@ pub mod indexes;
|
||||
mod swap_indexes;
|
||||
pub mod tasks;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
pub fn configure(cfg: &mut web::ServiceConfig, rate_limiters: RateLimiters) {
|
||||
cfg.service(web::scope("/tasks").configure(tasks::configure))
|
||||
.service(web::resource("/health").route(web::get().to(get_health)))
|
||||
.service(web::scope("/keys").configure(api_key::configure))
|
||||
.service(web::scope("/dumps").configure(dump::configure))
|
||||
.service(web::resource("/stats").route(web::get().to(get_stats)))
|
||||
.service(web::resource("/version").route(web::get().to(get_version)))
|
||||
.service(web::scope("/indexes").configure(indexes::configure))
|
||||
.service(web::scope("/indexes").configure(|cfg| indexes::configure(cfg, rate_limiters)))
|
||||
.service(web::scope("/swap-indexes").configure(swap_indexes::configure));
|
||||
}
|
||||
|
||||
@@ -51,13 +50,6 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn from_string_to_option<T, E>(input: &str) -> Result<Option<T>, E>
|
||||
where
|
||||
T: FromStr<Err = E>,
|
||||
{
|
||||
Ok(Some(input.parse()?))
|
||||
}
|
||||
|
||||
const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -84,15 +76,12 @@ impl From<Task> for SummarizedTaskView {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Pagination {
|
||||
#[serde(default)]
|
||||
#[deserr(default, from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
||||
pub offset: usize,
|
||||
#[serde(default = "PAGINATION_DEFAULT_LIMIT")]
|
||||
#[deserr(default = PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::fmt;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use deserr::{DeserializeFromValue, IntoValue, ValuePointerRef};
|
||||
use index_scheduler::IndexScheduler;
|
||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::tasks::{IndexSwap, KindWithContent};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::SummarizedTaskView;
|
||||
@@ -13,26 +11,23 @@ use crate::analytics::Analytics;
|
||||
use crate::error::MeilisearchHttpError;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
use crate::extractors::json::ValidatedJson;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("").route(web::post().to(SeqHandler(swap_indexes))));
|
||||
}
|
||||
|
||||
#[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SwapIndexesPayload {
|
||||
indexes: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn swap_indexes(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_SWAP }>, Data<IndexScheduler>>,
|
||||
params: ValidatedJson<Vec<SwapIndexesPayload>, SwapIndexesDeserrError>,
|
||||
params: web::Json<Vec<SwapIndexesPayload>>,
|
||||
req: HttpRequest,
|
||||
analytics: web::Data<dyn Analytics>,
|
||||
) -> Result<HttpResponse, ResponseError> {
|
||||
let params = params.into_inner();
|
||||
analytics.publish(
|
||||
"Indexes Swapped".to_string(),
|
||||
json!({
|
||||
@@ -43,7 +38,7 @@ pub async fn swap_indexes(
|
||||
let search_rules = &index_scheduler.filters().search_rules;
|
||||
|
||||
let mut swaps = vec![];
|
||||
for SwapIndexesPayload { indexes } in params.into_iter() {
|
||||
for SwapIndexesPayload { indexes } in params.into_inner().into_iter() {
|
||||
let (lhs, rhs) = match indexes.as_slice() {
|
||||
[lhs, rhs] => (lhs, rhs),
|
||||
_ => {
|
||||
@@ -62,49 +57,3 @@ pub async fn swap_indexes(
|
||||
let task: SummarizedTaskView = task.into();
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SwapIndexesDeserrError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SwapIndexesDeserrError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SwapIndexesDeserrError {}
|
||||
impl ErrorCode for SwapIndexesDeserrError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl deserr::MergeWithError<SwapIndexesDeserrError> for SwapIndexesDeserrError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: SwapIndexesDeserrError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl deserr::DeserializeError for SwapIndexesDeserrError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
let code = match location.last_field() {
|
||||
Some("indexes") => Code::InvalidSwapIndexes,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
|
||||
Err(SwapIndexesDeserrError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::str::FromStr;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use index_scheduler::error::DateField;
|
||||
use index_scheduler::{IndexScheduler, Query, TaskId};
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::index_uid::IndexUid;
|
||||
@@ -169,7 +168,6 @@ pub struct TaskCommonQueryRaw {
|
||||
pub statuses: Option<CS<StarOr<String>>>,
|
||||
pub index_uids: Option<CS<StarOr<String>>>,
|
||||
}
|
||||
|
||||
impl TaskCommonQueryRaw {
|
||||
fn validate(self) -> Result<TaskCommonQuery, ResponseError> {
|
||||
let Self { uids, canceled_by, types, statuses, index_uids } = self;
|
||||
@@ -292,37 +290,37 @@ impl TaskDateQueryRaw {
|
||||
|
||||
for (field_name, string_value, before_or_after, dest) in [
|
||||
(
|
||||
DateField::AfterEnqueuedAt,
|
||||
"afterEnqueuedAt",
|
||||
after_enqueued_at,
|
||||
DeserializeDateOption::After,
|
||||
&mut query.after_enqueued_at,
|
||||
),
|
||||
(
|
||||
DateField::BeforeEnqueuedAt,
|
||||
"beforeEnqueuedAt",
|
||||
before_enqueued_at,
|
||||
DeserializeDateOption::Before,
|
||||
&mut query.before_enqueued_at,
|
||||
),
|
||||
(
|
||||
DateField::AfterStartedAt,
|
||||
"afterStartedAt",
|
||||
after_started_at,
|
||||
DeserializeDateOption::After,
|
||||
&mut query.after_started_at,
|
||||
),
|
||||
(
|
||||
DateField::BeforeStartedAt,
|
||||
"beforeStartedAt",
|
||||
before_started_at,
|
||||
DeserializeDateOption::Before,
|
||||
&mut query.before_started_at,
|
||||
),
|
||||
(
|
||||
DateField::AfterFinishedAt,
|
||||
"afterFinishedAt",
|
||||
after_finished_at,
|
||||
DeserializeDateOption::After,
|
||||
&mut query.after_finished_at,
|
||||
),
|
||||
(
|
||||
DateField::BeforeFinishedAt,
|
||||
"beforeFinishedAt",
|
||||
before_finished_at,
|
||||
DeserializeDateOption::Before,
|
||||
&mut query.before_finished_at,
|
||||
@@ -692,7 +690,6 @@ async fn get_task(
|
||||
}
|
||||
|
||||
pub(crate) mod date_deserializer {
|
||||
use index_scheduler::error::DateField;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::macros::format_description;
|
||||
@@ -704,7 +701,7 @@ pub(crate) mod date_deserializer {
|
||||
}
|
||||
|
||||
pub fn deserialize_date(
|
||||
field_name: DateField,
|
||||
field_name: &str,
|
||||
value: &str,
|
||||
option: DeserializeDateOption,
|
||||
) -> std::result::Result<OffsetDateTime, ResponseError> {
|
||||
@@ -730,7 +727,7 @@ pub(crate) mod date_deserializer {
|
||||
}
|
||||
} else {
|
||||
Err(index_scheduler::Error::InvalidTaskDate {
|
||||
field: field_name,
|
||||
field: field_name.to_string(),
|
||||
date: value.to_string(),
|
||||
}
|
||||
.into())
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
use std::cmp::min;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
use std::str::{FromStr, ParseBoolError};
|
||||
use std::str::FromStr;
|
||||
use std::time::Instant;
|
||||
|
||||
use deserr::{
|
||||
DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef,
|
||||
};
|
||||
use either::Either;
|
||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode};
|
||||
use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS;
|
||||
use meilisearch_types::{milli, Document};
|
||||
use milli::tokenizer::TokenizerBuilder;
|
||||
@@ -33,33 +26,34 @@ pub const DEFAULT_CROP_MARKER: fn() -> String = || "…".to_string();
|
||||
pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "<em>".to_string();
|
||||
pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct SearchQuery {
|
||||
pub q: Option<String>,
|
||||
#[deserr(default = DEFAULT_SEARCH_OFFSET())]
|
||||
#[serde(default = "DEFAULT_SEARCH_OFFSET")]
|
||||
pub offset: usize,
|
||||
#[deserr(default = DEFAULT_SEARCH_LIMIT())]
|
||||
#[serde(default = "DEFAULT_SEARCH_LIMIT")]
|
||||
pub limit: usize,
|
||||
pub page: Option<usize>,
|
||||
pub hits_per_page: Option<usize>,
|
||||
pub attributes_to_retrieve: Option<BTreeSet<String>>,
|
||||
pub attributes_to_crop: Option<Vec<String>>,
|
||||
#[deserr(default = DEFAULT_CROP_LENGTH())]
|
||||
#[serde(default = "DEFAULT_CROP_LENGTH")]
|
||||
pub crop_length: usize,
|
||||
pub attributes_to_highlight: Option<HashSet<String>>,
|
||||
#[deserr(default)]
|
||||
// Default to false
|
||||
#[serde(default = "Default::default")]
|
||||
pub show_matches_position: bool,
|
||||
pub filter: Option<Value>,
|
||||
pub sort: Option<Vec<String>>,
|
||||
pub facets: Option<Vec<String>>,
|
||||
#[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
||||
#[serde(default = "DEFAULT_HIGHLIGHT_PRE_TAG")]
|
||||
pub highlight_pre_tag: String,
|
||||
#[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
||||
#[serde(default = "DEFAULT_HIGHLIGHT_POST_TAG")]
|
||||
pub highlight_post_tag: String,
|
||||
#[deserr(default = DEFAULT_CROP_MARKER())]
|
||||
#[serde(default = "DEFAULT_CROP_MARKER")]
|
||||
pub crop_marker: String,
|
||||
#[deserr(default)]
|
||||
#[serde(default)]
|
||||
pub matching_strategy: MatchingStrategy,
|
||||
}
|
||||
|
||||
@@ -69,8 +63,7 @@ impl SearchQuery {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[deserr(rename_all = camelCase)]
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum MatchingStrategy {
|
||||
/// Remove query words from last to first
|
||||
@@ -94,96 +87,6 @@ impl From<MatchingStrategy> for TermsMatchingStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchDeserError {
|
||||
error: String,
|
||||
code: Code,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SearchDeserError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.error)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SearchDeserError {}
|
||||
impl ErrorCode for SearchDeserError {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<SearchDeserError> for SearchDeserError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: SearchDeserError,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializeError for SearchDeserError {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||
|
||||
let code = match location.last_field() {
|
||||
Some("q") => Code::InvalidSearchQ,
|
||||
Some("offset") => Code::InvalidSearchOffset,
|
||||
Some("limit") => Code::InvalidSearchLimit,
|
||||
Some("page") => Code::InvalidSearchPage,
|
||||
Some("hitsPerPage") => Code::InvalidSearchHitsPerPage,
|
||||
Some("attributesToRetrieve") => Code::InvalidSearchAttributesToRetrieve,
|
||||
Some("attributesToCrop") => Code::InvalidSearchAttributesToCrop,
|
||||
Some("cropLength") => Code::InvalidSearchCropLength,
|
||||
Some("attributesToHighlight") => Code::InvalidSearchAttributesToHighlight,
|
||||
Some("showMatchesPosition") => Code::InvalidSearchShowMatchesPosition,
|
||||
Some("filter") => Code::InvalidSearchFilter,
|
||||
Some("sort") => Code::InvalidSearchSort,
|
||||
Some("facets") => Code::InvalidSearchFacets,
|
||||
Some("highlightPreTag") => Code::InvalidSearchHighlightPreTag,
|
||||
Some("highlightPostTag") => Code::InvalidSearchHighlightPostTag,
|
||||
Some("cropMarker") => Code::InvalidSearchCropMarker,
|
||||
Some("matchingStrategy") => Code::InvalidSearchMatchingStrategy,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
|
||||
Err(SearchDeserError { error, code })
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<ParseBoolError> for SearchDeserError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: ParseBoolError,
|
||||
merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
SearchDeserError::error::<Infallible>(
|
||||
None,
|
||||
ErrorKind::Unexpected { msg: other.to_string() },
|
||||
merge_location,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl MergeWithError<ParseIntError> for SearchDeserError {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: ParseIntError,
|
||||
merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
SearchDeserError::error::<Infallible>(
|
||||
None,
|
||||
ErrorKind::Unexpected { msg: other.to_string() },
|
||||
merge_location,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct SearchHit {
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
{"id":0,"isActive":false,"balance":"$2,668.55","picture":"http://placehold.it/32x32","age":36,"color":"Green","name":"Lucas Hess","gender":"male","email":"lucashess@chorizon.com","phone":"+1 (998) 478-2597","address":"412 Losee Terrace, Blairstown, Georgia, 2825","about":"Mollit ad in exercitation quis. Anim est ut consequat fugiat duis magna aliquip velit nisi. Commodo eiusmod est consequat proident consectetur aliqua enim fugiat. Aliqua adipisicing laboris elit proident enim veniam laboris mollit. Incididunt fugiat minim ad nostrud deserunt tempor in. Id irure officia labore qui est labore nulla nisi. Magna sit quis tempor esse consectetur amet labore duis aliqua consequat.\r\n","registered":"2016-06-21T09:30:25 -02:00","latitude":-44.174957,"longitude":-145.725388,"tags":["bug","bug"]}
|
||||
{"id":1,"isActive":true,"balance":"$1,706.13","picture":"http://placehold.it/32x32","age":27,"color":"Green","name":"Cherry Orr","gender":"female","email":"cherryorr@chorizon.com","phone":"+1 (995) 479-3174","address":"442 Beverly Road, Ventress, New Mexico, 3361","about":"Exercitation officia mollit proident nostrud ea. Pariatur voluptate labore nostrud magna duis non elit et incididunt Lorem velit duis amet commodo. Irure in velit laboris pariatur. Do tempor ex deserunt duis minim amet.\r\n","registered":"2020-03-18T11:12:21 -01:00","latitude":-24.356932,"longitude":27.184808,"tags":["new issue","bug"]}
|
||||
{"id":2,"isActive":true,"balance":"$2,467.47","picture":"http://placehold.it/32x32","age":34,"color":"blue","name":"Patricia Goff","gender":"female","email":"patriciagoff@chorizon.com","phone":"+1 (864) 463-2277","address":"866 Hornell Loop, Cresaptown, Ohio, 1700","about":"Non culpa duis dolore Lorem aliqua. Labore veniam laborum cupidatat nostrud ea exercitation. Esse nostrud sit veniam laborum minim ullamco nulla aliqua est cillum magna. Duis non esse excepteur veniam voluptate sunt cupidatat nostrud consequat sint adipisicing ut excepteur. Incididunt sit aliquip non id magna amet deserunt esse quis dolor.\r\n","registered":"2014-10-28T12:59:30 -01:00","latitude":-64.008555,"longitude":11.867098,"tags":["good first issue"]}
|
||||
{"id":3,"isActive":true,"balance":"$3,344.40","picture":"http://placehold.it/32x32","age":35,"color":"blue","name":"Adeline Flynn","gender":"female","email":"adelineflynn@chorizon.com","phone":"+1 (994) 600-2840","address":"428 Paerdegat Avenue, Hollymead, Pennsylvania, 948","about":"Ex velit magna minim labore dolor id laborum incididunt. Proident dolor fugiat exercitation ad adipisicing amet dolore. Veniam nisi pariatur aute eu amet sint elit duis exercitation. Eu fugiat Lorem nostrud consequat aute sunt. Minim excepteur cillum laboris enim tempor adipisicing nulla reprehenderit ea velit Lorem qui in incididunt. Esse ipsum mollit deserunt ea exercitation ex aliqua anim magna cupidatat culpa.\r\n","registered":"2014-03-27T06:24:45 -01:00","latitude":-74.485173,"longitude":-11.059859,"tags":["bug","good first issue","wontfix","new issue"]}
|
||||
{"id":4,"isActive":false,"balance":"$2,575.78","picture":"http://placehold.it/32x32","age":39,"color":"Green","name":"Mariana Pacheco","gender":"female","email":"marianapacheco@chorizon.com","phone":"+1 (820) 414-2223","address":"664 Rapelye Street, Faywood, California, 7320","about":"Sint cillum enim eu Lorem dolore. Est excepteur cillum consequat incididunt. Ut consectetur et do culpa eiusmod ex ut id proident aliqua. Sunt dolor anim minim labore incididunt deserunt enim velit sunt ut in velit. Nulla ipsum cillum qui est minim officia in occaecat exercitation Lorem sunt. Aliqua minim excepteur tempor incididunt dolore. Quis amet ullamco et proident aliqua magna consequat.\r\n","registered":"2015-09-02T03:23:35 -02:00","latitude":75.763501,"longitude":-78.777124,"tags":["new issue"]}
|
||||
{"id":5,"isActive":true,"balance":"$3,793.09","picture":"http://placehold.it/32x32","age":20,"color":"Green","name":"Warren Watson","gender":"male","email":"warrenwatson@chorizon.com","phone":"+1 (807) 583-2427","address":"671 Prince Street, Faxon, Connecticut, 4275","about":"Cillum incididunt mollit labore ipsum elit ea. Lorem labore consectetur nulla ea fugiat sint esse cillum ea commodo id qui. Sint cillum mollit dolore enim quis esse. Nisi labore duis dolor tempor laborum laboris ad minim pariatur in excepteur sit. Aliqua anim amet sunt ullamco labore amet culpa irure esse eiusmod deserunt consequat Lorem nostrud.\r\n","registered":"2017-06-04T06:02:17 -02:00","latitude":29.979223,"longitude":25.358943,"tags":["wontfix","wontfix","wontfix"]}
|
||||
{"id":6,"isActive":true,"balance":"$2,919.70","picture":"http://placehold.it/32x32","age":20,"color":"blue","name":"Shelia Berry","gender":"female","email":"sheliaberry@chorizon.com","phone":"+1 (853) 511-2651","address":"437 Forrest Street, Coventry, Illinois, 2056","about":"Id occaecat qui voluptate proident culpa cillum nisi reprehenderit. Pariatur nostrud proident adipisicing reprehenderit eiusmod qui minim proident aliqua id cupidatat laboris deserunt. Proident sint laboris sit mollit dolor qui incididunt quis veniam cillum cupidatat ad nostrud ut. Aliquip consequat eiusmod eiusmod irure tempor do incididunt id culpa laboris eiusmod.\r\n","registered":"2018-07-11T02:45:01 -02:00","latitude":54.815991,"longitude":-118.690609,"tags":["good first issue","bug","wontfix","new issue"]}
|
||||
{"id":7,"isActive":true,"balance":"$1,349.50","picture":"http://placehold.it/32x32","age":28,"color":"Green","name":"Chrystal Boyd","gender":"female","email":"chrystalboyd@chorizon.com","phone":"+1 (936) 563-2802","address":"670 Croton Loop, Sussex, Florida, 4692","about":"Consequat ex voluptate consectetur laborum nulla. Qui voluptate Lorem amet labore est esse sunt. Nulla cupidatat consequat quis incididunt exercitation aliquip reprehenderit ea ea adipisicing reprehenderit id consectetur quis. Exercitation est incididunt ullamco non proident consequat. Nisi veniam aliquip fugiat voluptate ex id aute duis ullamco magna ipsum ad laborum ipsum. Cupidatat velit dolore esse nisi.\r\n","registered":"2016-11-01T07:36:04 -01:00","latitude":-24.711933,"longitude":147.246705,"tags":[]}
|
||||
{"id":8,"isActive":false,"balance":"$3,999.56","picture":"http://placehold.it/32x32","age":30,"color":"brown","name":"Martin Porter","gender":"male","email":"martinporter@chorizon.com","phone":"+1 (895) 580-2304","address":"577 Regent Place, Aguila, Guam, 6554","about":"Nostrud nulla labore ex excepteur labore enim cillum pariatur in do Lorem eiusmod ullamco est. Labore aliquip id ut nisi commodo pariatur ea esse laboris. Incididunt eu dolor esse excepteur nulla minim proident non cillum nisi dolore incididunt ipsum tempor.\r\n","registered":"2014-09-20T02:08:30 -02:00","latitude":-88.344273,"longitude":37.964466,"tags":[]}
|
||||
{"id":9,"isActive":true,"balance":"$3,729.71","picture":"http://placehold.it/32x32","age":26,"color":"blue","name":"Kelli Mendez","gender":"female","email":"kellimendez@chorizon.com","phone":"+1 (936) 401-2236","address":"242 Caton Place, Grazierville, Alabama, 3968","about":"Consectetur occaecat dolore esse eiusmod enim ea aliqua eiusmod amet velit laborum. Velit quis consequat consectetur velit fugiat labore commodo amet do. Magna minim est ad commodo consequat fugiat. Laboris duis Lorem ipsum irure sit ipsum consequat tempor sit. Est ad nulla duis quis velit anim id nulla. Cupidatat ea esse laboris eu veniam cupidatat proident veniam quis.\r\n","registered":"2018-05-04T10:35:30 -02:00","latitude":49.37551,"longitude":41.872323,"tags":["new issue","new issue"]}
|
||||
{"id":10,"isActive":false,"balance":"$1,127.47","picture":"http://placehold.it/32x32","age":27,"color":"blue","name":"Maddox Johns","gender":"male","email":"maddoxjohns@chorizon.com","phone":"+1 (892) 470-2357","address":"756 Beard Street, Avalon, Louisiana, 114","about":"Voluptate et dolor magna do do. Id do enim ut nulla esse culpa fugiat excepteur quis. Nostrud ad aliquip aliqua qui esse ut consequat proident deserunt esse cupidatat do elit fugiat. Sint cillum aliquip cillum laboris laborum laboris ad aliquip enim reprehenderit cillum eu sint. Sint ut ad duis do culpa non eiusmod amet non ipsum commodo. Pariatur aliquip sit deserunt non. Ut consequat pariatur deserunt veniam est sit eiusmod officia aliquip commodo sunt in eu duis.\r\n","registered":"2016-04-22T06:41:25 -02:00","latitude":66.640229,"longitude":-17.222666,"tags":["new issue","good first issue","good first issue","new issue"]}
|
||||
{"id":11,"isActive":true,"balance":"$1,351.43","picture":"http://placehold.it/32x32","age":28,"color":"Green","name":"Evans Wagner","gender":"male","email":"evanswagner@chorizon.com","phone":"+1 (889) 496-2332","address":"118 Monaco Place, Lutsen, Delaware, 6209","about":"Sunt consectetur enim ipsum consectetur occaecat reprehenderit nulla pariatur. Cupidatat do exercitation tempor voluptate duis nostrud dolor consectetur. Excepteur aliquip Lorem voluptate cillum est. Nisi velit nulla nostrud ea id officia laboris et.\r\n","registered":"2016-10-27T01:26:31 -02:00","latitude":-77.673222,"longitude":-142.657214,"tags":["good first issue","good first issue"]}
|
||||
{"id":12,"isActive":false,"balance":"$3,394.96","picture":"http://placehold.it/32x32","age":25,"color":"blue","name":"Aida Kirby","gender":"female","email":"aidakirby@chorizon.com","phone":"+1 (942) 532-2325","address":"797 Engert Avenue, Wilsonia, Idaho, 6532","about":"Mollit aute esse Lorem do laboris anim reprehenderit excepteur. Ipsum culpa esse voluptate officia cupidatat minim. Velit officia proident nostrud sunt irure labore. Culpa ex commodo amet dolor amet voluptate Lorem ex esse commodo fugiat quis non. Ex est adipisicing veniam sunt dolore ut aliqua nisi ex sit. Esse voluptate esse anim id adipisicing enim aute ea exercitation tempor cillum.\r\n","registered":"2018-06-18T04:39:57 -02:00","latitude":-58.062041,"longitude":34.999254,"tags":["new issue","wontfix","bug","new issue"]}
|
||||
{"id":13,"isActive":true,"balance":"$2,812.62","picture":"http://placehold.it/32x32","age":40,"color":"blue","name":"Nelda Burris","gender":"female","email":"neldaburris@chorizon.com","phone":"+1 (813) 600-2576","address":"160 Opal Court, Fowlerville, Tennessee, 2170","about":"Ipsum aliquip adipisicing elit magna. Veniam irure quis laborum laborum sint velit amet. Irure non eiusmod laborum fugiat qui quis Lorem culpa veniam commodo. Fugiat cupidatat dolore et consequat pariatur enim ex velit consequat deserunt quis. Deserunt et quis laborum cupidatat cillum minim cupidatat nisi do commodo commodo labore cupidatat ea. In excepteur sit nostrud nulla nostrud dolor sint. Et anim culpa aliquip laborum Lorem elit.\r\n","registered":"2015-08-15T12:39:53 -02:00","latitude":66.6871,"longitude":179.549488,"tags":["wontfix"]}
|
||||
{"id":14,"isActive":true,"balance":"$1,718.33","picture":"http://placehold.it/32x32","age":35,"color":"blue","name":"Jennifer Hart","gender":"female","email":"jenniferhart@chorizon.com","phone":"+1 (850) 537-2513","address":"124 Veranda Place, Nash, Utah, 985","about":"Amet amet voluptate in occaecat pariatur. Nulla ipsum esse quis qui in quis qui. Non est non nisi qui tempor commodo consequat fugiat. Sint eu ipsum aute anim anim. Ea nostrud excepteur exercitation consectetur Lorem.\r\n","registered":"2016-09-04T11:46:59 -02:00","latitude":-66.827751,"longitude":99.220079,"tags":["wontfix","bug","new issue","new issue"]}
|
||||
{"id":15,"isActive":false,"balance":"$2,698.16","picture":"http://placehold.it/32x32","age":28,"color":"blue","name":"Aurelia Contreras","gender":"female","email":"aureliacontreras@chorizon.com","phone":"+1 (932) 442-3103","address":"655 Dwight Street, Grapeview, Palau, 8356","about":"Qui adipisicing consectetur aute veniam culpa ipsum. Occaecat occaecat ut mollit enim enim elit Lorem nostrud Lorem. Consequat laborum mollit nulla aute cillum sunt mollit commodo velit culpa. Pariatur pariatur velit nostrud tempor. In minim enim cillum exercitation in laboris labore ea sunt in incididunt fugiat.\r\n","registered":"2014-09-11T10:43:15 -02:00","latitude":-71.328973,"longitude":133.404895,"tags":["wontfix","bug","good first issue"]}
|
||||
{"id":16,"isActive":true,"balance":"$3,303.25","picture":"http://placehold.it/32x32","age":28,"color":"brown","name":"Estella Bass","gender":"female","email":"estellabass@chorizon.com","phone":"+1 (825) 436-2909","address":"435 Rockwell Place, Garberville, Wisconsin, 2230","about":"Sit eiusmod mollit velit non. Qui ea in exercitation elit reprehenderit occaecat tempor minim officia. Culpa amet voluptate sit eiusmod pariatur.\r\n","registered":"2017-11-23T09:32:09 -01:00","latitude":81.17014,"longitude":-145.262693,"tags":["new issue"]}
|
||||
{"id":17,"isActive":false,"balance":"$3,579.20","picture":"http://placehold.it/32x32","age":25,"color":"brown","name":"Ortega Brennan","gender":"male","email":"ortegabrennan@chorizon.com","phone":"+1 (906) 526-2287","address":"440 Berry Street, Rivera, Maine, 1849","about":"Veniam velit non laboris consectetur sit aliquip enim proident velit in ipsum reprehenderit reprehenderit. Dolor qui nulla adipisicing ad magna dolore do ut duis et aute est. Qui est elit cupidatat nostrud. Laboris voluptate reprehenderit minim sint exercitation cupidatat ipsum sint consectetur velit sunt et officia incididunt. Ut amet Lorem minim deserunt officia officia irure qui et Lorem deserunt culpa sit.\r\n","registered":"2016-03-31T02:17:13 -02:00","latitude":-68.407524,"longitude":-113.642067,"tags":["new issue","wontfix"]}
|
||||
{"id":18,"isActive":false,"balance":"$1,484.92","picture":"http://placehold.it/32x32","age":39,"color":"blue","name":"Leonard Tillman","gender":"male","email":"leonardtillman@chorizon.com","phone":"+1 (864) 541-3456","address":"985 Provost Street, Charco, New Hampshire, 8632","about":"Consectetur ut magna sit id officia nostrud ipsum. Lorem cupidatat laborum nostrud aliquip magna qui est cupidatat exercitation et. Officia qui magna commodo id cillum magna ut ad veniam sunt sint ex. Id minim do in do exercitation aliquip incididunt ex esse. Nisi aliqua quis excepteur qui aute excepteur dolore eu pariatur irure id eu cupidatat eiusmod. Aliqua amet et dolore enim et eiusmod qui irure pariatur qui officia adipisicing nulla duis.\r\n","registered":"2018-05-06T08:21:27 -02:00","latitude":-8.581801,"longitude":-61.910062,"tags":["wontfix","new issue","bug","bug"]}
|
||||
{"id":19,"isActive":true,"balance":"$3,572.55","picture":"http://placehold.it/32x32","age":33,"color":"brown","name":"Dale Payne","gender":"male","email":"dalepayne@chorizon.com","phone":"+1 (814) 469-3499","address":"536 Dare Court, Ironton, Arkansas, 8605","about":"Et velit cupidatat velit incididunt mollit. Occaecat do labore aliqua dolore excepteur occaecat ut veniam ad ullamco tempor. Ut anim laboris deserunt culpa esse. Pariatur Lorem nulla cillum cupidatat nostrud Lorem commodo reprehenderit ut est. In dolor cillum reprehenderit laboris incididunt ad reprehenderit aute ipsum officia id in consequat. Culpa exercitation voluptate fugiat est Lorem ipsum in dolore dolor consequat Lorem et.\r\n","registered":"2019-10-11T01:01:33 -02:00","latitude":-18.280968,"longitude":-126.091797,"tags":["bug","wontfix","wontfix","wontfix"]}
|
||||
{"id":20,"isActive":true,"balance":"$1,986.48","picture":"http://placehold.it/32x32","age":38,"color":"Green","name":"Florence Long","gender":"female","email":"florencelong@chorizon.com","phone":"+1 (972) 557-3858","address":"519 Hendrickson Street, Templeton, Hawaii, 2389","about":"Quis officia occaecat veniam veniam. Ex minim enim labore cupidatat qui. Proident esse deserunt laborum laboris sunt nostrud.\r\n","registered":"2016-05-02T09:18:59 -02:00","latitude":-27.110866,"longitude":-45.09445,"tags":[]}
|
||||
{"id":21,"isActive":true,"balance":"$1,440.09","picture":"http://placehold.it/32x32","age":40,"color":"blue","name":"Levy Whitley","gender":"male","email":"levywhitley@chorizon.com","phone":"+1 (911) 458-2411","address":"187 Thomas Street, Hachita, North Carolina, 2989","about":"Velit laboris non minim elit sint deserunt fugiat. Aute minim ex commodo aute cillum aliquip fugiat pariatur nulla eiusmod pariatur consectetur. Qui ex ea qui laborum veniam adipisicing magna minim ut. In irure anim voluptate mollit et. Adipisicing labore ea mollit magna aliqua culpa velit est. Excepteur nisi veniam enim velit in ad officia irure laboris.\r\n","registered":"2014-04-30T07:31:38 -02:00","latitude":-6.537315,"longitude":171.813536,"tags":["bug"]}
|
||||
{"id":22,"isActive":false,"balance":"$2,938.57","picture":"http://placehold.it/32x32","age":35,"color":"blue","name":"Bernard Mcfarland","gender":"male","email":"bernardmcfarland@chorizon.com","phone":"+1 (979) 442-3386","address":"409 Hall Street, Keyport, Federated States Of Micronesia, 7011","about":"Reprehenderit irure aute et anim ullamco enim est tempor id ipsum mollit veniam aute ullamco. Consectetur dolor velit tempor est reprehenderit ut id non est ullamco voluptate. Commodo aute ullamco culpa non voluptate incididunt non culpa culpa nisi id proident cupidatat.\r\n","registered":"2017-08-10T10:07:59 -02:00","latitude":63.766795,"longitude":68.177069,"tags":[]}
|
||||
{"id":23,"isActive":true,"balance":"$1,678.49","picture":"http://placehold.it/32x32","age":31,"color":"brown","name":"Blanca Mcclain","gender":"female","email":"blancamcclain@chorizon.com","phone":"+1 (976) 439-2772","address":"176 Crooke Avenue, Valle, Virginia, 5373","about":"Aliquip sunt irure ut consectetur elit. Cillum amet incididunt et anim elit in incididunt adipisicing fugiat veniam esse veniam. Nisi qui sit occaecat tempor nostrud est aute cillum anim excepteur laboris magna in. Fugiat fugiat veniam cillum laborum ut pariatur amet nulla nulla. Nostrud mollit in laborum minim exercitation aute. Lorem aute ipsum laboris est adipisicing qui ullamco tempor adipisicing cupidatat mollit.\r\n","registered":"2015-10-12T11:57:28 -02:00","latitude":-8.944564,"longitude":-150.711709,"tags":["bug","wontfix","good first issue"]}
|
||||
{"id":24,"isActive":true,"balance":"$2,276.87","picture":"http://placehold.it/32x32","age":28,"color":"brown","name":"Espinoza Ford","gender":"male","email":"espinozaford@chorizon.com","phone":"+1 (945) 429-3975","address":"137 Bowery Street, Itmann, District Of Columbia, 1864","about":"Deserunt nisi aliquip esse occaecat laborum qui aliqua excepteur ea cupidatat dolore magna consequat. Culpa aliquip cillum incididunt proident est officia consequat duis. Elit tempor ut cupidatat nisi ea sint non labore aliquip amet. Deserunt labore cupidatat laboris dolor duis occaecat velit aliquip reprehenderit esse. Sit ad qui consectetur id anim nisi amet eiusmod.\r\n","registered":"2014-03-26T02:16:08 -01:00","latitude":-37.137666,"longitude":-51.811757,"tags":["wontfix","bug"]}
|
||||
{"id":25,"isActive":true,"balance":"$3,973.43","picture":"http://placehold.it/32x32","age":29,"color":"Green","name":"Sykes Conley","gender":"male","email":"sykesconley@chorizon.com","phone":"+1 (851) 401-3916","address":"345 Grand Street, Woodlands, Missouri, 4461","about":"Pariatur ullamco duis reprehenderit ad sit dolore. Dolore ex fugiat labore incididunt nostrud. Minim deserunt officia sunt enim magna elit veniam reprehenderit nisi cupidatat dolor eiusmod. Veniam laboris sint cillum et laboris nostrud culpa laboris anim. Incididunt velit pariatur cupidatat sit dolore in. Voluptate consectetur officia id nostrud velit mollit dolor. Id laboris consectetur culpa sunt pariatur minim sunt laboris sit.\r\n","registered":"2015-09-12T06:03:56 -02:00","latitude":67.282955,"longitude":-64.341323,"tags":["wontfix"]}
|
||||
{"id":26,"isActive":false,"balance":"$1,431.50","picture":"http://placehold.it/32x32","age":35,"color":"blue","name":"Barlow Duran","gender":"male","email":"barlowduran@chorizon.com","phone":"+1 (995) 436-2562","address":"481 Everett Avenue, Allison, Nebraska, 3065","about":"Proident quis eu officia adipisicing aliquip. Lorem laborum magna dolor et incididunt cillum excepteur et amet. Veniam consectetur officia fugiat magna consequat dolore elit aute exercitation fugiat excepteur ullamco. Sit qui proident reprehenderit ea ad qui culpa exercitation reprehenderit anim cupidatat. Nulla et duis Lorem cillum duis pariatur amet voluptate labore ut aliqua mollit anim ea. Nostrud incididunt et proident adipisicing non consequat tempor ullamco adipisicing incididunt. Incididunt cupidatat tempor fugiat officia qui eiusmod reprehenderit.\r\n","registered":"2017-06-29T04:28:43 -02:00","latitude":-38.70606,"longitude":55.02816,"tags":["new issue"]}
|
||||
{"id":27,"isActive":true,"balance":"$3,478.27","picture":"http://placehold.it/32x32","age":31,"color":"blue","name":"Schwartz Morgan","gender":"male","email":"schwartzmorgan@chorizon.com","phone":"+1 (861) 507-2067","address":"451 Lincoln Road, Fairlee, Washington, 2717","about":"Labore eiusmod sint dolore sunt eiusmod esse et in id aliquip. Aliqua consequat occaecat laborum labore ipsum enim non nostrud adipisicing adipisicing cillum occaecat. Duis minim est culpa sunt nulla ullamco adipisicing magna irure. Occaecat quis irure eiusmod fugiat quis commodo reprehenderit labore cillum commodo id et.\r\n","registered":"2016-05-10T08:34:54 -02:00","latitude":-75.886403,"longitude":93.044471,"tags":["bug","bug","wontfix","wontfix"]}
|
||||
{"id":28,"isActive":true,"balance":"$2,825.59","picture":"http://placehold.it/32x32","age":32,"color":"blue","name":"Kristy Leon","gender":"female","email":"kristyleon@chorizon.com","phone":"+1 (948) 465-2563","address":"594 Macon Street, Floris, South Dakota, 3565","about":"Proident veniam voluptate magna id do. Laboris enim dolor culpa quis. Esse voluptate elit commodo duis incididunt velit aliqua. Qui aute commodo incididunt elit eu Lorem dolore. Non esse duis do reprehenderit culpa minim. Ullamco consequat id do exercitation exercitation mollit ipsum velit eiusmod quis.\r\n","registered":"2014-12-14T04:10:29 -01:00","latitude":-50.01615,"longitude":-68.908804,"tags":["wontfix","good first issue"]}
|
||||
{"id":29,"isActive":false,"balance":"$3,028.03","picture":"http://placehold.it/32x32","age":39,"color":"blue","name":"Ashley Pittman","gender":"male","email":"ashleypittman@chorizon.com","phone":"+1 (928) 507-3523","address":"646 Adelphi Street, Clara, Colorado, 6056","about":"Incididunt cillum consectetur nulla sit sit labore nulla sit. Ullamco nisi mollit reprehenderit tempor irure in Lorem duis. Sunt eu aute laboris dolore commodo ipsum sint cupidatat veniam amet culpa incididunt aute ad. Quis dolore aliquip id aute mollit eiusmod nisi ipsum ut labore adipisicing do culpa.\r\n","registered":"2016-01-07T10:40:48 -01:00","latitude":-58.766037,"longitude":-124.828485,"tags":["wontfix"]}
|
||||
{"id":30,"isActive":true,"balance":"$2,021.11","picture":"http://placehold.it/32x32","age":32,"color":"blue","name":"Stacy Espinoza","gender":"female","email":"stacyespinoza@chorizon.com","phone":"+1 (999) 487-3253","address":"931 Alabama Avenue, Bangor, Alaska, 8215","about":"Id reprehenderit cupidatat exercitation anim ad nisi irure. Minim est proident mollit laborum. Duis ad duis eiusmod quis.\r\n","registered":"2014-07-16T06:15:53 -02:00","latitude":41.560197,"longitude":177.697,"tags":["new issue","new issue","bug"]}
|
||||
{"id":31,"isActive":false,"balance":"$3,609.82","picture":"http://placehold.it/32x32","age":32,"color":"blue","name":"Vilma Garza","gender":"female","email":"vilmagarza@chorizon.com","phone":"+1 (944) 585-2021","address":"565 Tech Place, Sedley, Puerto Rico, 858","about":"Excepteur et fugiat mollit incididunt cupidatat. Mollit nisi veniam sint eu exercitation amet labore. Voluptate est magna est amet qui minim excepteur cupidatat dolor quis id excepteur aliqua reprehenderit. Proident nostrud ex veniam officia nisi enim occaecat ex magna officia id consectetur ad eu. In et est reprehenderit cupidatat ad minim veniam proident nulla elit nisi veniam proident ex. Eu in irure sit veniam amet incididunt fugiat proident quis ullamco laboris.\r\n","registered":"2017-06-30T07:43:52 -02:00","latitude":-12.574889,"longitude":-54.771186,"tags":["new issue","wontfix","wontfix"]}
|
||||
{"id":32,"isActive":false,"balance":"$2,882.34","picture":"http://placehold.it/32x32","age":38,"color":"brown","name":"June Dunlap","gender":"female","email":"junedunlap@chorizon.com","phone":"+1 (997) 504-2937","address":"353 Cozine Avenue, Goodville, Indiana, 1438","about":"Non dolore ut Lorem dolore amet veniam fugiat reprehenderit ut amet ea ut. Non aliquip cillum ad occaecat non et sint quis proident velit laborum ullamco et. Quis qui tempor eu voluptate et proident duis est commodo laboris ex enim. Nisi aliquip laboris nostrud veniam aliqua ullamco. Et officia proident dolor aliqua incididunt veniam proident.\r\n","registered":"2016-08-23T08:54:11 -02:00","latitude":-27.883363,"longitude":-163.919683,"tags":["new issue","new issue","bug","wontfix"]}
|
||||
{"id":33,"isActive":true,"balance":"$3,556.54","picture":"http://placehold.it/32x32","age":33,"color":"brown","name":"Cecilia Greer","gender":"female","email":"ceciliagreer@chorizon.com","phone":"+1 (977) 573-3498","address":"696 Withers Street, Lydia, Oklahoma, 3220","about":"Dolor pariatur veniam ad enim eiusmod fugiat ullamco nulla veniam. Dolore dolor sit excepteur veniam adipisicing adipisicing excepteur commodo qui reprehenderit magna exercitation enim reprehenderit. Cupidatat eu ullamco excepteur sint do. Et cupidatat ex adipisicing veniam eu tempor reprehenderit ut eiusmod amet proident veniam nostrud. Tempor ex enim mollit laboris magna tempor. Et aliqua nostrud esse pariatur quis. Ut pariatur ea ipsum pariatur.\r\n","registered":"2017-01-13T11:30:12 -01:00","latitude":60.467215,"longitude":84.684575,"tags":["wontfix","good first issue","good first issue","wontfix"]}
|
||||
{"id":34,"isActive":true,"balance":"$1,413.35","picture":"http://placehold.it/32x32","age":33,"color":"brown","name":"Mckay Schroeder","gender":"male","email":"mckayschroeder@chorizon.com","phone":"+1 (816) 480-3657","address":"958 Miami Court, Rehrersburg, Northern Mariana Islands, 567","about":"Amet do velit excepteur tempor sit eu voluptate. Excepteur amet culpa ipsum in pariatur mollit amet nisi veniam. Laboris elit consectetur id anim qui laboris. Reprehenderit mollit laboris occaecat esse sunt Lorem Lorem sunt occaecat.\r\n","registered":"2016-02-08T04:50:15 -01:00","latitude":-72.413287,"longitude":-159.254371,"tags":["good first issue"]}
|
||||
{"id":35,"isActive":true,"balance":"$2,306.53","picture":"http://placehold.it/32x32","age":34,"color":"blue","name":"Sawyer Mccormick","gender":"male","email":"sawyermccormick@chorizon.com","phone":"+1 (829) 569-3012","address":"749 Apollo Street, Eastvale, Texas, 7373","about":"Est irure ex occaecat aute. Lorem ad ullamco esse cillum deserunt qui proident anim officia dolore. Incididunt tempor cupidatat nulla cupidatat ullamco reprehenderit Lorem. Laboris tempor do pariatur sint non officia id qui deserunt amet Lorem pariatur consectetur exercitation. Adipisicing reprehenderit pariatur duis ex cupidatat cillum ad laboris ex. Sunt voluptate pariatur esse amet dolore minim aliquip reprehenderit nisi velit mollit.\r\n","registered":"2019-11-30T11:53:23 -01:00","latitude":-48.978194,"longitude":110.950191,"tags":["good first issue","new issue","new issue","bug"]}
|
||||
{"id":36,"isActive":false,"balance":"$1,844.54","picture":"http://placehold.it/32x32","age":37,"color":"brown","name":"Barbra Valenzuela","gender":"female","email":"barbravalenzuela@chorizon.com","phone":"+1 (992) 512-2649","address":"617 Schenck Court, Reinerton, Michigan, 2908","about":"Deserunt adipisicing nisi et amet aliqua amet. Veniam occaecat et elit excepteur veniam. Aute irure culpa nostrud occaecat. Excepteur sit aute mollit commodo. Do ex pariatur consequat sint Lorem veniam laborum excepteur. Non voluptate ex laborum enim irure. Adipisicing excepteur anim elit esse.\r\n","registered":"2019-03-29T01:59:31 -01:00","latitude":45.193723,"longitude":-12.486778,"tags":["new issue","new issue","wontfix","wontfix"]}
|
||||
{"id":37,"isActive":false,"balance":"$3,469.82","picture":"http://placehold.it/32x32","age":39,"color":"brown","name":"Opal Weiss","gender":"female","email":"opalweiss@chorizon.com","phone":"+1 (809) 400-3079","address":"535 Bogart Street, Frizzleburg, Arizona, 5222","about":"Reprehenderit nostrud minim adipisicing voluptate nisi consequat id sint. Proident tempor est esse cupidatat minim irure esse do do sint dolor. In officia duis et voluptate Lorem minim cupidatat ipsum enim qui dolor quis in Lorem. Aliquip commodo ex quis exercitation reprehenderit. Lorem id reprehenderit cillum adipisicing sunt ipsum incididunt incididunt.\r\n","registered":"2019-09-04T07:22:28 -02:00","latitude":72.50376,"longitude":61.656435,"tags":["bug","bug","good first issue","good first issue"]}
|
||||
{"id":38,"isActive":true,"balance":"$1,992.38","picture":"http://placehold.it/32x32","age":40,"color":"Green","name":"Christina Short","gender":"female","email":"christinashort@chorizon.com","phone":"+1 (884) 589-2705","address":"594 Willmohr Street, Dexter, Montana, 660","about":"Quis commodo eu dolor incididunt. Nisi magna mollit nostrud do consequat irure exercitation mollit aute deserunt. Magna aute quis occaecat incididunt deserunt tempor nostrud sint ullamco ipsum. Anim in occaecat exercitation laborum nostrud eiusmod reprehenderit ea culpa et sit. Culpa voluptate consectetur nostrud do eu fugiat excepteur officia pariatur enim duis amet.\r\n","registered":"2014-01-21T09:31:56 -01:00","latitude":-42.762739,"longitude":77.052349,"tags":["bug","new issue"]}
|
||||
{"id":39,"isActive":false,"balance":"$1,722.85","picture":"http://placehold.it/32x32","age":29,"color":"brown","name":"Golden Horton","gender":"male","email":"goldenhorton@chorizon.com","phone":"+1 (903) 426-2489","address":"191 Schenck Avenue, Mayfair, North Dakota, 5000","about":"Cillum velit aliqua velit in quis do mollit in et veniam. Nostrud proident non irure commodo. Ea culpa duis enim adipisicing do sint et est culpa reprehenderit officia laborum. Non et nostrud tempor nostrud nostrud ea duis esse laboris occaecat laborum. In eu ipsum sit tempor esse eiusmod enim aliquip aute. Officia ea anim ea ea. Consequat aute deserunt tempor nulla nisi tempor velit.\r\n","registered":"2015-08-19T02:56:41 -02:00","latitude":69.922534,"longitude":9.881433,"tags":["bug"]}
|
||||
{"id":40,"isActive":false,"balance":"$1,656.54","picture":"http://placehold.it/32x32","age":21,"color":"blue","name":"Stafford Emerson","gender":"male","email":"staffordemerson@chorizon.com","phone":"+1 (992) 455-2573","address":"523 Thornton Street, Conway, Vermont, 6331","about":"Adipisicing cupidatat elit minim elit nostrud elit non eiusmod sunt ut. Enim minim irure officia irure occaecat mollit eu nostrud eiusmod adipisicing sunt. Elit deserunt commodo minim dolor qui. Nostrud officia ex proident mollit et dolor tempor pariatur. Ex consequat tempor eiusmod irure mollit cillum laboris est veniam ea mollit deserunt. Tempor sit voluptate excepteur elit ullamco.\r\n","registered":"2019-02-16T04:07:08 -01:00","latitude":-29.143111,"longitude":-57.207703,"tags":["wontfix","good first issue","good first issue"]}
|
||||
{"id":41,"isActive":false,"balance":"$1,861.56","picture":"http://placehold.it/32x32","age":21,"color":"brown","name":"Salinas Gamble","gender":"male","email":"salinasgamble@chorizon.com","phone":"+1 (901) 525-2373","address":"991 Nostrand Avenue, Kansas, Mississippi, 6756","about":"Consequat tempor adipisicing cupidatat aliquip. Mollit proident incididunt ad ipsum laborum. Dolor in elit minim aliquip aliquip voluptate reprehenderit mollit eiusmod excepteur aliquip minim nulla cupidatat.\r\n","registered":"2017-08-21T05:47:53 -02:00","latitude":-22.593819,"longitude":-63.613004,"tags":["good first issue","bug","bug","wontfix"]}
|
||||
{"id":42,"isActive":true,"balance":"$3,179.74","picture":"http://placehold.it/32x32","age":34,"color":"brown","name":"Graciela Russell","gender":"female","email":"gracielarussell@chorizon.com","phone":"+1 (893) 464-3951","address":"361 Greenpoint Avenue, Shrewsbury, New Jersey, 4713","about":"Ex amet duis incididunt consequat minim dolore deserunt reprehenderit adipisicing in mollit aliqua adipisicing sunt. In ullamco eu qui est eiusmod qui. Fugiat esse est Lorem dolore nisi mollit exercitation. Aliquip occaecat esse exercitation ex non aute velit excepteur duis aliquip id. Velit id non aliquip fugiat minim qui exercitation culpa tempor consectetur. Minim dolor labore ea aute aute eu.\r\n","registered":"2015-05-18T09:52:56 -02:00","latitude":-14.634444,"longitude":12.931783,"tags":["wontfix","bug","wontfix"]}
|
||||
{"id":43,"isActive":true,"balance":"$1,777.38","picture":"http://placehold.it/32x32","age":25,"color":"blue","name":"Arnold Bender","gender":"male","email":"arnoldbender@chorizon.com","phone":"+1 (945) 581-3808","address":"781 Lorraine Street, Gallina, American Samoa, 1832","about":"Et mollit laboris duis ut duis eiusmod aute laborum duis irure labore deserunt. Ut occaecat ullamco quis excepteur. Et commodo non sint laboris tempor laboris aliqua consequat magna ea aute minim tempor pariatur. Dolore occaecat qui irure Lorem nulla consequat non.\r\n","registered":"2018-12-23T02:26:30 -01:00","latitude":41.208579,"longitude":51.948925,"tags":["bug","good first issue","good first issue","wontfix"]}
|
||||
{"id":44,"isActive":true,"balance":"$2,893.45","picture":"http://placehold.it/32x32","age":22,"color":"Green","name":"Joni Spears","gender":"female","email":"jonispears@chorizon.com","phone":"+1 (916) 565-2124","address":"307 Harwood Place, Canterwood, Maryland, 2047","about":"Dolore consequat deserunt aliquip duis consequat minim occaecat enim est. Nulla aute reprehenderit est enim duis cillum ullamco aliquip eiusmod sunt. Labore eiusmod aliqua Lorem velit aliqua quis ex mollit mollit duis culpa et qui in. Cupidatat est id ullamco irure dolor nulla.\r\n","registered":"2015-03-01T12:38:28 -01:00","latitude":8.19071,"longitude":146.323808,"tags":["wontfix","new issue","good first issue","good first issue"]}
|
||||
{"id":45,"isActive":true,"balance":"$2,830.36","picture":"http://placehold.it/32x32","age":20,"color":"brown","name":"Irene Bennett","gender":"female","email":"irenebennett@chorizon.com","phone":"+1 (904) 431-2211","address":"353 Ridgecrest Terrace, Springdale, Marshall Islands, 2686","about":"Consectetur Lorem dolor reprehenderit sunt duis. Pariatur non velit velit veniam elit reprehenderit in. Aute quis Lorem quis pariatur Lorem incididunt nulla magna adipisicing. Et id occaecat labore officia occaecat occaecat adipisicing.\r\n","registered":"2018-04-17T05:18:51 -02:00","latitude":-36.435177,"longitude":-127.552573,"tags":["bug","wontfix"]}
|
||||
{"id":46,"isActive":true,"balance":"$1,348.04","picture":"http://placehold.it/32x32","age":34,"color":"Green","name":"Lawson Curtis","gender":"male","email":"lawsoncurtis@chorizon.com","phone":"+1 (896) 532-2172","address":"942 Gerritsen Avenue, Southmont, Kansas, 8915","about":"Amet consectetur minim aute nostrud excepteur sint labore in culpa. Mollit qui quis ea amet sint ex incididunt nulla. Elit id esse ea consectetur laborum consequat occaecat aute consectetur ex. Commodo duis aute elit occaecat cupidatat non consequat ad officia qui dolore nostrud reprehenderit. Occaecat velit velit adipisicing exercitation consectetur. Incididunt et amet nostrud tempor do esse ullamco est Lorem irure. Eu aliqua eu exercitation sint.\r\n","registered":"2016-08-23T01:41:09 -02:00","latitude":-48.783539,"longitude":20.492944,"tags":[]}
|
||||
{"id":47,"isActive":true,"balance":"$1,132.41","picture":"http://placehold.it/32x32","age":38,"color":"Green","name":"Goff May","gender":"male","email":"goffmay@chorizon.com","phone":"+1 (859) 453-3415","address":"225 Rutledge Street, Boonville, Massachusetts, 4081","about":"Sint occaecat velit anim sint reprehenderit est. Adipisicing ea pariatur amet id non ex. Aute id laborum tempor aliquip magna ex eu incididunt aliquip eiusmod elit quis dolor. Anim est minim deserunt amet exercitation nulla elit nulla nulla culpa ullamco. Velit consectetur ipsum amet proident labore excepteur ut id excepteur voluptate commodo. Exercitation et laboris labore esse est laboris consectetur et sint.\r\n","registered":"2014-10-25T07:32:30 -02:00","latitude":13.079225,"longitude":76.215086,"tags":["bug"]}
|
||||
{"id":48,"isActive":true,"balance":"$1,201.87","picture":"http://placehold.it/32x32","age":38,"color":"Green","name":"Goodman Becker","gender":"male","email":"goodmanbecker@chorizon.com","phone":"+1 (825) 470-3437","address":"388 Seigel Street, Sisquoc, Kentucky, 8231","about":"Velit excepteur aute esse fugiat laboris aliqua magna. Est ex sit do labore ullamco aliquip. Duis ea commodo nostrud in fugiat. Aliqua consequat mollit dolore excepteur nisi ullamco commodo ea nostrud ea minim. Minim occaecat ut laboris ea consectetur veniam ipsum qui sit tempor incididunt anim amet eu. Velit sint incididunt eu adipisicing ipsum qui labore. Anim commodo labore reprehenderit aliquip labore elit minim deserunt amet exercitation officia non ea consectetur.\r\n","registered":"2019-09-05T04:49:03 -02:00","latitude":-23.792094,"longitude":-13.621221,"tags":["bug","bug","wontfix","new issue"]}
|
||||
{"id":49,"isActive":true,"balance":"$1,476.39","picture":"http://placehold.it/32x32","age":28,"color":"brown","name":"Maureen Dale","gender":"female","email":"maureendale@chorizon.com","phone":"+1 (984) 538-3684","address":"817 Newton Street, Bannock, Wyoming, 1468","about":"Tempor mollit exercitation excepteur cupidatat reprehenderit ad ex. Nulla laborum proident incididunt quis. Esse laborum deserunt qui anim. Sunt incididunt pariatur cillum anim proident eu ullamco dolor excepteur. Ullamco amet culpa nostrud adipisicing duis aliqua consequat duis non eu id mollit velit. Deserunt ullamco amet in occaecat.\r\n","registered":"2018-04-26T06:04:40 -02:00","latitude":-64.196802,"longitude":-117.396238,"tags":["wontfix"]}
|
||||
{"id":50,"isActive":true,"balance":"$1,947.08","picture":"http://placehold.it/32x32","age":21,"color":"Green","name":"Guerra Mcintyre","gender":"male","email":"guerramcintyre@chorizon.com","phone":"+1 (951) 536-2043","address":"423 Lombardy Street, Stewart, West Virginia, 908","about":"Sunt proident proident deserunt exercitation consectetur deserunt labore non commodo amet. Duis aute aliqua amet deserunt consectetur velit. Quis Lorem dolore occaecat deserunt reprehenderit non esse ullamco nostrud enim sunt ea fugiat. Elit amet veniam eu magna tempor. Mollit cupidatat laboris ex deserunt et labore sit tempor nostrud anim. Tempor aliqua occaecat voluptate reprehenderit eiusmod aliqua incididunt officia.\r\n","registered":"2015-07-16T05:11:42 -02:00","latitude":79.733743,"longitude":-20.602356,"tags":["bug","good first issue","good first issue"]}
|
||||
{"id":51,"isActive":true,"balance":"$2,960.90","picture":"http://placehold.it/32x32","age":23,"color":"blue","name":"Key Cervantes","gender":"male","email":"keycervantes@chorizon.com","phone":"+1 (931) 474-3865","address":"410 Barbey Street, Vernon, Oregon, 2328","about":"Duis amet minim eu consectetur laborum ad exercitation eiusmod nulla velit cillum consectetur. Nostrud aliqua cillum minim veniam quis do cupidatat mollit laborum. Culpa fugiat consectetur cillum non occaecat tempor non fugiat esse pariatur in ullamco. Occaecat amet officia et culpa officia deserunt in qui magna aute consequat eiusmod.\r\n","registered":"2019-12-15T12:13:35 -01:00","latitude":47.627647,"longitude":117.049918,"tags":["new issue"]}
|
||||
{"id":52,"isActive":false,"balance":"$1,884.02","picture":"http://placehold.it/32x32","age":35,"color":"blue","name":"Karen Nelson","gender":"female","email":"karennelson@chorizon.com","phone":"+1 (993) 528-3607","address":"930 Frank Court, Dunbar, New York, 8810","about":"Occaecat officia veniam consectetur aliqua laboris dolor irure nulla. Lorem ipsum sit nisi veniam mollit ea sint nisi irure. Eiusmod officia do laboris nostrud enim ullamco nulla officia in Lorem qui. Sint sunt incididunt quis reprehenderit incididunt. Sit dolore nulla consequat ea magna.\r\n","registered":"2014-06-23T09:21:44 -02:00","latitude":-59.059033,"longitude":76.565373,"tags":["new issue","bug"]}
|
||||
{"id":53,"isActive":true,"balance":"$3,559.55","picture":"http://placehold.it/32x32","age":32,"color":"brown","name":"Caitlin Burnett","gender":"female","email":"caitlinburnett@chorizon.com","phone":"+1 (945) 480-2796","address":"516 Senator Street, Emory, Iowa, 4145","about":"In aliqua ea esse in. Magna aute cupidatat culpa enim proident ad adipisicing laborum consequat exercitation nisi. Qui esse aliqua duis anim nulla esse enim nostrud ipsum tempor. Lorem deserunt ullamco do mollit culpa ipsum duis Lorem velit duis occaecat.\r\n","registered":"2019-01-09T02:26:31 -01:00","latitude":-82.774237,"longitude":42.316194,"tags":["bug","good first issue"]}
|
||||
{"id":54,"isActive":true,"balance":"$2,113.29","picture":"http://placehold.it/32x32","age":28,"color":"Green","name":"Richards Walls","gender":"male","email":"richardswalls@chorizon.com","phone":"+1 (865) 517-2982","address":"959 Brightwater Avenue, Stevens, Nevada, 2968","about":"Ad aute Lorem non pariatur anim ullamco ad amet eiusmod tempor velit. Mollit et tempor nisi aute adipisicing exercitation mollit do amet amet est fugiat enim. Ex voluptate nulla id tempor officia ullamco cillum dolor irure irure mollit et magna nisi. Pariatur voluptate qui laboris dolor id. Eu ipsum nulla dolore aute voluptate deserunt anim aliqua. Ut enim enim velit officia est nisi. Duis amet ut veniam aliquip minim tempor Lorem amet Lorem dolor duis.\r\n","registered":"2014-09-25T06:51:22 -02:00","latitude":80.09202,"longitude":87.49759,"tags":["wontfix","wontfix","bug"]}
|
||||
{"id":55,"isActive":true,"balance":"$1,977.66","picture":"http://placehold.it/32x32","age":36,"color":"brown","name":"Combs Stanley","gender":"male","email":"combsstanley@chorizon.com","phone":"+1 (827) 419-2053","address":"153 Beverley Road, Siglerville, South Carolina, 3666","about":"Commodo ullamco consequat eu ipsum eiusmod aute voluptate in. Ea laboris id deserunt nostrud pariatur et laboris minim tempor quis qui consequat non esse. Magna elit commodo mollit veniam Lorem enim nisi pariatur. Nisi non nisi adipisicing ea ipsum laborum dolore cillum. Amet do nisi esse laboris ipsum proident non veniam ullamco ea cupidatat sunt. Aliquip aute cillum quis laboris consectetur enim eiusmod nisi non id ullamco cupidatat sunt.\r\n","registered":"2019-08-22T07:53:15 -02:00","latitude":78.386181,"longitude":143.661058,"tags":[]}
|
||||
{"id":56,"isActive":false,"balance":"$3,886.12","picture":"http://placehold.it/32x32","age":23,"color":"brown","name":"Tucker Barry","gender":"male","email":"tuckerbarry@chorizon.com","phone":"+1 (808) 544-3433","address":"805 Jamaica Avenue, Cornfields, Minnesota, 3689","about":"Enim est sunt ullamco nulla aliqua commodo. Enim minim veniam non fugiat id tempor ad velit quis velit ad sunt consectetur laborum. Cillum deserunt tempor est adipisicing Lorem esse qui. Magna quis sunt cillum ea officia adipisicing eiusmod eu et nisi consectetur.\r\n","registered":"2016-08-29T07:28:00 -02:00","latitude":71.701551,"longitude":9.903068,"tags":[]}
|
||||
{"id":57,"isActive":false,"balance":"$1,844.56","picture":"http://placehold.it/32x32","age":20,"color":"Green","name":"Kaitlin Conner","gender":"female","email":"kaitlinconner@chorizon.com","phone":"+1 (862) 467-2666","address":"501 Knight Court, Joppa, Rhode Island, 274","about":"Occaecat id reprehenderit pariatur ea. Incididunt laborum reprehenderit ipsum velit labore excepteur nostrud voluptate officia ut culpa. Sint sunt in qui duis cillum aliqua do ullamco. Non do aute excepteur non labore sint consectetur tempor ad ea fugiat commodo labore. Dolor tempor culpa Lorem voluptate esse nostrud anim tempor irure reprehenderit. Deserunt ipsum cillum fugiat ut labore labore anim. In aliqua sunt dolore irure reprehenderit voluptate commodo consequat mollit amet laboris sit anim.\r\n","registered":"2019-05-30T06:38:24 -02:00","latitude":15.613464,"longitude":171.965629,"tags":[]}
|
||||
{"id":58,"isActive":true,"balance":"$2,876.10","picture":"http://placehold.it/32x32","age":38,"color":"Green","name":"Mamie Fischer","gender":"female","email":"mamiefischer@chorizon.com","phone":"+1 (948) 545-3901","address":"599 Hunterfly Place, Haena, Georgia, 6005","about":"Cillum eu aliquip ipsum anim in dolore labore ea. Laboris velit esse ea ea aute do adipisicing ullamco elit laborum aute tempor. Esse consectetur quis irure occaecat nisi cillum et consectetur cillum cillum quis quis commodo.\r\n","registered":"2019-05-27T05:07:10 -02:00","latitude":70.915079,"longitude":-48.813584,"tags":["bug","wontfix","wontfix","good first issue"]}
|
||||
{"id":59,"isActive":true,"balance":"$1,921.58","picture":"http://placehold.it/32x32","age":31,"color":"Green","name":"Harper Carson","gender":"male","email":"harpercarson@chorizon.com","phone":"+1 (912) 430-3243","address":"883 Dennett Place, Knowlton, New Mexico, 9219","about":"Exercitation minim esse proident cillum velit et deserunt incididunt adipisicing minim. Cillum Lorem consectetur laborum id consequat exercitation velit. Magna dolor excepteur sunt deserunt dolor ullamco non sint proident ipsum. Reprehenderit voluptate sit veniam consectetur ea sunt duis labore deserunt ipsum aute. Eiusmod aliqua anim voluptate id duis tempor aliqua commodo sunt. Do officia ea consectetur nostrud eiusmod laborum.\r\n","registered":"2019-12-07T07:33:15 -01:00","latitude":-60.812605,"longitude":-27.129016,"tags":["bug","new issue"]}
|
||||
{"id":60,"isActive":true,"balance":"$1,770.93","picture":"http://placehold.it/32x32","age":23,"color":"brown","name":"Jody Herrera","gender":"female","email":"jodyherrera@chorizon.com","phone":"+1 (890) 583-3222","address":"261 Jay Street, Strykersville, Ohio, 9248","about":"Sit adipisicing pariatur irure non sint cupidatat ex ipsum pariatur exercitation ea. Enim consequat enim eu eu sint eu elit ex esse aliquip. Pariatur ipsum dolore veniam nisi id tempor elit exercitation dolore ad fugiat labore velit.\r\n","registered":"2016-05-21T01:00:02 -02:00","latitude":-36.846586,"longitude":131.156223,"tags":[]}
|
||||
{"id":61,"isActive":false,"balance":"$2,813.41","picture":"http://placehold.it/32x32","age":37,"color":"Green","name":"Charles Castillo","gender":"male","email":"charlescastillo@chorizon.com","phone":"+1 (934) 467-2108","address":"675 Morton Street, Rew, Pennsylvania, 137","about":"Velit amet laborum amet sunt sint sit cupidatat deserunt dolor laborum consectetur veniam. Minim cupidatat amet exercitation nostrud ex deserunt ad Lorem amet aute consectetur labore reprehenderit. Minim mollit aliqua et deserunt ex nisi. Id irure dolor labore consequat ipsum consectetur.\r\n","registered":"2019-06-10T02:54:22 -02:00","latitude":-16.423202,"longitude":-146.293752,"tags":["new issue","new issue"]}
|
||||
{"id":62,"isActive":true,"balance":"$3,341.35","picture":"http://placehold.it/32x32","age":33,"color":"blue","name":"Estelle Ramirez","gender":"female","email":"estelleramirez@chorizon.com","phone":"+1 (816) 459-2073","address":"636 Nolans Lane, Camptown, California, 7794","about":"Dolor proident incididunt ex labore quis ullamco duis. Sit esse laboris nisi eu voluptate nulla cupidatat nulla fugiat veniam. Culpa cillum est esse dolor consequat. Pariatur ex sit irure qui do fugiat. Fugiat culpa veniam est nisi excepteur quis cupidatat et minim in esse minim dolor et. Anim aliquip labore dolor occaecat nisi sunt dolore pariatur veniam nostrud est ut.\r\n","registered":"2015-02-14T01:05:50 -01:00","latitude":-46.591249,"longitude":-83.385587,"tags":["good first issue","bug"]}
|
||||
{"id":63,"isActive":true,"balance":"$2,478.30","picture":"http://placehold.it/32x32","age":21,"color":"blue","name":"Knowles Hebert","gender":"male","email":"knowleshebert@chorizon.com","phone":"+1 (819) 409-2308","address":"361 Kathleen Court, Gratton, Connecticut, 7254","about":"Esse mollit nulla eiusmod esse duis non proident excepteur labore. Nisi ex culpa do mollit dolor ea deserunt elit anim ipsum nostrud. Cupidatat nostrud duis ipsum dolore amet et. Veniam in cillum ea cillum deserunt excepteur officia laboris nulla. Commodo incididunt aliquip qui sunt dolore occaecat labore do laborum irure. Labore culpa duis pariatur reprehenderit ad laboris occaecat anim cillum et fugiat ea.\r\n","registered":"2016-03-08T08:34:52 -01:00","latitude":71.042482,"longitude":152.460406,"tags":["good first issue","wontfix"]}
|
||||
{"id":64,"isActive":false,"balance":"$2,559.09","picture":"http://placehold.it/32x32","age":28,"color":"brown","name":"Thelma Mckenzie","gender":"female","email":"thelmamckenzie@chorizon.com","phone":"+1 (941) 596-2777","address":"202 Leonard Street, Riverton, Illinois, 8577","about":"Non ad ipsum elit commodo fugiat Lorem ipsum reprehenderit. Commodo incididunt officia cillum eiusmod officia proident ea incididunt ullamco magna commodo consectetur dolor. Nostrud esse nisi ea laboris. Veniam et dolore nulla excepteur pariatur laborum non. Eiusmod reprehenderit do tempor esse eu eu aliquip. Magna quis consectetur ipsum adipisicing mollit elit ad elit.\r\n","registered":"2020-04-14T12:43:06 -02:00","latitude":16.026129,"longitude":105.464476,"tags":[]}
|
||||
{"id":65,"isActive":true,"balance":"$1,025.08","picture":"http://placehold.it/32x32","age":34,"color":"blue","name":"Carole Rowland","gender":"female","email":"carolerowland@chorizon.com","phone":"+1 (862) 558-3448","address":"941 Melba Court, Bluetown, Florida, 9555","about":"Ullamco occaecat ipsum aliqua sit proident eu. Occaecat ut consectetur proident culpa aliqua excepteur quis qui anim irure sit proident mollit irure. Proident cupidatat deserunt dolor adipisicing.\r\n","registered":"2014-12-01T05:55:35 -01:00","latitude":-0.191998,"longitude":43.389652,"tags":["wontfix"]}
|
||||
{"id":66,"isActive":true,"balance":"$1,061.49","picture":"http://placehold.it/32x32","age":35,"color":"brown","name":"Higgins Aguilar","gender":"male","email":"higginsaguilar@chorizon.com","phone":"+1 (911) 540-3791","address":"132 Sackman Street, Layhill, Guam, 8729","about":"Anim ea dolore exercitation minim. Proident cillum non deserunt cupidatat veniam non occaecat aute ullamco irure velit laboris ex aliquip. Voluptate incididunt non ex nulla est ipsum. Amet anim do velit sunt irure sint minim nisi occaecat proident tempor elit exercitation nostrud.\r\n","registered":"2015-04-05T02:10:07 -02:00","latitude":74.702813,"longitude":151.314972,"tags":["bug"]}
|
||||
{"id":67,"isActive":true,"balance":"$3,510.14","picture":"http://placehold.it/32x32","age":28,"color":"brown","name":"Ilene Gillespie","gender":"female","email":"ilenegillespie@chorizon.com","phone":"+1 (937) 575-2676","address":"835 Lake Street, Naomi, Alabama, 4131","about":"Quis laborum consequat id cupidatat exercitation aute ad ex nulla dolore velit qui proident minim. Et do consequat nisi eiusmod exercitation exercitation enim voluptate elit ullamco. Cupidatat ut adipisicing consequat aute est voluptate sit ipsum culpa ullamco. Ex pariatur ex qui quis qui.\r\n","registered":"2015-06-28T09:41:45 -02:00","latitude":71.573342,"longitude":-95.295989,"tags":["wontfix","wontfix"]}
|
||||
{"id":68,"isActive":false,"balance":"$1,539.98","picture":"http://placehold.it/32x32","age":24,"color":"Green","name":"Angelina Dyer","gender":"female","email":"angelinadyer@chorizon.com","phone":"+1 (948) 574-3949","address":"575 Division Place, Gorham, Louisiana, 3458","about":"Cillum magna eu est veniam incididunt laboris laborum elit mollit incididunt proident non mollit. Dolor mollit culpa ullamco dolore aliqua adipisicing culpa officia. Reprehenderit minim nisi fugiat consectetur dolore.\r\n","registered":"2014-07-08T06:34:36 -02:00","latitude":-85.649593,"longitude":66.126018,"tags":["good first issue"]}
|
||||
{"id":69,"isActive":true,"balance":"$3,367.69","picture":"http://placehold.it/32x32","age":30,"color":"brown","name":"Marks Burt","gender":"male","email":"marksburt@chorizon.com","phone":"+1 (895) 497-3138","address":"819 Village Road, Wadsworth, Delaware, 6099","about":"Fugiat tempor aute voluptate proident exercitation tempor esse dolor id. Duis aliquip exercitation Lorem elit magna sint sit. Culpa adipisicing occaecat aliqua officia reprehenderit laboris sint aliquip. Magna do sunt consequat excepteur nisi do commodo non. Cillum officia nostrud consequat excepteur elit proident in. Tempor ipsum in ut qui cupidatat exercitation est nulla exercitation voluptate.\r\n","registered":"2014-08-31T06:12:18 -02:00","latitude":26.854112,"longitude":-143.313948,"tags":["good first issue"]}
|
||||
{"id":70,"isActive":false,"balance":"$3,755.72","picture":"http://placehold.it/32x32","age":23,"color":"blue","name":"Glass Perkins","gender":"male","email":"glassperkins@chorizon.com","phone":"+1 (923) 486-3725","address":"899 Roosevelt Court, Belleview, Idaho, 1737","about":"Esse magna id labore sunt qui eu enim esse cillum consequat enim eu culpa enim. Duis veniam cupidatat deserunt sunt irure ad Lorem proident aliqua mollit. Laborum mollit aute nulla est. Sunt id proident incididunt ipsum et dolor consectetur laborum enim dolor officia dolore laborum. Est commodo duis et ea consequat labore id id eu aliqua. Qui veniam sit eu aliquip ad sit dolor ullamco et laborum voluptate quis fugiat ex. Exercitation dolore cillum amet ad nisi consectetur occaecat sit aliqua laborum qui proident aliqua exercitation.\r\n","registered":"2015-05-22T05:44:33 -02:00","latitude":54.27147,"longitude":-65.065604,"tags":["wontfix"]}
|
||||
{"id":71,"isActive":true,"balance":"$3,381.63","picture":"http://placehold.it/32x32","age":38,"color":"Green","name":"Candace Sawyer","gender":"female","email":"candacesawyer@chorizon.com","phone":"+1 (830) 404-2636","address":"334 Arkansas Drive, Bordelonville, Tennessee, 8449","about":"Et aliqua elit incididunt et aliqua. Deserunt ut elit proident ullamco ut. Ex exercitation amet non eu reprehenderit ea voluptate qui sit reprehenderit ad sint excepteur.\r\n","registered":"2014-04-04T08:45:00 -02:00","latitude":6.484262,"longitude":-37.054928,"tags":["new issue","new issue"]}
|
||||
{"id":72,"isActive":true,"balance":"$1,640.98","picture":"http://placehold.it/32x32","age":27,"color":"Green","name":"Hendricks Martinez","gender":"male","email":"hendricksmartinez@chorizon.com","phone":"+1 (857) 566-3245","address":"636 Agate Court, Newry, Utah, 3304","about":"Do sit culpa amet incididunt officia enim occaecat incididunt excepteur enim tempor deserunt qui. Excepteur adipisicing anim consectetur adipisicing proident anim laborum qui. Aliquip nostrud cupidatat sit ullamco.\r\n","registered":"2018-06-15T10:36:11 -02:00","latitude":86.746034,"longitude":10.347893,"tags":["new issue"]}
|
||||
{"id":73,"isActive":false,"balance":"$1,239.74","picture":"http://placehold.it/32x32","age":38,"color":"blue","name":"Eleanor Shepherd","gender":"female","email":"eleanorshepherd@chorizon.com","phone":"+1 (894) 567-2617","address":"670 Lafayette Walk, Darlington, Palau, 8803","about":"Adipisicing ad incididunt id veniam magna cupidatat et labore eu deserunt mollit. Lorem voluptate exercitation elit eu aliquip cupidatat occaecat anim excepteur reprehenderit est est. Ipsum excepteur ea mollit qui nisi laboris ex qui. Cillum velit culpa culpa commodo laboris nisi Lorem non elit deserunt incididunt. Officia quis velit nulla sint incididunt duis mollit tempor adipisicing qui officia eu nisi Lorem. Do proident pariatur ex enim nostrud eu aute esse deserunt eu velit quis culpa exercitation. Occaecat ad cupidatat ullamco consequat duis anim deserunt occaecat aliqua sunt consectetur ipsum magna.\r\n","registered":"2020-02-29T12:15:28 -01:00","latitude":35.749621,"longitude":-94.40842,"tags":["good first issue","new issue","new issue","bug"]}
|
||||
{"id":74,"isActive":true,"balance":"$1,180.90","picture":"http://placehold.it/32x32","age":36,"color":"Green","name":"Stark Wong","gender":"male","email":"starkwong@chorizon.com","phone":"+1 (805) 575-3055","address":"522 Bond Street, Bawcomville, Wisconsin, 324","about":"Aute qui sit incididunt eu adipisicing exercitation sunt nostrud. Id laborum incididunt proident ipsum est cillum esse. Officia ullamco eu ut Lorem do minim ea dolor consequat sit eu est voluptate. Id commodo cillum enim culpa aliquip ullamco nisi Lorem cillum ipsum cupidatat anim officia eu. Dolore sint elit labore pariatur. Officia duis nulla voluptate et nulla ut voluptate laboris eu commodo veniam qui veniam.\r\n","registered":"2020-01-25T10:47:48 -01:00","latitude":-80.452139,"longitude":160.72546,"tags":["wontfix"]}
|
||||
{"id":75,"isActive":false,"balance":"$1,913.42","picture":"http://placehold.it/32x32","age":24,"color":"Green","name":"Emma Jacobs","gender":"female","email":"emmajacobs@chorizon.com","phone":"+1 (899) 554-3847","address":"173 Tapscott Street, Esmont, Maine, 7450","about":"Laboris consequat consectetur tempor labore ullamco ullamco voluptate quis quis duis ut ad. In est irure quis amet sunt nulla ad ut sit labore ut eu quis duis. Nostrud cupidatat aliqua sunt occaecat minim id consequat officia deserunt laborum. Ea dolor reprehenderit laborum veniam exercitation est nostrud excepteur laborum minim id qui et.\r\n","registered":"2019-03-29T06:24:13 -01:00","latitude":-35.53722,"longitude":155.703874,"tags":[]}
|
||||
{"id":76,"isActive":false,"balance":"$1,274.29","picture":"http://placehold.it/32x32","age":25,"color":"Green","name":"Clarice Gardner","gender":"female","email":"claricegardner@chorizon.com","phone":"+1 (810) 407-3258","address":"894 Brooklyn Road, Utting, New Hampshire, 6404","about":"Elit occaecat aute ea adipisicing mollit cupidatat aliquip excepteur veniam minim. Sunt quis dolore in commodo aute esse quis. Lorem in cillum commodo eu anim commodo mollit. Adipisicing enim sunt adipisicing cupidatat adipisicing eiusmod eu do sit nisi.\r\n","registered":"2014-10-20T10:13:32 -02:00","latitude":17.11935,"longitude":65.38197,"tags":["new issue","wontfix"]}
|
||||
@@ -199,7 +199,7 @@ async fn error_add_api_key_no_header() {
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-authorization-header"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -223,7 +223,7 @@ async fn error_add_api_key_bad_key() {
|
||||
"message": "The provided API key is invalid.",
|
||||
"code": "invalid_api_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -245,9 +245,9 @@ async fn error_add_api_key_missing_parameter() {
|
||||
|
||||
let expected_response = json!({
|
||||
"message": "`indexes` field is mandatory.",
|
||||
"code": "missing_api_key_indexes",
|
||||
"code": "missing_parameter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-api-key-indexes"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_parameter"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -263,9 +263,9 @@ async fn error_add_api_key_missing_parameter() {
|
||||
|
||||
let expected_response = json!({
|
||||
"message": "`actions` field is mandatory.",
|
||||
"code": "missing_api_key_actions",
|
||||
"code": "missing_parameter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-api-key-actions"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_parameter"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -281,9 +281,9 @@ async fn error_add_api_key_missing_parameter() {
|
||||
|
||||
let expected_response = json!({
|
||||
"message": "`expiresAt` field is mandatory.",
|
||||
"code": "missing_api_key_expires_at",
|
||||
"code": "missing_parameter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-api-key-expires-at"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_parameter"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -307,7 +307,7 @@ async fn error_add_api_key_invalid_parameters_description() {
|
||||
"message": r#"`description` field value `{"name":"products"}` is invalid. It should be a string or specified as a null value."#,
|
||||
"code": "invalid_api_key_description",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-description"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_description"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -331,7 +331,7 @@ async fn error_add_api_key_invalid_parameters_name() {
|
||||
"message": r#"`name` field value `{"name":"products"}` is invalid. It should be a string or specified as a null value."#,
|
||||
"code": "invalid_api_key_name",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-name"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_name"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -355,7 +355,7 @@ async fn error_add_api_key_invalid_parameters_indexes() {
|
||||
"message": r#"`indexes` field value `{"name":"products"}` is invalid. It should be an array of string representing index names."#,
|
||||
"code": "invalid_api_key_indexes",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -380,7 +380,7 @@ async fn error_add_api_key_invalid_index_uids() {
|
||||
"message": r#"`invalid index # / \name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)."#,
|
||||
"code": "invalid_api_key_indexes",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -405,7 +405,7 @@ async fn error_add_api_key_invalid_parameters_actions() {
|
||||
"message": r#"`actions` field value `{"name":"products"}` is invalid. It should be an array of string representing action names."#,
|
||||
"code": "invalid_api_key_actions",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-actions"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -425,7 +425,7 @@ async fn error_add_api_key_invalid_parameters_actions() {
|
||||
"message": r#"`actions` field value `["doc.add"]` is invalid. It should be an array of string representing action names."#,
|
||||
"code": "invalid_api_key_actions",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-actions"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -449,7 +449,7 @@ async fn error_add_api_key_invalid_parameters_expires_at() {
|
||||
"message": r#"`expiresAt` field value `{"name":"products"}` is invalid. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'."#,
|
||||
"code": "invalid_api_key_expires_at",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_expires_at"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -473,7 +473,7 @@ async fn error_add_api_key_invalid_parameters_expires_at_in_the_past() {
|
||||
"message": r#"`expiresAt` field value `"2010-11-13T00:00:00Z"` is invalid. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'."#,
|
||||
"code": "invalid_api_key_expires_at",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_expires_at"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -498,7 +498,7 @@ async fn error_add_api_key_invalid_parameters_uid() {
|
||||
"message": r#"`uid` field value `"aaaaabbbbbccc"` is invalid. It should be a valid UUID v4 string or omitted."#,
|
||||
"code": "invalid_api_key_uid",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-uid"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_uid"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -527,7 +527,7 @@ async fn error_add_api_key_parameters_uid_already_exist() {
|
||||
"message": "`uid` field value `4bc0887a-0e41-4f3b-935d-0c451dcee9c8` is already an existing API key.",
|
||||
"code": "api_key_already_exists",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#api-key-already-exists"
|
||||
"link": "https://docs.meilisearch.com/errors#api_key_already_exists"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -622,7 +622,7 @@ async fn error_get_api_key_no_header() {
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-authorization-header"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -642,7 +642,7 @@ async fn error_get_api_key_bad_key() {
|
||||
"message": "The provided API key is invalid.",
|
||||
"code": "invalid_api_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -662,7 +662,7 @@ async fn error_get_api_key_not_found() {
|
||||
"message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.",
|
||||
"code": "api_key_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#api-key-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#api_key_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -757,7 +757,7 @@ async fn error_list_api_keys_no_header() {
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-authorization-header"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -775,7 +775,7 @@ async fn error_list_api_keys_bad_key() {
|
||||
"message": "The provided API key is invalid.",
|
||||
"code": "invalid_api_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -835,7 +835,7 @@ async fn error_delete_api_key_no_header() {
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-authorization-header"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -855,7 +855,7 @@ async fn error_delete_api_key_bad_key() {
|
||||
"message": "The provided API key is invalid.",
|
||||
"code": "invalid_api_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -875,7 +875,7 @@ async fn error_delete_api_key_not_found() {
|
||||
"message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.",
|
||||
"code": "api_key_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#api-key-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#api_key_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -1168,7 +1168,7 @@ async fn error_patch_api_key_indexes() {
|
||||
let expected = json!({"message": "The `indexes` field cannot be modified for the given resource.",
|
||||
"code": "immutable_field",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#immutable-field"
|
||||
"link": "https://docs.meilisearch.com/errors#immutable_field"
|
||||
});
|
||||
|
||||
assert_json_include!(actual: response, expected: expected);
|
||||
@@ -1223,7 +1223,7 @@ async fn error_patch_api_key_actions() {
|
||||
let expected = json!({"message": "The `actions` field cannot be modified for the given resource.",
|
||||
"code": "immutable_field",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#immutable-field"
|
||||
"link": "https://docs.meilisearch.com/errors#immutable_field"
|
||||
});
|
||||
|
||||
assert_json_include!(actual: response, expected: expected);
|
||||
@@ -1270,7 +1270,7 @@ async fn error_patch_api_key_expiration_date() {
|
||||
let expected = json!({"message": "The `expiresAt` field cannot be modified for the given resource.",
|
||||
"code": "immutable_field",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#immutable-field"
|
||||
"link": "https://docs.meilisearch.com/errors#immutable_field"
|
||||
});
|
||||
|
||||
assert_json_include!(actual: response, expected: expected);
|
||||
@@ -1292,7 +1292,7 @@ async fn error_patch_api_key_no_header() {
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-authorization-header"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -1315,7 +1315,7 @@ async fn error_patch_api_key_bad_key() {
|
||||
"message": "The provided API key is invalid.",
|
||||
"code": "invalid_api_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -1338,7 +1338,7 @@ async fn error_patch_api_key_not_found() {
|
||||
"message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.",
|
||||
"code": "api_key_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#api-key-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#api_key_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -1377,7 +1377,7 @@ async fn error_patch_api_key_indexes_invalid_parameters() {
|
||||
"message": "`description` field value `13` is invalid. It should be a string or specified as a null value.",
|
||||
"code": "invalid_api_key_description",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-description"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_description"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -1394,7 +1394,7 @@ async fn error_patch_api_key_indexes_invalid_parameters() {
|
||||
"message": "`name` field value `13` is invalid. It should be a string or specified as a null value.",
|
||||
"code": "invalid_api_key_name",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key-name"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_name"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -1408,7 +1408,7 @@ async fn error_access_api_key_routes_no_master_key_set() {
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-master-key"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
});
|
||||
let expected_code = 401;
|
||||
|
||||
@@ -1438,7 +1438,7 @@ async fn error_access_api_key_routes_no_master_key_set() {
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-master-key"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
});
|
||||
let expected_code = 401;
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ static INVALID_RESPONSE: Lazy<Value> = Lazy::new(|| {
|
||||
json!({"message": "The provided API key is invalid.",
|
||||
"code": "invalid_api_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key"
|
||||
})
|
||||
});
|
||||
|
||||
@@ -520,7 +520,7 @@ async fn error_creating_index_without_action() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
|
||||
// try to create a index via add documents route
|
||||
|
||||
@@ -37,7 +37,7 @@ async fn error_api_key_bad_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "invalid_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type");
|
||||
|
||||
// patch
|
||||
let req = test::TestRequest::patch()
|
||||
@@ -59,7 +59,7 @@ async fn error_api_key_bad_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "invalid_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -96,7 +96,7 @@ async fn error_api_key_empty_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "invalid_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type");
|
||||
|
||||
// patch
|
||||
let req = test::TestRequest::patch()
|
||||
@@ -118,7 +118,7 @@ async fn error_api_key_empty_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "invalid_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -154,7 +154,7 @@ async fn error_api_key_missing_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "missing_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing_content_type");
|
||||
|
||||
// patch
|
||||
let req = test::TestRequest::patch()
|
||||
@@ -175,7 +175,7 @@ async fn error_api_key_missing_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "missing_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing_content_type");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -200,7 +200,7 @@ async fn error_api_key_empty_payload() {
|
||||
assert_eq!(status_code, 400);
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
assert_eq!(response["message"], json!(r#"A json payload is missing."#));
|
||||
|
||||
// patch
|
||||
@@ -217,7 +217,7 @@ async fn error_api_key_empty_payload() {
|
||||
assert_eq!(status_code, 400);
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
assert_eq!(response["message"], json!(r#"A json payload is missing."#));
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ async fn error_api_key_malformed_payload() {
|
||||
assert_eq!(status_code, 400);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
assert_eq!(
|
||||
response["message"],
|
||||
json!(
|
||||
@@ -265,7 +265,7 @@ async fn error_api_key_malformed_payload() {
|
||||
assert_eq!(status_code, 400);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
assert_eq!(
|
||||
response["message"],
|
||||
json!(
|
||||
|
||||
@@ -56,7 +56,7 @@ static INVALID_RESPONSE: Lazy<Value> = Lazy::new(|| {
|
||||
json!({"message": "The provided API key is invalid.",
|
||||
"code": "invalid_api_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-api-key"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key"
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -25,30 +25,8 @@ impl Index<'_> {
|
||||
|
||||
pub async fn load_test_set(&self) -> u64 {
|
||||
let url = format!("/indexes/{}/documents", urlencode(self.uid.as_ref()));
|
||||
let (response, code) = self
|
||||
.service
|
||||
.post_str(
|
||||
url,
|
||||
include_str!("../assets/test_set.json"),
|
||||
("content-type", "application/json"),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, 202);
|
||||
let update_id = response["taskUid"].as_i64().unwrap();
|
||||
self.wait_task(update_id as u64).await;
|
||||
update_id as u64
|
||||
}
|
||||
|
||||
pub async fn load_test_set_ndjson(&self) -> u64 {
|
||||
let url = format!("/indexes/{}/documents", urlencode(self.uid.as_ref()));
|
||||
let (response, code) = self
|
||||
.service
|
||||
.post_str(
|
||||
url,
|
||||
include_str!("../assets/test_set.ndjson"),
|
||||
("content-type", "application/x-ndjson"),
|
||||
)
|
||||
.await;
|
||||
let (response, code) =
|
||||
self.service.post_str(url, include_str!("../assets/test_set.json")).await;
|
||||
assert_eq!(code, 202);
|
||||
let update_id = response["taskUid"].as_i64().unwrap();
|
||||
self.wait_task(update_id as u64).await;
|
||||
|
||||
@@ -8,7 +8,7 @@ use actix_web::dev::ServiceResponse;
|
||||
use actix_web::http::StatusCode;
|
||||
use byte_unit::{Byte, ByteUnit};
|
||||
use clap::Parser;
|
||||
use meilisearch::option::{IndexerOpts, MaxMemory, Opt};
|
||||
use meilisearch::option::{IndexerOpts, MaxMemory, Opt, RateLimiterConfig};
|
||||
use meilisearch::{analytics, create_app, setup_meilisearch};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::{json, Value};
|
||||
@@ -192,6 +192,10 @@ pub fn default_settings(dir: impl AsRef<Path>) -> Opt {
|
||||
max_task_db_size: Byte::from_unit(1.0, ByteUnit::GiB).unwrap(),
|
||||
http_payload_size_limit: Byte::from_unit(10.0, ByteUnit::MiB).unwrap(),
|
||||
snapshot_dir: ".".into(),
|
||||
rate_limiter_options: RateLimiterConfig {
|
||||
rate_limiting_disable_all: true,
|
||||
..Parser::parse_from(None as Option<&str>)
|
||||
},
|
||||
indexer_options: IndexerOpts {
|
||||
// memory has to be unlimited because several meilisearch are running in test context.
|
||||
max_indexing_memory: MaxMemory::unlimited(),
|
||||
|
||||
@@ -39,12 +39,11 @@ impl Service {
|
||||
&self,
|
||||
url: impl AsRef<str>,
|
||||
body: impl AsRef<str>,
|
||||
header: (&str, &str),
|
||||
) -> (Value, StatusCode) {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(url.as_ref())
|
||||
.set_payload(body.as_ref().to_string())
|
||||
.insert_header(header);
|
||||
.insert_header(("content-type", "application/json"));
|
||||
self.request(req).await
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ async fn error_json_bad_content_type() {
|
||||
"message": r#"A Content-Type header is missing. Accepted values for the Content-Type header are: `application/json`"#,
|
||||
"code": "missing_content_type",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-content-type",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_content_type",
|
||||
}),
|
||||
"when calling the route `{}` with no content-type",
|
||||
route,
|
||||
@@ -117,7 +117,7 @@ async fn error_json_bad_content_type() {
|
||||
"message": expected_error_message,
|
||||
"code": "invalid_content_type",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-content-type",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_content_type",
|
||||
}),
|
||||
"when calling the route `{}` with a content-type of `{}`",
|
||||
route,
|
||||
|
||||
@@ -193,7 +193,7 @@ async fn error_add_documents_test_bad_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "invalid_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type");
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -214,7 +214,7 @@ async fn error_add_documents_test_bad_content_types() {
|
||||
);
|
||||
assert_eq!(response["code"], "invalid_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#invalid_content_type");
|
||||
}
|
||||
|
||||
/// missing content-type must be refused
|
||||
@@ -248,7 +248,7 @@ async fn error_add_documents_test_no_content_type() {
|
||||
);
|
||||
assert_eq!(response["code"], "missing_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing_content_type");
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -268,7 +268,7 @@ async fn error_add_documents_test_no_content_type() {
|
||||
);
|
||||
assert_eq!(response["code"], "missing_content_type");
|
||||
assert_eq!(response["type"], "invalid_request");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing-content-type");
|
||||
assert_eq!(response["link"], "https://docs.meilisearch.com/errors#missing_content_type");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -297,7 +297,7 @@ async fn error_add_malformed_csv_documents() {
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -318,7 +318,7 @@ async fn error_add_malformed_csv_documents() {
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -347,7 +347,7 @@ async fn error_add_malformed_json_documents() {
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -368,7 +368,7 @@ async fn error_add_malformed_json_documents() {
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
|
||||
// truncate
|
||||
|
||||
@@ -393,7 +393,7 @@ async fn error_add_malformed_json_documents() {
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
|
||||
// add one more char to the long string to test if the truncating works.
|
||||
let document = format!("\"{}m\"", long);
|
||||
@@ -412,7 +412,7 @@ async fn error_add_malformed_json_documents() {
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -436,12 +436,12 @@ async fn error_add_malformed_ndjson_documents() {
|
||||
assert_eq!(
|
||||
response["message"],
|
||||
json!(
|
||||
r#"The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`."#
|
||||
r#"The `ndjson` payload provided is malformed. `Couldn't serialize document value: trailing characters at line 2 column 1`."#
|
||||
)
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -456,11 +456,11 @@ async fn error_add_malformed_ndjson_documents() {
|
||||
assert_eq!(status_code, 400);
|
||||
assert_eq!(
|
||||
response["message"],
|
||||
json!("The `ndjson` payload provided is malformed. `Couldn't serialize document value: key must be a string at line 2 column 2`.")
|
||||
json!("The `ndjson` payload provided is malformed. `Couldn't serialize document value: trailing characters at line 2 column 1`.")
|
||||
);
|
||||
assert_eq!(response["code"], json!("malformed_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#malformed_payload"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -484,7 +484,7 @@ async fn error_add_missing_payload_csv_documents() {
|
||||
assert_eq!(response["message"], json!(r#"A csv payload is missing."#));
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -500,7 +500,7 @@ async fn error_add_missing_payload_csv_documents() {
|
||||
assert_eq!(response["message"], json!(r#"A csv payload is missing."#));
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -524,7 +524,7 @@ async fn error_add_missing_payload_json_documents() {
|
||||
assert_eq!(response["message"], json!(r#"A json payload is missing."#));
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -540,7 +540,7 @@ async fn error_add_missing_payload_json_documents() {
|
||||
assert_eq!(response["message"], json!(r#"A json payload is missing."#));
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -564,7 +564,7 @@ async fn error_add_missing_payload_ndjson_documents() {
|
||||
assert_eq!(response["message"], json!(r#"A ndjson payload is missing."#));
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
|
||||
// put
|
||||
let req = test::TestRequest::put()
|
||||
@@ -580,7 +580,7 @@ async fn error_add_missing_payload_ndjson_documents() {
|
||||
assert_eq!(response["message"], json!(r#"A ndjson payload is missing."#));
|
||||
assert_eq!(response["code"], json!("missing_payload"));
|
||||
assert_eq!(response["type"], json!("invalid_request"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing-payload"));
|
||||
assert_eq!(response["link"], json!("https://docs.meilisearch.com/errors#missing_payload"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -639,7 +639,7 @@ async fn error_document_add_create_index_bad_uid() {
|
||||
"message": "`883 fj!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
|
||||
"code": "invalid_index_uid",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-index-uid"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_index_uid"
|
||||
});
|
||||
|
||||
assert_eq!(code, 400);
|
||||
@@ -737,22 +737,6 @@ async fn add_larger_dataset() {
|
||||
.await;
|
||||
assert_eq!(code, 200, "failed with `{}`", response);
|
||||
assert_eq!(response["results"].as_array().unwrap().len(), 77);
|
||||
|
||||
// x-ndjson add large test
|
||||
let server = Server::new().await;
|
||||
let index = server.index("test");
|
||||
let update_id = index.load_test_set_ndjson().await;
|
||||
let (response, code) = index.get_task(update_id).await;
|
||||
assert_eq!(code, 200);
|
||||
assert_eq!(response["status"], "succeeded");
|
||||
assert_eq!(response["type"], "documentAdditionOrUpdate");
|
||||
assert_eq!(response["details"]["indexedDocuments"], 77);
|
||||
assert_eq!(response["details"]["receivedDocuments"], 77);
|
||||
let (response, code) = index
|
||||
.get_all_documents(GetAllDocumentsOptions { limit: Some(1000), ..Default::default() })
|
||||
.await;
|
||||
assert_eq!(code, 200, "failed with `{}`", response);
|
||||
assert_eq!(response["results"].as_array().unwrap().len(), 77);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -781,7 +765,7 @@ async fn error_add_documents_bad_document_id() {
|
||||
assert_eq!(response["error"]["type"], json!("invalid_request"));
|
||||
assert_eq!(
|
||||
response["error"]["link"],
|
||||
json!("https://docs.meilisearch.com/errors#invalid-document-id")
|
||||
json!("https://docs.meilisearch.com/errors#invalid_document_id")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -809,7 +793,7 @@ async fn error_add_documents_missing_document_id() {
|
||||
assert_eq!(response["error"]["type"], json!("invalid_request"));
|
||||
assert_eq!(
|
||||
response["error"]["link"],
|
||||
json!("https://docs.meilisearch.com/errors#missing-document-id")
|
||||
json!("https://docs.meilisearch.com/errors#missing_document_id")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -843,7 +827,7 @@ async fn error_document_field_limit_reached() {
|
||||
"message": "A document cannot contain more than 65,535 fields.",
|
||||
"code": "document_fields_limit_reached",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#document-fields-limit-reached"
|
||||
"link": "https://docs.meilisearch.com/errors#document_fields_limit_reached"
|
||||
});
|
||||
|
||||
assert_eq!(response["error"], expected_error);
|
||||
@@ -889,7 +873,7 @@ async fn error_add_documents_payload_size() {
|
||||
"message": "The provided payload reached the size limit.",
|
||||
"code": "payload_too_large",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#payload-too-large"
|
||||
"link": "https://docs.meilisearch.com/errors#payload_too_large"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -912,104 +896,16 @@ async fn error_primary_key_inference() {
|
||||
index.wait_task(0).await;
|
||||
let (response, code) = index.get_task(0).await;
|
||||
assert_eq!(code, 200);
|
||||
assert_eq!(response["status"], "failed");
|
||||
|
||||
insta::assert_json_snapshot!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" },
|
||||
@r###"
|
||||
{
|
||||
"uid": 0,
|
||||
"indexUid": "test",
|
||||
"status": "failed",
|
||||
"type": "documentAdditionOrUpdate",
|
||||
"canceledBy": null,
|
||||
"details": {
|
||||
"receivedDocuments": 1,
|
||||
"indexedDocuments": 1
|
||||
},
|
||||
"error": {
|
||||
"message": "The primary key inference process failed because the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.",
|
||||
"code": "index_primary_key_no_candidate_found",
|
||||
let expected_error = json!({
|
||||
"message": r#"The primary key inference process failed because the engine did not find any fields containing `id` substring in their name. If your document identifier does not contain any `id` substring, you can set the primary key of the index."#,
|
||||
"code": "primary_key_inference_failed",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-primary-key-no-candidate-found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
"startedAt": "[date]",
|
||||
"finishedAt": "[date]"
|
||||
}
|
||||
"###);
|
||||
"link": "https://docs.meilisearch.com/errors#primary_key_inference_failed"
|
||||
});
|
||||
|
||||
let documents = json!([
|
||||
{
|
||||
"primary_id": "12",
|
||||
"object_id": "42",
|
||||
"id": "124",
|
||||
"title": "11",
|
||||
"desc": "foobar"
|
||||
}
|
||||
]);
|
||||
|
||||
index.add_documents(documents, None).await;
|
||||
index.wait_task(1).await;
|
||||
let (response, code) = index.get_task(1).await;
|
||||
assert_eq!(code, 200);
|
||||
|
||||
insta::assert_json_snapshot!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" },
|
||||
@r###"
|
||||
{
|
||||
"uid": 1,
|
||||
"indexUid": "test",
|
||||
"status": "failed",
|
||||
"type": "documentAdditionOrUpdate",
|
||||
"canceledBy": null,
|
||||
"details": {
|
||||
"receivedDocuments": 1,
|
||||
"indexedDocuments": 1
|
||||
},
|
||||
"error": {
|
||||
"message": "The primary key inference process failed because the engine found 3 fields ending with `id` in their name, such as 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.",
|
||||
"code": "index_primary_key_multiple_candidates_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-primary-key-multiple-candidates-found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
"startedAt": "[date]",
|
||||
"finishedAt": "[date]"
|
||||
}
|
||||
"###);
|
||||
|
||||
let documents = json!([
|
||||
{
|
||||
"primary_id": "12",
|
||||
"title": "11",
|
||||
"desc": "foobar"
|
||||
}
|
||||
]);
|
||||
|
||||
index.add_documents(documents, None).await;
|
||||
index.wait_task(2).await;
|
||||
let (response, code) = index.get_task(2).await;
|
||||
assert_eq!(code, 200);
|
||||
|
||||
insta::assert_json_snapshot!(response, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" },
|
||||
@r###"
|
||||
{
|
||||
"uid": 2,
|
||||
"indexUid": "test",
|
||||
"status": "succeeded",
|
||||
"type": "documentAdditionOrUpdate",
|
||||
"canceledBy": null,
|
||||
"details": {
|
||||
"receivedDocuments": 1,
|
||||
"indexedDocuments": 1
|
||||
},
|
||||
"error": null,
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
"startedAt": "[date]",
|
||||
"finishedAt": "[date]"
|
||||
}
|
||||
"###);
|
||||
assert_eq!(response["error"], expected_error);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
||||
@@ -95,7 +95,7 @@ async fn error_delete_batch_unexisting_index() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
assert_eq!(code, 202);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ async fn error_get_unexisting_document() {
|
||||
"message": "Document `1` not found.",
|
||||
"code": "document_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#document-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#document_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -90,7 +90,7 @@ async fn error_get_unexisting_index_all_documents() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
|
||||
@@ -13,7 +13,7 @@ async fn error_document_update_create_index_bad_uid() {
|
||||
"message": "`883 fj!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
|
||||
"code": "invalid_index_uid",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-index-uid"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_index_uid"
|
||||
});
|
||||
|
||||
assert_eq!(code, 400);
|
||||
@@ -167,7 +167,7 @@ async fn error_update_documents_bad_document_id() {
|
||||
assert_eq!(response["error"]["type"], json!("invalid_request"));
|
||||
assert_eq!(
|
||||
response["error"]["link"],
|
||||
json!("https://docs.meilisearch.com/errors#invalid-document-id")
|
||||
json!("https://docs.meilisearch.com/errors#invalid_document_id")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,6 +193,6 @@ async fn error_update_documents_missing_document_id() {
|
||||
assert_eq!(response["error"]["type"], "invalid_request");
|
||||
assert_eq!(
|
||||
response["error"]["link"],
|
||||
"https://docs.meilisearch.com/errors#missing-document-id"
|
||||
"https://docs.meilisearch.com/errors#missing_document_id"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ async fn error_create_existing_index() {
|
||||
"message": "Index `test` already exists.",
|
||||
"code": "index_already_exists",
|
||||
"type": "invalid_request",
|
||||
"link":"https://docs.meilisearch.com/errors#index-already-exists"
|
||||
"link":"https://docs.meilisearch.com/errors#index_already_exists"
|
||||
});
|
||||
|
||||
assert_eq!(response["error"], expected_response);
|
||||
@@ -192,7 +192,7 @@ async fn error_create_with_invalid_index_uid() {
|
||||
"message": "`test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
|
||||
"code": "invalid_index_uid",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-index-uid"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_index_uid"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
|
||||
@@ -35,7 +35,7 @@ async fn error_delete_unexisting_index() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
|
||||
let response = index.wait_task(0).await;
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn error_get_unexisting_index() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -190,7 +190,7 @@ async fn get_invalid_index_uid() {
|
||||
"message": "Index `this is not a valid index name` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ async fn error_get_stats_unexisting_index() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
|
||||
@@ -98,7 +98,7 @@ async fn error_update_existing_primary_key() {
|
||||
"message": "Index already has a primary key: `id`.",
|
||||
"code": "index_primary_key_already_exists",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-primary-key-already-exists"
|
||||
"link": "https://docs.meilisearch.com/errors#index_primary_key_already_exists"
|
||||
});
|
||||
|
||||
assert_eq!(response["error"], expected_response);
|
||||
@@ -117,7 +117,7 @@ async fn error_update_unexisting_index() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response["error"], expected_response);
|
||||
|
||||
@@ -12,7 +12,7 @@ async fn search_unexisting_index() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
});
|
||||
|
||||
index
|
||||
@@ -37,105 +37,25 @@ async fn search_unexisting_parameter() {
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn search_invalid_crop_marker() {
|
||||
async fn search_invalid_highlight_and_crop_tags() {
|
||||
let server = Server::new().await;
|
||||
let index = server.index("test");
|
||||
|
||||
// object
|
||||
let response = index.search_post(json!({"cropMarker": { "marker": "<crop>" }})).await;
|
||||
meili_snap::snapshot!(format!("{:#?}", response), @r###"
|
||||
(
|
||||
Object {
|
||||
"message": String("invalid type: Map `{\"marker\":\"<crop>\"}`, expected a String at `.cropMarker`."),
|
||||
"code": String("invalid_search_crop_marker"),
|
||||
"type": String("invalid_request"),
|
||||
"link": String("https://docs.meilisearch.com/errors#invalid-search-crop-marker"),
|
||||
},
|
||||
400,
|
||||
)
|
||||
"###);
|
||||
let fields = &["cropMarker", "highlightPreTag", "highlightPostTag"];
|
||||
|
||||
// array
|
||||
let response = index.search_post(json!({"cropMarker": ["marker", "<crop>"]})).await;
|
||||
meili_snap::snapshot!(format!("{:#?}", response), @r###"
|
||||
(
|
||||
Object {
|
||||
"message": String("invalid type: Sequence `[\"marker\",\"<crop>\"]`, expected a String at `.cropMarker`."),
|
||||
"code": String("invalid_search_crop_marker"),
|
||||
"type": String("invalid_request"),
|
||||
"link": String("https://docs.meilisearch.com/errors#invalid-search-crop-marker"),
|
||||
},
|
||||
400,
|
||||
)
|
||||
"###);
|
||||
}
|
||||
for field in fields {
|
||||
// object
|
||||
let (response, code) =
|
||||
index.search_post(json!({field.to_string(): {"marker": "<crop>"}})).await;
|
||||
assert_eq!(code, 400, "field {} passing object: {}", &field, response);
|
||||
assert_eq!(response["code"], "bad_request");
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn search_invalid_highlight_pre_tag() {
|
||||
let server = Server::new().await;
|
||||
let index = server.index("test");
|
||||
|
||||
// object
|
||||
let response = index.search_post(json!({"highlightPreTag": { "marker": "<em>" }})).await;
|
||||
meili_snap::snapshot!(format!("{:#?}", response), @r###"
|
||||
(
|
||||
Object {
|
||||
"message": String("invalid type: Map `{\"marker\":\"<em>\"}`, expected a String at `.highlightPreTag`."),
|
||||
"code": String("invalid_search_highlight_pre_tag"),
|
||||
"type": String("invalid_request"),
|
||||
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag"),
|
||||
},
|
||||
400,
|
||||
)
|
||||
"###);
|
||||
|
||||
// array
|
||||
let response = index.search_post(json!({"highlightPreTag": ["marker", "<em>"]})).await;
|
||||
meili_snap::snapshot!(format!("{:#?}", response), @r###"
|
||||
(
|
||||
Object {
|
||||
"message": String("invalid type: Sequence `[\"marker\",\"<em>\"]`, expected a String at `.highlightPreTag`."),
|
||||
"code": String("invalid_search_highlight_pre_tag"),
|
||||
"type": String("invalid_request"),
|
||||
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag"),
|
||||
},
|
||||
400,
|
||||
)
|
||||
"###);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn search_invalid_highlight_post_tag() {
|
||||
let server = Server::new().await;
|
||||
let index = server.index("test");
|
||||
|
||||
// object
|
||||
let response = index.search_post(json!({"highlightPostTag": { "marker": "</em>" }})).await;
|
||||
meili_snap::snapshot!(format!("{:#?}", response), @r###"
|
||||
(
|
||||
Object {
|
||||
"message": String("invalid type: Map `{\"marker\":\"</em>\"}`, expected a String at `.highlightPostTag`."),
|
||||
"code": String("invalid_search_highlight_post_tag"),
|
||||
"type": String("invalid_request"),
|
||||
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag"),
|
||||
},
|
||||
400,
|
||||
)
|
||||
"###);
|
||||
|
||||
// array
|
||||
let response = index.search_post(json!({"highlightPostTag": ["marker", "</em>"]})).await;
|
||||
meili_snap::snapshot!(format!("{:#?}", response), @r###"
|
||||
(
|
||||
Object {
|
||||
"message": String("invalid type: Sequence `[\"marker\",\"</em>\"]`, expected a String at `.highlightPostTag`."),
|
||||
"code": String("invalid_search_highlight_post_tag"),
|
||||
"type": String("invalid_request"),
|
||||
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag"),
|
||||
},
|
||||
400,
|
||||
)
|
||||
"###);
|
||||
// array
|
||||
let (response, code) =
|
||||
index.search_post(json!({field.to_string(): ["marker", "<crop>"]})).await;
|
||||
assert_eq!(code, 400, "field {} passing array: {}", &field, response);
|
||||
assert_eq!(response["code"], "bad_request");
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@@ -153,7 +73,7 @@ async fn filter_invalid_syntax_object() {
|
||||
"message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, or `_geoRadius` at `title & Glass`.\n1:14 title & Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": "title & Glass"}), |response, code| {
|
||||
@@ -178,7 +98,7 @@ async fn filter_invalid_syntax_array() {
|
||||
"message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, or `_geoRadius` at `title & Glass`.\n1:14 title & Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": ["title & Glass"]}), |response, code| {
|
||||
@@ -203,7 +123,7 @@ async fn filter_invalid_syntax_string() {
|
||||
"message": "Found unexpected characters at the end of the filter: `XOR title = Glass`. You probably forgot an `OR` or an `AND` rule.\n15:32 title = Glass XOR title = Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": "title = Glass XOR title = Glass"}), |response, code| {
|
||||
@@ -228,7 +148,7 @@ async fn filter_invalid_attribute_array() {
|
||||
"message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": ["many = Glass"]}), |response, code| {
|
||||
@@ -253,7 +173,7 @@ async fn filter_invalid_attribute_string() {
|
||||
"message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": "many = Glass"}), |response, code| {
|
||||
@@ -278,7 +198,7 @@ async fn filter_reserved_geo_attribute_array() {
|
||||
"message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the _geoRadius(latitude, longitude, distance) built-in rule to filter on _geo field coordinates.\n1:5 _geo = Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": ["_geo = Glass"]}), |response, code| {
|
||||
@@ -303,7 +223,7 @@ async fn filter_reserved_geo_attribute_string() {
|
||||
"message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the _geoRadius(latitude, longitude, distance) built-in rule to filter on _geo field coordinates.\n1:5 _geo = Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": "_geo = Glass"}), |response, code| {
|
||||
@@ -328,7 +248,7 @@ async fn filter_reserved_attribute_array() {
|
||||
"message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression.\n1:13 _geoDistance = Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": ["_geoDistance = Glass"]}), |response, code| {
|
||||
@@ -353,7 +273,7 @@ async fn filter_reserved_attribute_string() {
|
||||
"message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression.\n1:13 _geoDistance = Glass",
|
||||
"code": "invalid_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-filter"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_filter"
|
||||
});
|
||||
index
|
||||
.search(json!({"filter": "_geoDistance = Glass"}), |response, code| {
|
||||
@@ -378,7 +298,7 @@ async fn sort_geo_reserved_attribute() {
|
||||
"message": "`_geo` is a reserved keyword and thus can't be used as a sort expression. Use the _geoPoint(latitude, longitude) built-in rule to sort on _geo field coordinates.",
|
||||
"code": "invalid_sort",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-sort"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_sort"
|
||||
});
|
||||
index
|
||||
.search(
|
||||
@@ -408,7 +328,7 @@ async fn sort_reserved_attribute() {
|
||||
"message": "`_geoDistance` is a reserved keyword and thus can't be used as a sort expression.",
|
||||
"code": "invalid_sort",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-sort"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_sort"
|
||||
});
|
||||
index
|
||||
.search(
|
||||
@@ -438,7 +358,7 @@ async fn sort_unsortable_attribute() {
|
||||
"message": "Attribute `title` is not sortable. Available sortable attributes are: `id`.",
|
||||
"code": "invalid_sort",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-sort"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_sort"
|
||||
});
|
||||
index
|
||||
.search(
|
||||
@@ -468,7 +388,7 @@ async fn sort_invalid_syntax() {
|
||||
"message": "Invalid syntax for the sort parameter: expected expression ending by `:asc` or `:desc`, found `title`.",
|
||||
"code": "invalid_sort",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-sort"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_sort"
|
||||
});
|
||||
index
|
||||
.search(
|
||||
@@ -502,7 +422,7 @@ async fn sort_unset_ranking_rule() {
|
||||
"message": "The sort ranking rule must be specified in the ranking rules settings to use the sort parameter at search time.",
|
||||
"code": "invalid_sort",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-sort"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_sort"
|
||||
});
|
||||
index
|
||||
.search(
|
||||
|
||||
@@ -111,56 +111,3 @@ async fn hits_per_page_0_should_not_return_any_result() {
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn ensure_placeholder_search_hit_count_valid() {
|
||||
let server = Server::new().await;
|
||||
let index = server.index("basic");
|
||||
|
||||
let documents = json!([
|
||||
{
|
||||
"title": "Shazam!",
|
||||
"id": "287947",
|
||||
"distinct": 1,
|
||||
},
|
||||
{
|
||||
"title": "Captain Marvel",
|
||||
"id": "299537",
|
||||
"distinct": 4,
|
||||
},
|
||||
{
|
||||
"title": "Escape Room",
|
||||
"id": "522681",
|
||||
"distinct": 2,
|
||||
},
|
||||
{
|
||||
"title": "How to Train Your Dragon: The Hidden World",
|
||||
"id": "166428",
|
||||
"distinct": 3,
|
||||
},
|
||||
{
|
||||
"title": "Glass",
|
||||
"id": "450465",
|
||||
"distinct": 3,
|
||||
}
|
||||
]);
|
||||
index.add_documents(documents, None).await;
|
||||
index.wait_task(0).await;
|
||||
|
||||
let (_response, _code) = index
|
||||
.update_settings(
|
||||
json!({ "rankingRules": ["distinct:asc"], "distinctAttribute": "distinct"}),
|
||||
)
|
||||
.await;
|
||||
index.wait_task(1).await;
|
||||
|
||||
for page in 0..=4 {
|
||||
index
|
||||
.search(json!({"page": page, "hitsPerPage": 1}), |response, code| {
|
||||
assert_eq!(code, 200, "{}", response);
|
||||
assert_eq!(response["totalHits"], 4);
|
||||
assert_eq!(response["totalPages"], 4);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ async fn error_update_setting_unexisting_index_invalid_uid() {
|
||||
"message": "`test##! ` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
|
||||
"code": "invalid_index_uid",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-index-uid"});
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_index_uid"});
|
||||
|
||||
assert_eq!(response, expected);
|
||||
}
|
||||
@@ -290,7 +290,7 @@ async fn error_set_invalid_ranking_rules() {
|
||||
"message": r#"`manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules."#,
|
||||
"code": "invalid_ranking_rule",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-ranking-rule"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_ranking_rule"
|
||||
});
|
||||
|
||||
assert_eq!(response["error"], expected_error);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use meilisearch::option::ScheduleSnapshot;
|
||||
use meilisearch::Opt;
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -37,7 +36,8 @@ async fn perform_snapshot() {
|
||||
|
||||
let options = Opt {
|
||||
snapshot_dir: snapshot_dir.path().to_owned(),
|
||||
schedule_snapshot: ScheduleSnapshot::Enabled(1),
|
||||
snapshot_interval_sec: 1,
|
||||
schedule_snapshot: true,
|
||||
..default_settings(temp.path())
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ async fn error_get_unexisting_task_status() {
|
||||
"message": "Task `1` not found.",
|
||||
"code": "task_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#task-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#task_not_found"
|
||||
});
|
||||
|
||||
assert_eq!(response, expected_response);
|
||||
@@ -184,7 +184,7 @@ async fn get_task_filter_error() {
|
||||
"message": "Query deserialize error: unknown field `lol`",
|
||||
"code": "bad_request",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#bad-request"
|
||||
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -193,9 +193,9 @@ async fn get_task_filter_error() {
|
||||
insta::assert_json_snapshot!(response, @r###"
|
||||
{
|
||||
"message": "Task uid `pied` is invalid. It should only contain numeric characters.",
|
||||
"code": "invalid_task_uids",
|
||||
"code": "invalid_task_uids_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-task-uids"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_task_uids_filter"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -206,7 +206,7 @@ async fn get_task_filter_error() {
|
||||
"message": "Query deserialize error: invalid digit found in string",
|
||||
"code": "bad_request",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#bad-request"
|
||||
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -215,9 +215,9 @@ async fn get_task_filter_error() {
|
||||
insta::assert_json_snapshot!(response, @r###"
|
||||
{
|
||||
"message": "Task `beforeStartedAt` `pied` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
|
||||
"code": "invalid_task_before_started_at",
|
||||
"code": "invalid_task_date_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_task_date_filter"
|
||||
}
|
||||
"###);
|
||||
}
|
||||
@@ -233,7 +233,7 @@ async fn delete_task_filter_error() {
|
||||
"message": "Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.",
|
||||
"code": "missing_task_filters",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-task-filters"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_task_filters"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -244,7 +244,7 @@ async fn delete_task_filter_error() {
|
||||
"message": "Query deserialize error: unknown field `lol`",
|
||||
"code": "bad_request",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#bad-request"
|
||||
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -253,9 +253,9 @@ async fn delete_task_filter_error() {
|
||||
insta::assert_json_snapshot!(response, @r###"
|
||||
{
|
||||
"message": "Task uid `pied` is invalid. It should only contain numeric characters.",
|
||||
"code": "invalid_task_uids",
|
||||
"code": "invalid_task_uids_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-task-uids"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_task_uids_filter"
|
||||
}
|
||||
"###);
|
||||
}
|
||||
@@ -271,7 +271,7 @@ async fn cancel_task_filter_error() {
|
||||
"message": "Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.",
|
||||
"code": "missing_task_filters",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#missing-task-filters"
|
||||
"link": "https://docs.meilisearch.com/errors#missing_task_filters"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -282,7 +282,7 @@ async fn cancel_task_filter_error() {
|
||||
"message": "Query deserialize error: unknown field `lol`",
|
||||
"code": "bad_request",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#bad-request"
|
||||
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||
}
|
||||
"###);
|
||||
|
||||
@@ -291,9 +291,9 @@ async fn cancel_task_filter_error() {
|
||||
insta::assert_json_snapshot!(response, @r###"
|
||||
{
|
||||
"message": "Task uid `pied` is invalid. It should only contain numeric characters.",
|
||||
"code": "invalid_task_uids",
|
||||
"code": "invalid_task_uids_filter",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-task-uids"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_task_uids_filter"
|
||||
}
|
||||
"###);
|
||||
}
|
||||
@@ -418,7 +418,7 @@ async fn test_summarized_delete_batch() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
@@ -477,7 +477,7 @@ async fn test_summarized_delete_document() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
@@ -539,7 +539,7 @@ async fn test_summarized_settings_update() {
|
||||
"message": "`custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.",
|
||||
"code": "invalid_ranking_rule",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-ranking-rule"
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_ranking_rule"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
@@ -628,7 +628,7 @@ async fn test_summarized_index_creation() {
|
||||
"message": "Index `test` already exists.",
|
||||
"code": "index_already_exists",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-already-exists"
|
||||
"link": "https://docs.meilisearch.com/errors#index_already_exists"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
@@ -661,7 +661,7 @@ async fn test_summarized_index_deletion() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
@@ -744,7 +744,7 @@ async fn test_summarized_index_update() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
@@ -772,7 +772,7 @@ async fn test_summarized_index_update() {
|
||||
"message": "Index `test` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index-not-found"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
@@ -862,9 +862,9 @@ async fn test_summarized_index_swap() {
|
||||
},
|
||||
"error": {
|
||||
"message": "Indexes `cattos`, `doggos` not found.",
|
||||
"code": "invalid_swap_indexes",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid-swap-indexes"
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
},
|
||||
"duration": "[duration]",
|
||||
"enqueuedAt": "[date]",
|
||||
|
||||
Reference in New Issue
Block a user