Implements the get and delete tasks route

This commit is contained in:
Tamo 2024-08-08 19:14:19 +02:00
parent f00a285a6d
commit 742d0ee531
No known key found for this signature in database
GPG Key ID: 20CD8020AFA88D69
25 changed files with 1787 additions and 85 deletions

70
Cargo.lock generated
View File

@ -71,6 +71,7 @@ dependencies = [
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
"zstd",
] ]
[[package]] [[package]]
@ -92,6 +93,7 @@ dependencies = [
"bytestring", "bytestring",
"cfg-if", "cfg-if",
"http 0.2.11", "http 0.2.11",
"regex",
"regex-lite", "regex-lite",
"serde", "serde",
"tracing", "tracing",
@ -197,6 +199,7 @@ dependencies = [
"mime", "mime",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"regex",
"regex-lite", "regex-lite",
"serde", "serde",
"serde_json", "serde_json",
@ -3532,6 +3535,10 @@ dependencies = [
"tracing-trace", "tracing-trace",
"url", "url",
"urlencoding", "urlencoding",
"utoipa",
"utoipa-rapidoc",
"utoipa-redoc",
"utoipa-scalar",
"uuid", "uuid",
"wiremock", "wiremock",
"yaup", "yaup",
@ -3587,6 +3594,7 @@ dependencies = [
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
"utoipa",
"uuid", "uuid",
] ]
@ -3698,6 +3706,7 @@ dependencies = [
"uell", "uell",
"ureq", "ureq",
"url", "url",
"utoipa",
"uuid", "uuid",
] ]
@ -5953,6 +5962,67 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "utoipa"
version = "5.0.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf66139459b75afa33caddb62bb2afee3838923b630b9e0ef38c369e543382f"
dependencies = [
"indexmap",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-gen"
version = "5.0.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c136da726bb82a527afa1fdf3f4619eaf104e2982f071f25239cef1c67c79eb"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.60",
"uuid",
]
[[package]]
name = "utoipa-rapidoc"
version = "4.0.1-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4d3324d5874fb734762214827dd30b47aa78510d12abab674a97f6d7c53688f"
dependencies = [
"actix-web",
"serde",
"serde_json",
"utoipa",
]
[[package]]
name = "utoipa-redoc"
version = "4.0.1-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4375bb6b0cb78a240c973f5e99977c482f3e92aeea1907367caa28776b9aaf9"
dependencies = [
"actix-web",
"serde",
"serde_json",
"utoipa",
]
[[package]]
name = "utoipa-scalar"
version = "0.2.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dc122c11f9642b20b3be88b60c1a3672319811f139698ac6999e72992ac7c7e"
dependencies = [
"actix-web",
"serde",
"serde_json",
"utoipa",
]
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.10.0" version = "1.10.0"

View File

@ -40,6 +40,7 @@ time = { version = "0.3.36", features = [
"macros", "macros",
] } ] }
tokio = "1.38" tokio = "1.38"
utoipa = { version = "5.0.0-rc.0", features = ["macros"] }
uuid = { version = "1.10.0", features = ["serde", "v4"] } uuid = { version = "1.10.0", features = ["serde", "v4"] }
[dev-dependencies] [dev-dependencies]

View File

@ -16,6 +16,7 @@ use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use deserr::{DeserializeError, Deserr, MergeWithError, ValueKind}; use deserr::{DeserializeError, Deserr, MergeWithError, ValueKind};
use utoipa::{PartialSchema, ToSchema};
use super::{DeserrParseBoolError, DeserrParseIntError}; use super::{DeserrParseBoolError, DeserrParseIntError};
use crate::index_uid::IndexUid; use crate::index_uid::IndexUid;
@ -29,6 +30,18 @@ use crate::tasks::{Kind, Status};
#[derive(Default, Debug, Clone, Copy)] #[derive(Default, Debug, Clone, Copy)]
pub struct Param<T>(pub T); pub struct Param<T>(pub T);
impl<T: ToSchema> ToSchema for Param<T> {
fn name() -> std::borrow::Cow<'static, str> {
T::name()
}
}
impl<T: PartialSchema> PartialSchema for Param<T> {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
T::schema()
}
}
impl<T> Deref for Param<T> { impl<T> Deref for Param<T> {
type Target = T; type Target = T;

View File

@ -7,17 +7,25 @@ use aweb::rt::task::JoinError;
use convert_case::Casing; use convert_case::Casing;
use milli::heed::{Error as HeedError, MdbError}; use milli::heed::{Error as HeedError, MdbError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct ResponseError { pub struct ResponseError {
#[serde(skip)] #[serde(skip)]
pub code: StatusCode, pub code: StatusCode,
/// The error message.
pub message: String, pub message: String,
/// The error code.
#[schema(value_type = Code)]
#[serde(rename = "code")] #[serde(rename = "code")]
error_code: String, error_code: String,
/// The error type.
#[schema(value_type = ErrorType)]
#[serde(rename = "type")] #[serde(rename = "type")]
error_type: String, error_type: String,
/// A link to the documentation about this specific error.
#[serde(rename = "link")] #[serde(rename = "link")]
error_link: String, error_link: String,
} }
@ -97,7 +105,9 @@ pub trait ErrorCode {
} }
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
enum ErrorType { #[derive(ToSchema)]
#[schema(rename_all = "snake_case")]
pub enum ErrorType {
Internal, Internal,
InvalidRequest, InvalidRequest,
Auth, Auth,
@ -129,7 +139,8 @@ impl fmt::Display for ErrorType {
/// `MyErrorCode::default().error_code()`. /// `MyErrorCode::default().error_code()`.
macro_rules! make_error_codes { macro_rules! make_error_codes {
($($code_ident:ident, $err_type:ident, $status:ident);*) => { ($($code_ident:ident, $err_type:ident, $status:ident);*) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, ToSchema)]
#[schema(rename_all = "snake_case")]
pub enum Code { pub enum Code {
$($code_ident),* $($code_ident),*
} }

View File

@ -1,8 +1,9 @@
use deserr::Deserr; use deserr::Deserr;
use milli::OrderBy; use milli::OrderBy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Deserr)] #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Deserr, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[deserr(rename_all = camelCase)] #[deserr(rename_all = camelCase)]
pub enum FacetValuesSort { pub enum FacetValuesSort {

View File

@ -4,13 +4,15 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use deserr::Deserr; use deserr::Deserr;
use utoipa::ToSchema;
use crate::error::{Code, ErrorCode}; use crate::error::{Code, ErrorCode};
/// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400
/// bytes long /// bytes long
#[derive(Debug, Clone, PartialEq, Eq, Deserr, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, Deserr, PartialOrd, Ord, ToSchema)]
#[deserr(try_from(String) = IndexUid::try_from -> IndexUidFormatError)] #[deserr(try_from(String) = IndexUid::try_from -> IndexUidFormatError)]
#[schema(value_type = String, example = "movies")]
pub struct IndexUid(String); pub struct IndexUid(String);
impl IndexUid { impl IndexUid {

View File

@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
use time::macros::{format_description, time}; use time::macros::{format_description, time};
use time::{Date, OffsetDateTime, PrimitiveDateTime}; use time::{Date, OffsetDateTime, PrimitiveDateTime};
use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
use crate::deserr::{immutable_field_error, DeserrError, DeserrJsonError}; use crate::deserr::{immutable_field_error, DeserrError, DeserrJsonError};
@ -32,19 +33,31 @@ impl<C: Default + ErrorCode> MergeWithError<IndexUidPatternFormatError> for Dese
} }
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct CreateApiKey { pub struct CreateApiKey {
/// A description for the key. `null` if empty.
#[schema(example = json!(null))]
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
pub description: Option<String>, pub description: Option<String>,
/// A human-readable name for the key. `null` if empty.
#[schema(example = "Indexing Products API key")]
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
pub name: Option<String>, pub name: Option<String>,
/// A uuid v4 to identify the API Key. If not specified, it's generated by Meilisearch.
#[schema(value_type = Uuid, example = json!(null))]
#[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, try_from(&String) = Uuid::from_str -> uuid::Error)] #[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, try_from(&String) = Uuid::from_str -> uuid::Error)]
pub uid: KeyId, pub uid: KeyId,
/// A list of actions permitted for the key. `["*"]` for all actions. The `*` character can be used as a wildcard when located at the last position. e.g. `documents.*` to authorize access on all documents endpoints.
#[schema(example = json!(["documents.add"]))]
#[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)] #[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)]
pub actions: Vec<Action>, pub actions: Vec<Action>,
/// A list of accesible indexes permitted for the key. `["*"]` for all indexes. The `*` character can be used as a wildcard when located at the last position. e.g. `products_*` to allow access to all indexes whose names start with `products_`.
#[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)] #[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)]
#[schema(value_type = Vec<String>, example = json!(["products"]))]
pub indexes: Vec<IndexUidPattern>, pub indexes: Vec<IndexUidPattern>,
/// Represent the expiration date and time as RFC 3339 format. `null` equals to no expiration time.
#[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, try_from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] #[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, try_from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)]
pub expires_at: Option<OffsetDateTime>, pub expires_at: Option<OffsetDateTime>,
} }
@ -86,12 +99,15 @@ fn deny_immutable_fields_api_key(
} }
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)]
#[schema(rename_all = "camelCase")]
pub struct PatchApiKey { pub struct PatchApiKey {
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
#[schema(value_type = Option<String>, example = "This key is used to update documents in the products index")]
pub description: Setting<String>, pub description: Setting<String>,
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
#[schema(value_type = Option<String>, example = "Indexing Products API key")]
pub name: Setting<String>, pub name: Setting<String>,
} }
@ -179,7 +195,9 @@ fn parse_expiration_date(
} }
} }
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr)] #[derive(
Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr, ToSchema,
)]
#[repr(u8)] #[repr(u8)]
pub enum Action { pub enum Action {
#[serde(rename = "*")] #[serde(rename = "*")]

View File

@ -1,8 +1,9 @@
use deserr::Deserr; use deserr::Deserr;
use milli::LocalizedAttributesRule; use milli::LocalizedAttributesRule;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Clone, PartialEq, Eq, Deserr, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserr, Serialize, Deserialize, ToSchema)]
#[deserr(rename_all = camelCase)] #[deserr(rename_all = camelCase)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LocalizedAttributesRuleView { pub struct LocalizedAttributesRuleView {
@ -33,7 +34,7 @@ impl From<LocalizedAttributesRuleView> for LocalizedAttributesRule {
/// this enum implements `Deserr` in order to be used in the API. /// this enum implements `Deserr` in order to be used in the API.
macro_rules! make_locale { macro_rules! make_locale {
($(($iso_639_1:ident, $iso_639_1_str:expr) => ($iso_639_3:ident, $iso_639_3_str:expr),)+) => { ($(($iso_639_1:ident, $iso_639_1_str:expr) => ($iso_639_3:ident, $iso_639_3_str:expr),)+) => {
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserr, Serialize, Deserialize, Ord, PartialOrd)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserr, Serialize, Deserialize, Ord, PartialOrd, ToSchema)]
#[deserr(rename_all = camelCase)] #[deserr(rename_all = camelCase)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum Locale { pub enum Locale {

View File

@ -13,6 +13,7 @@ use milli::proximity::ProximityPrecision;
use milli::update::Setting; use milli::update::Setting;
use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET}; use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET};
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use utoipa::ToSchema;
use crate::deserr::DeserrJsonError; use crate::deserr::DeserrJsonError;
use crate::error::deserr_codes::*; use crate::error::deserr_codes::*;
@ -39,10 +40,10 @@ where
.serialize(s) .serialize(s)
} }
#[derive(Clone, Default, Debug, Serialize, PartialEq, Eq)] #[derive(Clone, Default, Debug, Serialize, PartialEq, Eq, ToSchema)]
pub struct Checked; pub struct Checked;
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
pub struct Unchecked; pub struct Unchecked;
impl<E> Deserr<E> for Unchecked impl<E> Deserr<E> for Unchecked
@ -69,54 +70,63 @@ fn validate_min_word_size_for_typo_setting<E: DeserializeError>(
Ok(s) Ok(s)
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError<InvalidSettingsTypoTolerance>)] #[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError<InvalidSettingsTypoTolerance>)]
pub struct MinWordSizeTyposSetting { pub struct MinWordSizeTyposSetting {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<u8>, example = json!(5))]
pub one_typo: Setting<u8>, pub one_typo: Setting<u8>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<u8>, example = json!(9))]
pub two_typos: Setting<u8>, pub two_typos: Setting<u8>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrJsonError<InvalidSettingsTypoTolerance>>)] #[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrJsonError<InvalidSettingsTypoTolerance>>)]
pub struct TypoSettings { pub struct TypoSettings {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<bool>, example = json!(true))]
pub enabled: Setting<bool>, pub enabled: Setting<bool>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
#[schema(value_type = Option<MinWordSizeTyposSetting>, example = json!({ "oneTypo": 5, "twoTypo": 9 }))]
pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>, pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<BTreeSet<String>>, example = json!(["iPhone", "phone"]))]
pub disable_on_words: Setting<BTreeSet<String>>, pub disable_on_words: Setting<BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<BTreeSet<String>>, example = json!(["uuid", "url"]))]
pub disable_on_attributes: Setting<BTreeSet<String>>, pub disable_on_attributes: Setting<BTreeSet<String>>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct FacetingSettings { pub struct FacetingSettings {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<usize>, example = json!(10))]
pub max_values_per_facet: Setting<usize>, pub max_values_per_facet: Setting<usize>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<BTreeMap<String, FacetValuesSort>>, example = json!({ "genre": FacetValuesSort::Count }))]
pub sort_facet_values_by: Setting<BTreeMap<String, FacetValuesSort>>, pub sort_facet_values_by: Setting<BTreeMap<String, FacetValuesSort>>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct PaginationSettings { pub struct PaginationSettings {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<usize>, example = json!(250))]
pub max_total_hits: Setting<usize>, pub max_total_hits: Setting<usize>,
} }
@ -137,70 +147,105 @@ impl MergeWithError<milli::CriterionError> for DeserrJsonError<InvalidSettingsRa
/// Holds all the settings for an index. `T` can either be `Checked` if they represents settings /// 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 /// 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>`. /// call to `check` will return a `Settings<Checked>` from a `Settings<Unchecked>`.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
#[serde( #[serde(
deny_unknown_fields, deny_unknown_fields,
rename_all = "camelCase", rename_all = "camelCase",
bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>") bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>")
)] )]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct Settings<T> { pub struct Settings<T> {
/// Fields displayed in the returned documents.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["id", "title", "description", "url"]))]
pub displayed_attributes: WildcardSetting, pub displayed_attributes: WildcardSetting,
/// Fields in which to search for matching query words sorted by order of importance.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["title", "description"]))]
pub searchable_attributes: WildcardSetting, pub searchable_attributes: WildcardSetting,
/// Attributes to use for faceting and filtering. See [Filtering and Faceted Search](https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters).
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date", "genre"]))]
pub filterable_attributes: Setting<BTreeSet<String>>, pub filterable_attributes: Setting<BTreeSet<String>>,
/// Attributes to use when sorting search results.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date"]))]
pub sortable_attributes: Setting<BTreeSet<String>>, pub sortable_attributes: Setting<BTreeSet<String>>,
/// List of ranking rules sorted by order of importance. The order is customizable.
/// [A list of ordered built-in ranking rules](https://www.meilisearch.com/docs/learn/relevancy/relevancy).
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsRankingRules>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsRankingRules>)]
#[schema(value_type = Option<Vec<String>>, example = json!([RankingRuleView::Words, RankingRuleView::Typo, RankingRuleView::Proximity, RankingRuleView::Attribute, RankingRuleView::Exactness]))]
pub ranking_rules: Setting<Vec<RankingRuleView>>, pub ranking_rules: Setting<Vec<RankingRuleView>>,
/// List of words ignored when present in search queries.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsStopWords>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsStopWords>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["the", "a", "them", "their"]))]
pub stop_words: Setting<BTreeSet<String>>, pub stop_words: Setting<BTreeSet<String>>,
/// List of characters not delimiting where one term begins and ends.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsNonSeparatorTokens>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsNonSeparatorTokens>)]
#[schema(value_type = Option<Vec<String>>, example = json!([" ", "\n"]))]
pub non_separator_tokens: Setting<BTreeSet<String>>, pub non_separator_tokens: Setting<BTreeSet<String>>,
/// List of characters delimiting where one term begins and ends.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsSeparatorTokens>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSeparatorTokens>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["S"]))]
pub separator_tokens: Setting<BTreeSet<String>>, pub separator_tokens: Setting<BTreeSet<String>>,
/// List of strings Meilisearch should parse as a single term.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsDictionary>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsDictionary>)]
#[schema(value_type = Option<Vec<String>>, example = json!(["iPhone pro"]))]
pub dictionary: Setting<BTreeSet<String>>, pub dictionary: Setting<BTreeSet<String>>,
/// List of associated words treated similarly. A word associated to an array of word as synonyms.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsSynonyms>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSynonyms>)]
#[schema(value_type = Option<BTreeMap<String, Vec<String>>>, example = json!({ "he": ["she", "they", "them"], "phone": ["iPhone", "android"]}))]
pub synonyms: Setting<BTreeMap<String, Vec<String>>>, pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
/// Search returns documents with distinct (different) values of the given field.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)]
#[schema(value_type = Option<String>, example = json!("sku"))]
pub distinct_attribute: Setting<String>, pub distinct_attribute: Setting<String>,
/// Precision level when calculating the proximity ranking rule.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsProximityPrecision>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsProximityPrecision>)]
#[schema(value_type = Option<String>, example = json!(ProximityPrecisionView::ByAttribute))]
pub proximity_precision: Setting<ProximityPrecisionView>, pub proximity_precision: Setting<ProximityPrecisionView>,
/// Customize typo tolerance feature.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
#[schema(value_type = Option<TypoSettings>, example = json!({ "enabled": true, "disableOnAttributes": ["title"]}))]
pub typo_tolerance: Setting<TypoSettings>, pub typo_tolerance: Setting<TypoSettings>,
/// Faceting settings.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)]
#[schema(value_type = Option<FacetingSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
pub faceting: Setting<FacetingSettings>, pub faceting: Setting<FacetingSettings>,
/// Pagination settings.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)]
#[schema(value_type = Option<PaginationSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
pub pagination: Setting<PaginationSettings>, pub pagination: Setting<PaginationSettings>,
/// Embedder required for performing meaning-based search queries.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsEmbedders>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsEmbedders>)]
#[schema(value_type = String)] // TODO: TAMO
pub embedders: Setting<BTreeMap<String, Setting<milli::vector::settings::EmbeddingSettings>>>, pub embedders: Setting<BTreeMap<String, Setting<milli::vector::settings::EmbeddingSettings>>>,
/// Maximum duration of a search query.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchCutoffMs>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSearchCutoffMs>)]
#[schema(value_type = Option<u64>, example = json!(50))]
pub search_cutoff_ms: Setting<u64>, pub search_cutoff_ms: Setting<u64>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsLocalizedAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsLocalizedAttributes>)]
#[schema(value_type = Option<Vec<LocalizedAttributesRuleView>>, example = json!(50))]
pub localized_attributes: Setting<Vec<LocalizedAttributesRuleView>>, pub localized_attributes: Setting<Vec<LocalizedAttributesRuleView>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default, error = DeserrJsonError<InvalidSettingsFacetSearch>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsFacetSearch>)]

View File

@ -7,26 +7,42 @@ use crate::error::ResponseError;
use crate::settings::{Settings, Unchecked}; use crate::settings::{Settings, Unchecked};
use crate::tasks::{serialize_duration, Details, IndexSwap, Kind, Status, Task, TaskId}; use crate::tasks::{serialize_duration, Details, IndexSwap, Kind, Status, Task, TaskId};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct TaskView { pub struct TaskView {
/// The unique sequential identifier of the task.
#[schema(value_type = u32, example = 4312)]
pub uid: TaskId, pub uid: TaskId,
/// The unique identifier of the index where this task is operated.
#[schema(example = json!("movies"))]
pub batch_uid: Option<BatchId>, pub batch_uid: Option<BatchId>,
#[serde(default)] #[serde(default)]
pub index_uid: Option<String>, pub index_uid: Option<String>,
pub status: Status, pub status: Status,
/// The type of the task.
#[serde(rename = "type")] #[serde(rename = "type")]
pub kind: Kind, pub kind: Kind,
/// The uid of the task that performed the taskCancelation if the task has been canceled.
#[schema(value_type = Option<u32>, example = json!(4326))]
pub canceled_by: Option<TaskId>, pub canceled_by: Option<TaskId>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<DetailsView>, pub details: Option<DetailsView>,
pub error: Option<ResponseError>, pub error: Option<ResponseError>,
/// Total elasped time the engine was in processing state expressed as a `ISO-8601` duration format.
#[schema(value_type = Option<String>, example = json!(null))]
#[serde(serialize_with = "serialize_duration", default)] #[serde(serialize_with = "serialize_duration", default)]
pub duration: Option<Duration>, pub duration: Option<Duration>,
/// An `RFC 3339` format for date/time/duration.
#[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))]
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]
pub enqueued_at: OffsetDateTime, pub enqueued_at: OffsetDateTime,
/// An `RFC 3339` format for date/time/duration.
#[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))]
#[serde(with = "time::serde::rfc3339::option", default)] #[serde(with = "time::serde::rfc3339::option", default)]
pub started_at: Option<OffsetDateTime>, pub started_at: Option<OffsetDateTime>,
/// An `RFC 3339` format for date/time/duration.
#[schema(value_type = String, example = json!("2024-08-08_14:12:09.393Z"))]
#[serde(with = "time::serde::rfc3339::option", default)] #[serde(with = "time::serde::rfc3339::option", default)]
pub finished_at: Option<OffsetDateTime>, pub finished_at: Option<OffsetDateTime>,
} }
@ -53,32 +69,44 @@ impl TaskView {
#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DetailsView { pub struct DetailsView {
/// Number of documents received for documentAdditionOrUpdate task.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub received_documents: Option<u64>, pub received_documents: Option<u64>,
/// Number of documents finally indexed for documentAdditionOrUpdate task or a documentAdditionOrUpdate batch of tasks.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub indexed_documents: Option<Option<u64>>, pub indexed_documents: Option<Option<u64>>,
/// Number of documents edited for editDocumentByFunction task.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub edited_documents: Option<Option<u64>>, pub edited_documents: Option<Option<u64>>,
/// Value for the primaryKey field encountered if any for indexCreation or indexUpdate task.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub primary_key: Option<Option<String>>, pub primary_key: Option<Option<String>>,
/// Number of provided document ids for the documentDeletion task.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub provided_ids: Option<usize>, pub provided_ids: Option<usize>,
/// Number of documents finally deleted for documentDeletion and indexDeletion tasks.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub deleted_documents: Option<Option<u64>>, pub deleted_documents: Option<Option<u64>>,
/// Number of tasks that match the request for taskCancelation or taskDeletion tasks.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub matched_tasks: Option<u64>, pub matched_tasks: Option<u64>,
/// Number of tasks canceled for taskCancelation.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub canceled_tasks: Option<Option<u64>>, pub canceled_tasks: Option<Option<u64>>,
/// Number of tasks deleted for taskDeletion.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub deleted_tasks: Option<Option<u64>>, pub deleted_tasks: Option<Option<u64>>,
/// Original filter query for taskCancelation or taskDeletion tasks.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub original_filter: Option<Option<String>>, pub original_filter: Option<Option<String>>,
/// Identifier generated for the dump for dumpCreation task.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub dump_uid: Option<Option<String>>, pub dump_uid: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Option<Object>>, pub context: Option<Option<Object>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<String>, pub function: Option<String>,
/// [Learn more about the settings in this guide](https://www.meilisearch.com/docs/reference/api/settings).
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)] #[serde(flatten)]
pub settings: Option<Box<Settings<Unchecked>>>, pub settings: Option<Box<Settings<Unchecked>>>,

View File

@ -9,6 +9,7 @@ use milli::Object;
use roaring::RoaringBitmap; use roaring::RoaringBitmap;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
use crate::batches::BatchId; use crate::batches::BatchId;
@ -151,7 +152,7 @@ pub enum KindWithContent {
SnapshotCreation, SnapshotCreation,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct IndexSwap { pub struct IndexSwap {
pub indexes: (String, String), pub indexes: (String, String),
@ -363,9 +364,22 @@ impl From<&KindWithContent> for Option<Details> {
} }
} }
/// The status of a task.
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence, PartialOrd, Ord, Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Sequence,
PartialOrd,
Ord,
ToSchema,
)] )]
#[schema(example = json!(Status::Processing))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum Status { pub enum Status {
Enqueued, Enqueued,
@ -424,10 +438,23 @@ impl fmt::Display for ParseTaskStatusError {
} }
impl std::error::Error for ParseTaskStatusError {} impl std::error::Error for ParseTaskStatusError {}
/// The type of the task.
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence, PartialOrd, Ord, Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Sequence,
PartialOrd,
Ord,
ToSchema,
)] )]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase", example = json!(enum_iterator::all::<Kind>().collect::<Vec<_>>()))]
pub enum Kind { pub enum Kind {
DocumentAdditionOrUpdate, DocumentAdditionOrUpdate,
DocumentEdition, DocumentEdition,
@ -444,6 +471,10 @@ pub enum Kind {
} }
impl Kind { impl Kind {
pub fn all_variants() -> Vec<Self> {
enum_iterator::all::<Kind>().collect()
}
pub fn related_to_one_index(&self) -> bool { pub fn related_to_one_index(&self) -> bool {
match self { match self {
Kind::DocumentAdditionOrUpdate Kind::DocumentAdditionOrUpdate

View File

@ -105,6 +105,10 @@ tracing-actix-web = "0.7.11"
build-info = { version = "1.7.0", path = "../build-info" } build-info = { version = "1.7.0", path = "../build-info" }
roaring = "0.10.7" roaring = "0.10.7"
mopa-maintained = "0.2.3" mopa-maintained = "0.2.3"
utoipa = { version = "5.0.0-rc.0", features = ["actix_extras", "macros", "non_strict_integers", "preserve_order", "uuid", "time", "openapi_extensions"] }
utoipa-scalar = { version = "0.2.0-rc.0", features = ["actix-web"] }
utoipa-rapidoc = { version = "4.0.1-rc.0", features = ["actix-web"] }
utoipa-redoc = { version = "4.0.1-rc.0", features = ["actix-web"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.10.0" actix-rt = "2.10.0"

View File

@ -13,6 +13,7 @@ use meilisearch_types::error::{Code, ResponseError};
use meilisearch_types::keys::{CreateApiKey, Key, PatchApiKey}; use meilisearch_types::keys::{CreateApiKey, Key, PatchApiKey};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
use utoipa::{IntoParams, OpenApi, ToSchema};
use uuid::Uuid; use uuid::Uuid;
use super::PAGINATION_DEFAULT_LIMIT; use super::PAGINATION_DEFAULT_LIMIT;
@ -21,6 +22,20 @@ use crate::extractors::authentication::GuardedData;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::Pagination; use crate::routes::Pagination;
#[derive(OpenApi)]
#[openapi(
paths(create_api_key, list_api_keys, get_api_key, patch_api_key, delete_api_key),
tags((
name = "Keys",
description = "Manage API `keys` for a Meilisearch instance. Each key has a given set of permissions.
You must have the master key or the default admin key to access the keys route. More information about the keys and their rights.
Accessing any route under `/keys` without having set a master key will result in an error.",
external_docs(url = "https://www.meilisearch.com/docs/reference/api/keys"),
)),
)]
pub struct ApiKeyApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::resource("") web::resource("")
@ -35,6 +50,52 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
/// Create an API Key
///
/// Create an API Key.
#[utoipa::path(
post,
path = "/",
tag = "Keys",
security(("Bearer" = ["keys.create", "keys.*", "*"])),
request_body = CreateApiKey,
responses(
(status = 202, description = "Key has been created", body = KeyView, content_type = "application/json", example = json!(
{
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
"name": "Indexing Products API key",
"description": null,
"actions": [
"documents.add"
],
"indexes": [
"products"
],
"expiresAt": "2021-11-13T00:00:00Z",
"createdAt": "2021-11-12T10:00:00Z",
"updatedAt": "2021-11-12T10:00:00Z"
}
)),
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn create_api_key( pub async fn create_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>,
body: AwebJson<CreateApiKey, DeserrJsonError>, body: AwebJson<CreateApiKey, DeserrJsonError>,
@ -51,11 +112,14 @@ pub async fn create_api_key(
Ok(HttpResponse::Created().json(res)) Ok(HttpResponse::Created().json(res))
} }
#[derive(Deserr, Debug, Clone, Copy)] #[derive(Deserr, Debug, Clone, Copy, IntoParams)]
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct ListApiKeys { pub struct ListApiKeys {
#[into_params(value_type = usize, default = 0)]
#[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)] #[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
pub offset: Param<usize>, pub offset: Param<usize>,
#[into_params(value_type = usize, default = PAGINATION_DEFAULT_LIMIT)]
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)] #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
pub limit: Param<usize>, pub limit: Param<usize>,
} }
@ -66,6 +130,60 @@ impl ListApiKeys {
} }
} }
/// Get API Keys
///
/// List all API Keys
/// TODO: Tamo fix the return type
#[utoipa::path(
get,
path = "/",
tag = "Keys",
security(("Bearer" = ["keys.get", "keys.*", "*"])),
params(ListApiKeys),
responses(
(status = 202, description = "List of keys", body = serde_json::Value, content_type = "application/json", example = json!(
{
"results": [
{
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
"name": "An API Key",
"description": null,
"actions": [
"documents.add"
],
"indexes": [
"movies"
],
"expiresAt": "2022-11-12T10:00:00Z",
"createdAt": "2021-11-12T10:00:00Z",
"updatedAt": "2021-11-12T10:00:00Z"
}
],
"limit": 20,
"offset": 0,
"total": 1
}
)),
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn list_api_keys( pub async fn list_api_keys(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>, list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>,
@ -84,6 +202,52 @@ pub async fn list_api_keys(
Ok(HttpResponse::Ok().json(page_view)) Ok(HttpResponse::Ok().json(page_view))
} }
/// Get an API Key
///
/// Get an API key from its `uid` or its `key` field.
#[utoipa::path(
get,
path = "/{key}",
tag = "Keys",
security(("Bearer" = ["keys.get", "keys.*", "*"])),
params(("uidOrKey" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key", nullable = false)),
responses(
(status = 200, description = "The key is returned", body = KeyView, content_type = "application/json", example = json!(
{
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
"name": "An API Key",
"description": null,
"actions": [
"documents.add"
],
"indexes": [
"movies"
],
"expiresAt": "2022-11-12T10:00:00Z",
"createdAt": "2021-11-12T10:00:00Z",
"updatedAt": "2021-11-12T10:00:00Z"
}
)),
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn get_api_key( pub async fn get_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
@ -103,6 +267,55 @@ pub async fn get_api_key(
Ok(HttpResponse::Ok().json(res)) Ok(HttpResponse::Ok().json(res))
} }
/// Update an API Key
///
/// Update an API key from its `uid` or its `key` field.
/// Only the `name` and `description` of the api key can be updated.
/// If there is an issue with the `key` or `uid` of a key, then you must recreate one from scratch.
#[utoipa::path(
patch,
path = "/{key}",
tag = "Keys",
security(("Bearer" = ["keys.update", "keys.*", "*"])),
params(("uidOrKey" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key", nullable = false)),
request_body = PatchApiKey,
responses(
(status = 200, description = "The key have been updated", body = KeyView, content_type = "application/json", example = json!(
{
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
"name": "An API Key",
"description": null,
"actions": [
"documents.add"
],
"indexes": [
"movies"
],
"expiresAt": "2022-11-12T10:00:00Z",
"createdAt": "2021-11-12T10:00:00Z",
"updatedAt": "2021-11-12T10:00:00Z"
}
)),
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn patch_api_key( pub async fn patch_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, Data<AuthController>>,
body: AwebJson<PatchApiKey, DeserrJsonError>, body: AwebJson<PatchApiKey, DeserrJsonError>,
@ -123,6 +336,39 @@ pub async fn patch_api_key(
Ok(HttpResponse::Ok().json(res)) Ok(HttpResponse::Ok().json(res))
} }
/// Update an API Key
///
/// Update an API key from its `uid` or its `key` field.
/// Only the `name` and `description` of the api key can be updated.
/// If there is an issue with the `key` or `uid` of a key, then you must recreate one from scratch.
#[utoipa::path(
delete,
path = "/{key}",
tag = "Keys",
security(("Bearer" = ["keys.delete", "keys.*", "*"])),
params(("uidOrKey" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key", nullable = false)),
responses(
(status = NO_CONTENT, description = "The key have been removed"),
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn delete_api_key( pub async fn delete_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, Data<AuthController>>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
@ -144,19 +390,30 @@ pub struct AuthParam {
key: String, key: String,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct KeyView { pub(super) struct KeyView {
/// The name of the API Key if any
name: Option<String>, name: Option<String>,
/// The description of the API Key if any
description: Option<String>, description: Option<String>,
/// The actual API Key you can send to Meilisearch
key: String, key: String,
/// The `Uuid` specified while creating the key or autogenerated by Meilisearch.
uid: Uuid, uid: Uuid,
/// The actions accessible with this key.
actions: Vec<Action>, actions: Vec<Action>,
/// The indexes accessible with this key.
indexes: Vec<String>, indexes: Vec<String>,
/// The expiration date of the key. Once this timestamp is exceeded the key is not deleted but cannot be used anymore.
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")] #[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
expires_at: Option<OffsetDateTime>, expires_at: Option<OffsetDateTime>,
/// The date of creation of this API Key.
#[schema(read_only)]
#[serde(serialize_with = "time::serde::rfc3339::serialize")] #[serde(serialize_with = "time::serde::rfc3339::serialize")]
created_at: OffsetDateTime, created_at: OffsetDateTime,
/// The date of the last update made on this key.
#[schema(read_only)]
#[serde(serialize_with = "time::serde::rfc3339::serialize")] #[serde(serialize_with = "time::serde::rfc3339::serialize")]
updated_at: OffsetDateTime, updated_at: OffsetDateTime,
} }

View File

@ -5,6 +5,7 @@ use meilisearch_auth::AuthController;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::tasks::KindWithContent; use meilisearch_types::tasks::KindWithContent;
use tracing::debug; use tracing::debug;
use utoipa::OpenApi;
use crate::analytics::Analytics; use crate::analytics::Analytics;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
@ -13,12 +14,61 @@ use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView}; use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView};
use crate::Opt; use crate::Opt;
#[derive(OpenApi)]
#[openapi(
paths(create_dump),
tags((
name = "Dumps",
description = "The `dumps` route allows the creation of database dumps.
Dumps are `.dump` files that can be used to launch Meilisearch. Dumps are compatible between Meilisearch versions.
Creating a dump is also referred to as exporting it, whereas launching Meilisearch with a dump is referred to as importing it.
During a [dump export](https://www.meilisearch.com/docs/reference/api/dump#create-a-dump), all indexes of the current instance are
exportedtogether with their documents and settingsand saved as a single `.dump` file. During a dump import,
all indexes contained in the indicated `.dump` file are imported along with their associated documents and settings.
Any existing index with the same uid as an index in the dump file will be overwritten.
Dump imports are [performed at launch](https://www.meilisearch.com/docs/learn/advanced/dumps#importing-a-dump) using an option.",
external_docs(url = "https://www.meilisearch.com/docs/reference/api/dump"),
)),
)]
pub struct DumpApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("").route(web::post().to(SeqHandler(create_dump)))); cfg.service(web::resource("").route(web::post().to(SeqHandler(create_dump))));
} }
crate::empty_analytics!(DumpAnalytics, "Dump Created"); crate::empty_analytics!(DumpAnalytics, "Dump Created");
/// Create a dump
///
/// Triggers a dump creation process. Once the process is complete, a dump is created in the
/// [dump directory](https://www.meilisearch.com/docs/learn/self_hosted/configure_meilisearch_at_launch#dump-directory).
/// If the dump directory does not exist yet, it will be created.
#[utoipa::path(
post,
path = "/",
tag = "Dumps",
security(("Bearer" = ["dumps.create", "dumps.*", "*"])),
responses(
(status = 202, description = "Dump is being created", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 0,
"indexUid": null,
"status": "enqueued",
"type": "DumpCreation",
"enqueuedAt": "2021-01-01T09:39:00.000000Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn create_dump( pub async fn create_dump(
index_scheduler: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<IndexScheduler>>,
auth_controller: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<AuthController>>,

View File

@ -31,6 +31,7 @@ use tempfile::tempfile;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter}; use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter};
use tracing::debug; use tracing::debug;
use utoipa::{IntoParams, OpenApi, ToSchema};
use crate::analytics::{Aggregate, AggregateMethod, Analytics}; use crate::analytics::{Aggregate, AggregateMethod, Analytics};
use crate::error::MeilisearchHttpError; use crate::error::MeilisearchHttpError;
@ -71,6 +72,19 @@ pub struct DocumentParam {
document_id: String, document_id: String,
} }
#[derive(OpenApi)]
#[openapi(
paths(get_documents, replace_documents, update_documents, clear_all_documents, delete_documents_batch),
tags(
(
name = "Documents",
description = "Documents are objects composed of fields that can store any type of data. Each field contains an attribute and its associated value. Documents are stored inside [indexes](https://www.meilisearch.com/docs/learn/getting_started/indexes).",
external_docs(url = "https://www.meilisearch.com/docs/learn/getting_started/documents"),
),
),
)]
pub struct DocumentsApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::resource("") web::resource("")
@ -286,17 +300,23 @@ pub struct BrowseQueryGet {
filter: Option<String>, filter: Option<String>,
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, IntoParams, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct BrowseQuery { pub struct BrowseQuery {
#[schema(default, example = 150)]
#[deserr(default, error = DeserrJsonError<InvalidDocumentOffset>)] #[deserr(default, error = DeserrJsonError<InvalidDocumentOffset>)]
offset: usize, offset: usize,
#[schema(default = 20, example = 1)]
#[deserr(default = PAGINATION_DEFAULT_LIMIT, error = DeserrJsonError<InvalidDocumentLimit>)] #[deserr(default = PAGINATION_DEFAULT_LIMIT, error = DeserrJsonError<InvalidDocumentLimit>)]
limit: usize, limit: usize,
#[schema(example = json!(["title, description"]))]
#[deserr(default, error = DeserrJsonError<InvalidDocumentFields>)] #[deserr(default, error = DeserrJsonError<InvalidDocumentFields>)]
fields: Option<Vec<String>>, fields: Option<Vec<String>>,
#[schema(default, example = true)]
#[deserr(default, error = DeserrJsonError<InvalidDocumentRetrieveVectors>)] #[deserr(default, error = DeserrJsonError<InvalidDocumentRetrieveVectors>)]
retrieve_vectors: bool, retrieve_vectors: bool,
#[schema(default, example = "popularity > 1000")]
#[deserr(default, error = DeserrJsonError<InvalidDocumentFilter>)] #[deserr(default, error = DeserrJsonError<InvalidDocumentFilter>)]
filter: Option<Value>, filter: Option<Value>,
} }
@ -326,6 +346,62 @@ pub async fn documents_by_query_post(
documents_by_query(&index_scheduler, index_uid, body) documents_by_query(&index_scheduler, index_uid, body)
} }
/// Get documents
///
/// Get documents by batches.
#[utoipa::path(
get,
path = "/{indexUid}/documents",
tags = ["Indexes", "Documents"],
security(("Bearer" = ["documents.get", "documents.*", "*"])),
params(
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
// Here we can use the post version of the browse query since it contains the exact same parameter
BrowseQuery
),
responses(
// body = PaginationView<Document>
(status = 200, description = "The documents are returned", body = serde_json::Value, content_type = "application/json", example = json!(
{
"results": [
{
"id": 25684,
"title": "American Ninja 5",
"poster": "https://image.tmdb.org/t/p/w1280/iuAQVI4mvjI83wnirpD8GVNRVuY.jpg",
"overview": "When a scientists daughter is kidnapped, American Ninja, attempts to find her, but this time he teams up with a youngster he has trained in the ways of the ninja.",
"release_date": 725846400
},
{
"id": 45881,
"title": "The Bridge of San Luis Rey",
"poster": "https://image.tmdb.org/t/p/w500/4X7quIcdkc24Cveg5XdpfRqxtYA.jpg",
"overview": "The Bridge of San Luis Rey is American author Thornton Wilder's second novel, first published in 1927 to worldwide acclaim. It tells the story of several interrelated people who die in the collapse of an Inca rope-fiber suspension bridge in Peru, and the events that lead up to their being on the bridge.[ A friar who has witnessed the tragic accident then goes about inquiring into the lives of the victims, seeking some sort of cosmic answer to the question of why each had to die. The novel won the Pulitzer Prize in 1928.",
"release_date": 1072915200
}
],
"limit": 20,
"offset": 0,
"total": 2
}
)),
(status = 404, description = "Index not found", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Index `movies` not found.",
"code": "index_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#index_not_found"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn get_documents( pub async fn get_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
@ -396,11 +472,17 @@ fn documents_by_query(
Ok(HttpResponse::Ok().json(ret)) Ok(HttpResponse::Ok().json(ret))
} }
#[derive(Deserialize, Debug, Deserr)] #[derive(Deserialize, Debug, Deserr, IntoParams)]
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase")]
pub struct UpdateDocumentsQuery { pub struct UpdateDocumentsQuery {
/// The primary key of the documents. primaryKey is optional. If you want to set the primary key of your index through this route,
/// it only has to be done the first time you add documents to the index. After which it will be ignored if given.
#[param(example = "id")]
#[deserr(default, error = DeserrQueryParamError<InvalidIndexPrimaryKey>)] #[deserr(default, error = DeserrQueryParamError<InvalidIndexPrimaryKey>)]
pub primary_key: Option<String>, pub primary_key: Option<String>,
/// Customize the csv delimiter when importing CSV documents.
#[param(value_type = char, default = ",", example = ";")]
#[deserr(default, try_from(char) = from_char_csv_delimiter -> DeserrQueryParamError<InvalidDocumentCsvDelimiter>, error = DeserrQueryParamError<InvalidDocumentCsvDelimiter>)] #[deserr(default, try_from(char) = from_char_csv_delimiter -> DeserrQueryParamError<InvalidDocumentCsvDelimiter>, error = DeserrQueryParamError<InvalidDocumentCsvDelimiter>)]
pub csv_delimiter: Option<u8>, pub csv_delimiter: Option<u8>,
} }
@ -451,6 +533,51 @@ impl<Method: AggregateMethod> Aggregate for DocumentsAggregator<Method> {
} }
} }
/// Add or replace documents
///
/// Add a list of documents or replace them if they already exist.
///
/// If you send an already existing document (same id) the whole existing document will be overwritten by the new document. Fields previously in the document not present in the new document are removed.
///
/// For a partial update of the document see Add or update documents route.
/// > info
/// > If the provided index does not exist, it will be created.
/// > info
/// > Use the reserved `_geo` object to add geo coordinates to a document. `_geo` is an object made of `lat` and `lng` field.
/// >
/// > When the vectorStore feature is enabled you can use the reserved `_vectors` field in your documents.
/// > It can accept an array of floats, multiple arrays of floats in an outer array or an object.
/// > This object accepts keys corresponding to the different embedders defined your index settings.
#[utoipa::path(
post,
path = "/{indexUid}/documents",
tags = ["Indexes", "Documents"],
security(("Bearer" = ["documents.add", "documents.*", "*"])),
params(
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
// Here we can use the post version of the browse query since it contains the exact same parameter
UpdateDocumentsQuery,
),
responses(
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 147,
"indexUid": null,
"status": "enqueued",
"type": "documentAdditionOrUpdate",
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn replace_documents( pub async fn replace_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
@ -508,6 +635,49 @@ pub async fn replace_documents(
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
/// Add or update documents
///
/// Add a list of documents or update them if they already exist.
/// If you send an already existing document (same id) the old document will be only partially updated according to the fields of the new document. Thus, any fields not present in the new document are kept and remained unchanged.
/// To completely overwrite a document, see Add or replace documents route.
/// > info
/// > If the provided index does not exist, it will be created.
/// > info
/// > Use the reserved `_geo` object to add geo coordinates to a document. `_geo` is an object made of `lat` and `lng` field.
/// >
/// > When the vectorStore feature is enabled you can use the reserved `_vectors` field in your documents.
/// > It can accept an array of floats, multiple arrays of floats in an outer array or an object.
/// > This object accepts keys corresponding to the different embedders defined your index settings.
#[utoipa::path(
put,
path = "/{indexUid}/documents",
tags = ["Indexes", "Documents"],
security(("Bearer" = ["documents.add", "documents.*", "*"])),
params(
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
// Here we can use the post version of the browse query since it contains the exact same parameter
UpdateDocumentsQuery,
),
responses(
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 147,
"indexUid": null,
"status": "enqueued",
"type": "documentAdditionOrUpdate",
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn update_documents( pub async fn update_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
@ -742,6 +912,38 @@ async fn copy_body_to_file(
Ok(read_file) Ok(read_file)
} }
/// Delete documents
///
/// Delete a selection of documents based on array of document id's.
#[utoipa::path(
delete,
path = "/{indexUid}/documents",
tags = ["Indexes", "Documents"],
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
params(
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
),
// TODO: how to return an array of strings
responses(
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 147,
"indexUid": null,
"status": "enqueued",
"type": "documentAdditionOrUpdate",
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn delete_documents_batch( pub async fn delete_documents_batch(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
@ -936,6 +1138,38 @@ pub async fn edit_documents_by_function(
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
/// Delete all documents
///
/// Delete all documents in the specified index.
#[utoipa::path(
delete,
path = "/{indexUid}/documents",
tags = ["Indexes", "Documents"],
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
params(
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
UpdateDocumentsQuery,
),
responses(
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 147,
"indexUid": null,
"status": "enqueued",
"type": "documentDeletion",
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn clear_all_documents( pub async fn clear_all_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,

View File

@ -16,6 +16,7 @@ use meilisearch_types::tasks::KindWithContent;
use serde::Serialize; use serde::Serialize;
use time::OffsetDateTime; use time::OffsetDateTime;
use tracing::debug; use tracing::debug;
use utoipa::{IntoParams, OpenApi, ToSchema};
use super::{get_task_id, Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; use super::{get_task_id, Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT};
use crate::analytics::{Aggregate, Analytics}; use crate::analytics::{Aggregate, Analytics};
@ -36,6 +37,22 @@ mod settings_analytics;
pub mod similar; pub mod similar;
mod similar_analytics; mod similar_analytics;
#[derive(OpenApi)]
#[openapi(
nest(
(path = "/", api = documents::DocumentsApi),
),
paths(list_indexes, create_index, get_index, update_index, delete_index, get_index_stats),
tags(
(
name = "Indexes",
description = "An index is an entity that gathers a set of [documents](https://www.meilisearch.com/docs/learn/getting_started/documents) with its own [settings](https://www.meilisearch.com/docs/reference/api/settings). Learn more about indexes.",
external_docs(url = "https://www.meilisearch.com/docs/reference/api/indexes"),
),
),
)]
pub struct IndexesApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::resource("") web::resource("")
@ -59,14 +76,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct IndexView { pub struct IndexView {
/// Unique identifier for the index
pub uid: String, pub uid: String,
/// An `RFC 3339` format for date/time/duration.
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime, pub created_at: OffsetDateTime,
/// An `RFC 3339` format for date/time/duration.
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]
pub updated_at: OffsetDateTime, pub updated_at: OffsetDateTime,
/// Custom primaryKey for documents
pub primary_key: Option<String>, pub primary_key: Option<String>,
} }
@ -84,20 +105,61 @@ impl IndexView {
} }
} }
#[derive(Deserr, Debug, Clone, Copy)] #[derive(Deserr, Debug, Clone, Copy, IntoParams)]
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase")]
pub struct ListIndexes { pub struct ListIndexes {
/// The number of indexes to skip before starting to retrieve anything
#[param(value_type = Option<usize>, default, example = 100)]
#[deserr(default, error = DeserrQueryParamError<InvalidIndexOffset>)] #[deserr(default, error = DeserrQueryParamError<InvalidIndexOffset>)]
pub offset: Param<usize>, pub offset: Param<usize>,
/// The number of indexes to retrieve
#[param(value_type = Option<usize>, default = 20, example = 1)]
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidIndexLimit>)] #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidIndexLimit>)]
pub limit: Param<usize>, pub limit: Param<usize>,
} }
impl ListIndexes { impl ListIndexes {
fn as_pagination(self) -> Pagination { fn as_pagination(self) -> Pagination {
Pagination { offset: self.offset.0, limit: self.limit.0 } Pagination { offset: self.offset.0, limit: self.limit.0 }
} }
} }
/// List indexes
///
/// List all indexes.
#[utoipa::path(
get,
path = "/",
tag = "Indexes",
security(("Bearer" = ["indexes.get", "indexes.*", "*"])),
params(ListIndexes),
responses(
(status = 200, description = "Indexes are returned", body = serde_json::Value, content_type = "application/json", example = json!(
{
"results": [
{
"uid": "movies",
"primaryKey": "movie_id",
"createdAt": "2019-11-20T09:40:33.711324Z",
"updatedAt": "2019-11-20T09:40:33.711324Z"
}
],
"limit": 1,
"offset": 0,
"total": 1
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn list_indexes( pub async fn list_indexes(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
paginate: AwebQueryParameter<ListIndexes, DeserrQueryParamError>, paginate: AwebQueryParameter<ListIndexes, DeserrQueryParamError>,
@ -121,11 +183,16 @@ pub async fn list_indexes(
Ok(HttpResponse::Ok().json(ret)) Ok(HttpResponse::Ok().json(ret))
} }
#[derive(Deserr, Debug)] #[derive(Deserr, Debug, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
#[schema(rename_all = "camelCase")]
pub struct IndexCreateRequest { pub struct IndexCreateRequest {
/// The name of the index
#[schema(example = "movies")]
#[deserr(error = DeserrJsonError<InvalidIndexUid>, missing_field_error = DeserrJsonError::missing_index_uid)] #[deserr(error = DeserrJsonError<InvalidIndexUid>, missing_field_error = DeserrJsonError::missing_index_uid)]
uid: IndexUid, uid: IndexUid,
/// The primary key of the index
#[schema(example = "id")]
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)] #[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
primary_key: Option<String>, primary_key: Option<String>,
} }
@ -149,6 +216,35 @@ impl Aggregate for IndexCreatedAggregate {
} }
} }
/// Create index
///
/// Create an index.
#[utoipa::path(
post,
path = "/",
tag = "Indexes",
security(("Bearer" = ["indexes.create", "indexes.*", "*"])),
request_body = IndexCreateRequest,
responses(
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 147,
"indexUid": "movies",
"status": "enqueued",
"type": "indexCreation",
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn create_index( pub async fn create_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>,
body: AwebJson<IndexCreateRequest, DeserrJsonError>, body: AwebJson<IndexCreateRequest, DeserrJsonError>,
@ -198,13 +294,42 @@ fn deny_immutable_fields_index(
} }
} }
#[derive(Deserr, Debug)] /// Get index
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)] ///
pub struct UpdateIndexRequest { /// Get information about an index.
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)] #[utoipa::path(
primary_key: Option<String>, get,
} path = "/{indexUid}",
tag = "Indexes",
security(("Bearer" = ["indexes.get", "indexes.*", "*"])),
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
responses(
(status = 200, description = "The index is returned", body = IndexView, content_type = "application/json", example = json!(
{
"uid": "movies",
"primaryKey": "movie_id",
"createdAt": "2019-11-20T09:40:33.711324Z",
"updatedAt": "2019-11-20T09:40:33.711324Z"
}
)),
(status = 404, description = "Index not found", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Index `movies` not found.",
"code": "index_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#index_not_found"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn get_index( pub async fn get_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
@ -237,6 +362,48 @@ impl Aggregate for IndexUpdatedAggregate {
serde_json::to_value(*self).unwrap_or_default() serde_json::to_value(*self).unwrap_or_default()
} }
} }
#[derive(Deserr, Debug, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)]
#[schema(rename_all = "camelCase")]
pub struct UpdateIndexRequest {
/// The new primary key of the index
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
primary_key: Option<String>,
}
/// Update index
///
/// Update the `primaryKey` of an index.
/// Return an error if the index doesn't exists yet or if it contains documents.
#[utoipa::path(
patch,
path = "/{indexUid}",
tag = "Indexes",
security(("Bearer" = ["indexes.update", "indexes.*", "*"])),
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
request_body = UpdateIndexRequest,
responses(
(status = ACCEPTED, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 0,
"indexUid": "movies",
"status": "enqueued",
"type": "indexUpdate",
"enqueuedAt": "2021-01-01T09:39:00.000000Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
>>>>>>> 0f289a437 (Implements the get and delete tasks route):meilisearch/src/routes/indexes/mod.rs
pub async fn update_index( pub async fn update_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_UPDATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_UPDATE }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
@ -269,6 +436,35 @@ pub async fn update_index(
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
/// Delete index
///
/// Delete an index.
#[utoipa::path(
delete,
path = "/{indexUid}",
tag = "Indexes",
security(("Bearer" = ["indexes.delete", "indexes.*", "*"])),
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
responses(
(status = ACCEPTED, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 0,
"indexUid": "movies",
"status": "enqueued",
"type": "indexDeletion",
"enqueuedAt": "2021-01-01T09:39:00.000000Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn delete_index( pub async fn delete_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_DELETE }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
@ -289,14 +485,15 @@ pub async fn delete_index(
} }
/// Stats of an `Index`, as known to the `stats` route. /// Stats of an `Index`, as known to the `stats` route.
#[derive(Serialize, Debug)] #[derive(Serialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct IndexStats { pub struct IndexStats {
/// Number of documents in the index /// Number of documents in the index
pub number_of_documents: u64, pub number_of_documents: u64,
/// Whether the index is currently performing indexation, according to the scheduler. /// Whether or not the index is currently ingesting document
pub is_indexing: bool, pub is_indexing: bool,
/// Association of every field name with the number of times it occurs in the documents. /// Association of every field name with the number of times it occurs in the documents.
#[schema(value_type = HashMap<String, u64>)]
pub field_distribution: FieldDistribution, pub field_distribution: FieldDistribution,
} }
@ -310,6 +507,44 @@ impl From<index_scheduler::IndexStats> for IndexStats {
} }
} }
/// Get stats of index
///
/// Get the stats of an index.
#[utoipa::path(
get,
path = "/{indexUid}/stats",
tags = ["Indexes", "Stats"],
security(("Bearer" = ["stats.get", "stats.*", "*"])),
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
responses(
(status = OK, description = "The stats of the index", body = IndexStats, content_type = "application/json", example = json!(
{
"numberOfDocuments": 10,
"isIndexing": true,
"fieldDistribution": {
"genre": 10,
"author": 9
}
}
)),
(status = 404, description = "Index not found", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Index `movies` not found.",
"code": "index_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#index_not_found"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn get_index_stats( pub async fn get_index_stats(
index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,

View File

@ -14,9 +14,11 @@ use index_scheduler::IndexScheduler;
use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::deserr::DeserrJsonError;
use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::error::{Code, ResponseError};
use serde::Serialize;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing_subscriber::filter::Targets; use tracing_subscriber::filter::Targets;
use tracing_subscriber::Layer; use tracing_subscriber::Layer;
use utoipa::{OpenApi, ToSchema};
use crate::error::MeilisearchHttpError; use crate::error::MeilisearchHttpError;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
@ -24,6 +26,20 @@ use crate::extractors::authentication::GuardedData;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::{LogRouteHandle, LogStderrHandle}; use crate::{LogRouteHandle, LogStderrHandle};
#[derive(OpenApi)]
#[openapi(
paths(get_logs, cancel_logs, update_stderr_target),
tags((
name = "Logs",
description = "Everything about retrieving or customizing logs.
Currently [experimental](https://www.meilisearch.com/docs/learn/experimental/overview).",
external_docs(url = "https://www.meilisearch.com/docs/learn/experimental/log_customization"),
)),
)]
pub struct LogsApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::resource("stream") web::resource("stream")
@ -33,12 +49,16 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(web::resource("stderr").route(web::post().to(SeqHandler(update_stderr_target)))); .service(web::resource("stderr").route(web::post().to(SeqHandler(update_stderr_target))));
} }
#[derive(Debug, Default, Clone, Copy, Deserr, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, Deserr, Serialize, PartialEq, Eq, ToSchema)]
#[deserr(rename_all = camelCase)] #[deserr(rename_all = camelCase)]
#[schema(rename_all = "camelCase")]
pub enum LogMode { pub enum LogMode {
/// Output the logs in a human readable form.
#[default] #[default]
Human, Human,
/// Output the logs in json.
Json, Json,
/// Output the logs in the firefox profiler format. They can then be loaded and visualized at https://profiler.firefox.com/
Profile, Profile,
} }
@ -83,16 +103,26 @@ impl MergeWithError<MyParseError> for DeserrJsonError<BadRequest> {
} }
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields, validate = validate_get_logs -> DeserrJsonError<InvalidSettingsTypoTolerance>)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields, validate = validate_get_logs -> DeserrJsonError<InvalidSettingsTypoTolerance>)]
#[schema(rename_all = "camelCase")]
pub struct GetLogs { pub struct GetLogs {
/// Lets you specify which parts of the code you want to inspect and is formatted like that: code_part=log_level,code_part=log_level
/// - If the `code_part` is missing, then the `log_level` will be applied to everything.
/// - If the `log_level` is missing, then the `code_part` will be selected in `info` log level.
#[deserr(default = "info".parse().unwrap(), try_from(&String) = MyTargets::from_str -> DeserrJsonError<BadRequest>)] #[deserr(default = "info".parse().unwrap(), try_from(&String) = MyTargets::from_str -> DeserrJsonError<BadRequest>)]
#[schema(value_type = String, default = "info", example = json!("milli=trace,index_scheduler,actix_web=off"))]
target: MyTargets, target: MyTargets,
/// Lets you customize the format of the logs.
#[deserr(default, error = DeserrJsonError<BadRequest>)] #[deserr(default, error = DeserrJsonError<BadRequest>)]
#[schema(default = LogMode::default)]
mode: LogMode, mode: LogMode,
/// A boolean to indicate if you want to profile the memory as well. This is only useful while using the `profile` mode.
/// Be cautious, though; it slows down the engine a lot.
#[deserr(default = false, error = DeserrJsonError<BadRequest>)] #[deserr(default = false, error = DeserrJsonError<BadRequest>)]
#[schema(default = false)]
profile_memory: bool, profile_memory: bool,
} }
@ -248,6 +278,46 @@ fn entry_stream(
) )
} }
/// Retrieve logs
///
/// Stream logs over HTTP. The format of the logs depends on the configuration specified in the payload.
/// The logs are sent as multi-part, and the stream never stops, so make sure your clients correctly handle that.
/// To make the server stop sending you logs, you can call the `DELETE /logs/stream` route.
///
/// There can only be one listener at a timeand an error will be returned if you call this route while it's being used by another client.
#[utoipa::path(
post,
path = "/stream",
tag = "Logs",
security(("Bearer" = ["metrics.get", "metrics.*", "*"])),
request_body = GetLogs,
responses(
(status = OK, description = "Logs are being returned", body = String, content_type = "application/json", example = json!(
r#"
2024-10-08T13:35:02.643750Z WARN HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: tracing_actix_web::middleware: Error encountered while processing the incoming HTTP request: ResponseError { code: 400, message: "Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625", error_code: "feature_not_enabled", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#feature_not_enabled" }
2024-10-08T13:35:02.644191Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: meilisearch: close time.busy=1.66ms time.idle=658µs
2024-10-08T13:35:18.564152Z INFO HTTP request{method=PATCH host="localhost:7700" route=/experimental-features query_parameters= user_agent=curl/8.6.0 status_code=200}: meilisearch: close time.busy=1.17ms time.idle=127µs
2024-10-08T13:35:23.094987Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=200}: meilisearch: close time.busy=2.12ms time.idle=595µs
"#
)),
(status = 400, description = "The route is already being used", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "The `/logs/stream` route is currently in use by someone else.",
"code": "bad_request",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad_request"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn get_logs( pub async fn get_logs(
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
logs: Data<LogRouteHandle>, logs: Data<LogRouteHandle>,
@ -280,6 +350,27 @@ pub async fn get_logs(
} }
} }
/// Stop retrieving logs
///
/// Call this route to make the engine stops sending logs through the `POST /logs/stream` route.
#[utoipa::path(
delete,
path = "/stream",
tag = "Logs",
security(("Bearer" = ["metrics.get", "metrics.*", "*"])),
responses(
(status = NO_CONTENT, description = "Logs are being returned"),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn cancel_logs( pub async fn cancel_logs(
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
logs: Data<LogRouteHandle>, logs: Data<LogRouteHandle>,
@ -293,13 +384,38 @@ pub async fn cancel_logs(
Ok(HttpResponse::NoContent().finish()) Ok(HttpResponse::NoContent().finish())
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, ToSchema)]
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct UpdateStderrLogs { pub struct UpdateStderrLogs {
/// Lets you specify which parts of the code you want to inspect and is formatted like that: code_part=log_level,code_part=log_level
/// - If the `code_part` is missing, then the `log_level` will be applied to everything.
/// - If the `log_level` is missing, then the `code_part` will be selected in `info` log level.
#[deserr(default = "info".parse().unwrap(), try_from(&String) = MyTargets::from_str -> DeserrJsonError<BadRequest>)] #[deserr(default = "info".parse().unwrap(), try_from(&String) = MyTargets::from_str -> DeserrJsonError<BadRequest>)]
#[schema(value_type = String, default = "info", example = json!("milli=trace,index_scheduler,actix_web=off"))]
target: MyTargets, target: MyTargets,
} }
/// Update target of the console logs
///
/// This route lets you specify at runtime the level of the console logs outputted on stderr.
#[utoipa::path(
post,
path = "/stderr",
tag = "Logs",
request_body = UpdateStderrLogs,
security(("Bearer" = ["metrics.get", "metrics.*", "*"])),
responses(
(status = NO_CONTENT, description = "The console logs have been updated"),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn update_stderr_target( pub async fn update_stderr_target(
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
logs: Data<LogStderrHandle>, logs: Data<LogStderrHandle>,

View File

@ -1,12 +1,17 @@
use crate::extractors::authentication::policies::ActionPolicy;
use crate::extractors::authentication::{AuthenticationError, GuardedData};
use crate::routes::create_all_stats;
use crate::search_queue::SearchQueue;
use actix_web::http::header; use actix_web::http::header;
use actix_web::web::{self, Data}; use actix_web::web::{self, Data};
use actix_web::HttpResponse; use actix_web::HttpResponse;
use index_scheduler::{IndexScheduler, Query}; use index_scheduler::IndexScheduler;
use meilisearch_auth::AuthController; use meilisearch_auth::AuthController;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::keys::actions; use meilisearch_types::keys::actions;
use meilisearch_types::tasks::Status;
use prometheus::{Encoder, TextEncoder}; use prometheus::{Encoder, TextEncoder};
use utoipa::OpenApi;
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::extractors::authentication::policies::ActionPolicy; use crate::extractors::authentication::policies::ActionPolicy;
@ -14,10 +19,100 @@ use crate::extractors::authentication::{AuthenticationError, GuardedData};
use crate::routes::create_all_stats; use crate::routes::create_all_stats;
use crate::search_queue::SearchQueue; use crate::search_queue::SearchQueue;
#[derive(OpenApi)]
#[openapi(paths(get_metrics))]
pub struct MetricApi;
pub fn configure(config: &mut web::ServiceConfig) { pub fn configure(config: &mut web::ServiceConfig) {
config.service(web::resource("").route(web::get().to(get_metrics))); config.service(web::resource("").route(web::get().to(get_metrics)));
} }
/// Get prometheus metrics
///
/// Retrieve metrics on the engine. See https://www.meilisearch.com/docs/learn/experimental/metrics
/// Currently, [the feature is experimental](https://www.meilisearch.com/docs/learn/experimental/overview)
/// which means it must be enabled.
#[utoipa::path(
get,
path = "/",
tag = "Stats",
security(("Bearer" = ["metrics.get", "metrics.*", "*"])),
responses(
(status = 200, description = "The metrics of the instance", body = String, content_type = "text/plain", example = json!(
r#"
# HELP meilisearch_db_size_bytes Meilisearch DB Size In Bytes
# TYPE meilisearch_db_size_bytes gauge
meilisearch_db_size_bytes 1130496
# HELP meilisearch_http_requests_total Meilisearch HTTP requests total
# TYPE meilisearch_http_requests_total counter
meilisearch_http_requests_total{method="GET",path="/metrics",status="400"} 1
meilisearch_http_requests_total{method="PATCH",path="/experimental-features",status="200"} 1
# HELP meilisearch_http_response_time_seconds Meilisearch HTTP response times
# TYPE meilisearch_http_response_time_seconds histogram
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.005"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.01"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.025"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.05"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.075"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.1"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.25"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.5"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="0.75"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="1"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="2.5"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="5"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="7.5"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="10"} 0
meilisearch_http_response_time_seconds_bucket{method="GET",path="/metrics",le="+Inf"} 0
meilisearch_http_response_time_seconds_sum{method="GET",path="/metrics"} 0
meilisearch_http_response_time_seconds_count{method="GET",path="/metrics"} 0
# HELP meilisearch_index_count Meilisearch Index Count
# TYPE meilisearch_index_count gauge
meilisearch_index_count 1
# HELP meilisearch_index_docs_count Meilisearch Index Docs Count
# TYPE meilisearch_index_docs_count gauge
meilisearch_index_docs_count{index="mieli"} 2
# HELP meilisearch_is_indexing Meilisearch Is Indexing
# TYPE meilisearch_is_indexing gauge
meilisearch_is_indexing 0
# HELP meilisearch_last_update Meilisearch Last Update
# TYPE meilisearch_last_update gauge
meilisearch_last_update 1726675964
# HELP meilisearch_nb_tasks Meilisearch Number of tasks
# TYPE meilisearch_nb_tasks gauge
meilisearch_nb_tasks{kind="indexes",value="mieli"} 39
meilisearch_nb_tasks{kind="statuses",value="canceled"} 0
meilisearch_nb_tasks{kind="statuses",value="enqueued"} 0
meilisearch_nb_tasks{kind="statuses",value="failed"} 4
meilisearch_nb_tasks{kind="statuses",value="processing"} 0
meilisearch_nb_tasks{kind="statuses",value="succeeded"} 35
meilisearch_nb_tasks{kind="types",value="documentAdditionOrUpdate"} 9
meilisearch_nb_tasks{kind="types",value="documentDeletion"} 0
meilisearch_nb_tasks{kind="types",value="documentEdition"} 0
meilisearch_nb_tasks{kind="types",value="dumpCreation"} 0
meilisearch_nb_tasks{kind="types",value="indexCreation"} 0
meilisearch_nb_tasks{kind="types",value="indexDeletion"} 8
meilisearch_nb_tasks{kind="types",value="indexSwap"} 0
meilisearch_nb_tasks{kind="types",value="indexUpdate"} 0
meilisearch_nb_tasks{kind="types",value="settingsUpdate"} 22
meilisearch_nb_tasks{kind="types",value="snapshotCreation"} 0
meilisearch_nb_tasks{kind="types",value="taskCancelation"} 0
meilisearch_nb_tasks{kind="types",value="taskDeletion"} 0
# HELP meilisearch_used_db_size_bytes Meilisearch Used DB Size In Bytes
# TYPE meilisearch_used_db_size_bytes gauge
meilisearch_used_db_size_bytes 409600
"#
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn get_metrics( pub async fn get_metrics(
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
auth_controller: Data<AuthController>, auth_controller: Data<AuthController>,

View File

@ -1,20 +1,38 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse};
use index_scheduler::IndexScheduler;
use meilisearch_auth::AuthController;
use meilisearch_types::error::{Code, ResponseError};
use meilisearch_types::settings::{Settings, Unchecked};
use meilisearch_types::tasks::{Kind, Status, Task, TaskId};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use tracing::debug;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData; use crate::extractors::authentication::GuardedData;
use crate::search_queue::SearchQueue; use crate::search_queue::SearchQueue;
use crate::Opt; use crate::Opt;
use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse};
use index_scheduler::IndexScheduler;
use meilisearch_auth::AuthController;
use meilisearch_types::error::{Code, ErrorType, ResponseError};
use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::keys::CreateApiKey;
use meilisearch_types::settings::{
Checked, FacetingSettings, MinWordSizeTyposSetting, PaginationSettings, Settings, TypoSettings,
Unchecked,
};
use meilisearch_types::task_view::{DetailsView, TaskView};
use meilisearch_types::tasks::{Kind, Status, Task, TaskId};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use tracing::debug;
use utoipa::{OpenApi, ToSchema};
use utoipa_rapidoc::RapiDoc;
use utoipa_redoc::{Redoc, Servable};
use utoipa_scalar::{Scalar, Servable as ScalarServable};
use self::api_key::KeyView;
use self::indexes::documents::BrowseQuery;
use self::indexes::{IndexCreateRequest, IndexStats, UpdateIndexRequest};
use self::logs::GetLogs;
use self::logs::LogMode;
use self::logs::UpdateStderrLogs;
use self::open_api_utils::OpenApiAuth;
use self::tasks::AllTasks;
const PAGINATION_DEFAULT_LIMIT: usize = 20; const PAGINATION_DEFAULT_LIMIT: usize = 20;
@ -27,24 +45,50 @@ mod logs;
mod metrics; mod metrics;
mod multi_search; mod multi_search;
mod multi_search_analytics; mod multi_search_analytics;
mod open_api_utils;
mod snapshot; mod snapshot;
mod swap_indexes; mod swap_indexes;
pub mod tasks; pub mod tasks;
#[derive(OpenApi)]
#[openapi(
nest(
(path = "/tasks", api = tasks::TaskApi),
(path = "/indexes", api = indexes::IndexesApi),
(path = "/snapshots", api = snapshot::SnapshotApi),
(path = "/dumps", api = dump::DumpApi),
(path = "/keys", api = api_key::ApiKeyApi),
(path = "/metrics", api = metrics::MetricApi),
(path = "/logs", api = logs::LogsApi),
),
paths(get_health, get_version, get_stats),
tags(
(name = "Stats", description = "Stats gives extended information and metrics about indexes and the Meilisearch database."),
),
modifiers(&OpenApiAuth),
components(schemas(BrowseQuery, UpdateIndexRequest, IndexUid, IndexCreateRequest, KeyView, Action, CreateApiKey, UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind))
)]
pub struct MeilisearchApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("/tasks").configure(tasks::configure)) let openapi = MeilisearchApi::openapi();
.service(web::scope("/batches").configure(batches::configure))
.service(web::resource("/health").route(web::get().to(get_health))) cfg.service(web::scope("/tasks").configure(tasks::configure)) // done
.service(web::scope("/logs").configure(logs::configure)) .service(web::scope("/batches").configure(batches::configure)) // TODO
.service(web::scope("/keys").configure(api_key::configure)) .service(Scalar::with_url("/scalar", openapi.clone())) // done
.service(web::scope("/dumps").configure(dump::configure)) .service(RapiDoc::with_openapi("/api-docs/openapi.json", openapi.clone()).path("/rapidoc")) // done
.service(web::scope("/snapshots").configure(snapshot::configure)) .service(Redoc::with_url("/redoc", openapi)) // done
.service(web::resource("/stats").route(web::get().to(get_stats))) .service(web::resource("/health").route(web::get().to(get_health))) // done
.service(web::resource("/version").route(web::get().to(get_version))) .service(web::scope("/logs").configure(logs::configure)) // done
.service(web::scope("/indexes").configure(indexes::configure)) .service(web::scope("/keys").configure(api_key::configure)) // done
.service(web::scope("/multi-search").configure(multi_search::configure)) .service(web::scope("/dumps").configure(dump::configure)) // done
.service(web::scope("/swap-indexes").configure(swap_indexes::configure)) .service(web::scope("/snapshots").configure(snapshot::configure)) // done
.service(web::scope("/metrics").configure(metrics::configure)) .service(web::resource("/stats").route(web::get().to(get_stats))) // done
.service(web::resource("/version").route(web::get().to(get_version))) // done
.service(web::scope("/indexes").configure(indexes::configure)) // WIP
.service(web::scope("/multi-search").configure(multi_search::configure)) // TODO
.service(web::scope("/swap-indexes").configure(swap_indexes::configure)) // TODO
.service(web::scope("/metrics").configure(metrics::configure)) // done
.service(web::scope("/experimental-features").configure(features::configure)); .service(web::scope("/experimental-features").configure(features::configure));
} }
@ -98,14 +142,20 @@ pub fn is_dry_run(req: &HttpRequest, opt: &Opt) -> Result<bool, ResponseError> {
.map_or(false, |s| s.to_lowercase() == "true")) .map_or(false, |s| s.to_lowercase() == "true"))
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SummarizedTaskView { pub struct SummarizedTaskView {
/// The task unique identifier.
#[schema(value_type = u32)]
task_uid: TaskId, task_uid: TaskId,
/// The index affected by this task. May be `null` if the task is not linked to any index.
index_uid: Option<String>, index_uid: Option<String>,
/// The status of the task.
status: Status, status: Status,
/// The type of the task.
#[serde(rename = "type")] #[serde(rename = "type")]
kind: Kind, kind: Kind,
/// The date on which the task was enqueued.
#[serde(serialize_with = "time::serde::rfc3339::serialize")] #[serde(serialize_with = "time::serde::rfc3339::serialize")]
enqueued_at: OffsetDateTime, enqueued_at: OffsetDateTime,
} }
@ -128,6 +178,7 @@ pub struct Pagination {
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PaginationView<T> { pub struct PaginationView<T> {
pub results: Vec<T>, pub results: Vec<T>,
pub offset: usize, pub offset: usize,
@ -283,17 +334,56 @@ pub async fn running() -> HttpResponse {
HttpResponse::Ok().json(serde_json::json!({ "status": "Meilisearch is running" })) HttpResponse::Ok().json(serde_json::json!({ "status": "Meilisearch is running" }))
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Stats { pub struct Stats {
/// The size of the database, in bytes.
pub database_size: u64, pub database_size: u64,
#[serde(skip)] #[serde(skip)]
pub used_database_size: u64, pub used_database_size: u64,
/// The date of the last update in the RFC 3339 formats. Can be `null` if no update has ever been processed.
#[serde(serialize_with = "time::serde::rfc3339::option::serialize")] #[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
pub last_update: Option<OffsetDateTime>, pub last_update: Option<OffsetDateTime>,
/// The stats of every individual index your API key lets you access.
#[schema(value_type = HashMap<String, indexes::IndexStats>)]
pub indexes: BTreeMap<String, indexes::IndexStats>, pub indexes: BTreeMap<String, indexes::IndexStats>,
} }
/// Get stats of all indexes.
///
/// Get stats of all indexes.
#[utoipa::path(
get,
path = "/stats",
tag = "Stats",
security(("Bearer" = ["stats.get", "stats.*", "*"])),
responses(
(status = 200, description = "The stats of the instance", body = Stats, content_type = "application/json", example = json!(
{
"databaseSize": 567,
"lastUpdate": "2019-11-20T09:40:33.711324Z",
"indexes": {
"movies": {
"numberOfDocuments": 10,
"isIndexing": true,
"fieldDistribution": {
"genre": 10,
"author": 9
}
}
}
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
async fn get_stats( async fn get_stats(
index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>,
auth_controller: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<AuthController>>, auth_controller: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<AuthController>>,
@ -343,14 +433,43 @@ pub fn create_all_stats(
Ok(stats) Ok(stats)
} }
#[derive(Serialize)] #[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct VersionResponse { struct VersionResponse {
/// The commit used to compile this build of Meilisearch.
commit_sha: String, commit_sha: String,
/// The date of this build.
commit_date: String, commit_date: String,
/// The version of Meilisearch.
pkg_version: String, pkg_version: String,
} }
/// Get version
///
/// Current version of Meilisearch.
#[utoipa::path(
get,
path = "/version",
tag = "Version",
security(("Bearer" = ["version", "*"])),
responses(
(status = 200, description = "Instance is healthy", body = VersionResponse, content_type = "application/json", example = json!(
{
"commitSha": "b46889b5f0f2f8b91438a08a358ba8f05fc09fc1",
"commitDate": "2021-07-08",
"pkgVersion": "0.23.0"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
async fn get_version( async fn get_version(
_index_scheduler: GuardedData<ActionPolicy<{ actions::VERSION }>, Data<IndexScheduler>>, _index_scheduler: GuardedData<ActionPolicy<{ actions::VERSION }>, Data<IndexScheduler>>,
) -> HttpResponse { ) -> HttpResponse {
@ -370,6 +489,35 @@ async fn get_version(
}) })
} }
#[derive(Default, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
struct HealthResponse {
/// The status of the instance.
status: HealthStatus,
}
#[derive(Default, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
enum HealthStatus {
#[default]
Available,
}
/// Get Health
///
/// The health check endpoint enables you to periodically test the health of your Meilisearch instance.
#[utoipa::path(
get,
path = "/health",
tag = "Health",
responses(
(status = 200, description = "Instance is healthy", body = HealthResponse, content_type = "application/json", example = json!(
{
"status": "available"
}
)),
)
)]
pub async fn get_health( pub async fn get_health(
index_scheduler: Data<IndexScheduler>, index_scheduler: Data<IndexScheduler>,
auth_controller: Data<AuthController>, auth_controller: Data<AuthController>,
@ -379,5 +527,5 @@ pub async fn get_health(
index_scheduler.health().unwrap(); index_scheduler.health().unwrap();
auth_controller.health().unwrap(); auth_controller.health().unwrap();
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "available" }))) Ok(HttpResponse::Ok().json(&HealthResponse::default()))
} }

View File

@ -0,0 +1,24 @@
use serde::Serialize;
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
#[derive(Debug, Serialize)]
pub struct OpenApiAuth;
impl utoipa::Modify for OpenApiAuth {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(schema) = openapi.components.as_mut() {
schema.add_security_scheme(
"Bearer",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("Uuidv4, string or JWT")
.description(Some(
"An API key is a token that you provide when making API calls. Include the token in a header parameter called `Authorization`.
Example: `Authorization: Bearer 8fece4405662dd830e4cb265e7e047aab2e79672a760a12712d2a263c9003509`"))
.build(),
),
);
}
}
}

View File

@ -4,6 +4,7 @@ use index_scheduler::IndexScheduler;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::tasks::KindWithContent; use meilisearch_types::tasks::KindWithContent;
use tracing::debug; use tracing::debug;
use utoipa::OpenApi;
use crate::analytics::Analytics; use crate::analytics::Analytics;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
@ -12,12 +13,56 @@ use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView}; use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView};
use crate::Opt; use crate::Opt;
#[derive(OpenApi)]
#[openapi(
paths(create_snapshot),
tags((
name = "Snapshots",
description = "The snapshots route allows the creation of database snapshots. Snapshots are .snapshot files that can be used to launch Meilisearch.
Creating a snapshot is also referred to as exporting it, whereas launching Meilisearch with a snapshot is referred to as importing it.
During a snapshot export, all indexes of the current instance are exportedtogether with their documents and settingsand saved as a single .snapshot file.
During a snapshot import, all indexes contained in the indicated .snapshot file are imported along with their associated documents and settings.
Snapshot imports are performed at launch using an option.",
external_docs(url = "https://www.meilisearch.com/docs/reference/api/snapshots"),
)),
)]
pub struct SnapshotApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("").route(web::post().to(SeqHandler(create_snapshot)))); cfg.service(web::resource("").route(web::post().to(SeqHandler(create_snapshot))));
} }
crate::empty_analytics!(SnapshotAnalytics, "Snapshot Created"); crate::empty_analytics!(SnapshotAnalytics, "Snapshot Created");
/// Create a snapshot
///
/// Triggers a snapshot creation process. Once the process is complete, a snapshot is created in the snapshot directory. If the snapshot directory does not exist yet, it will be created.
#[utoipa::path(
post,
path = "/",
tag = "Snapshots",
security(("Bearer" = ["snapshots.create", "snapshots.*", "*"])),
responses(
(status = 202, description = "Snapshot is being created", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 0,
"indexUid": null,
"status": "enqueued",
"type": "snapshotCreation",
"enqueuedAt": "2021-01-01T09:39:00.000000Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
pub async fn create_snapshot( pub async fn create_snapshot(
index_scheduler: GuardedData<ActionPolicy<{ actions::SNAPSHOTS_CREATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::SNAPSHOTS_CREATE }>, Data<IndexScheduler>>,
req: HttpRequest, req: HttpRequest,

View File

@ -17,14 +17,29 @@ use time::format_description::well_known::Rfc3339;
use time::macros::format_description; use time::macros::format_description;
use time::{Date, Duration, OffsetDateTime, Time}; use time::{Date, Duration, OffsetDateTime, Time};
use tokio::task; use tokio::task;
use utoipa::{IntoParams, OpenApi, ToSchema};
use super::{get_task_id, is_dry_run, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; use super::{get_task_id, is_dry_run, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT};
use crate::analytics::{Aggregate, AggregateMethod, Analytics}; use crate::analytics::Analytics;
use super::{get_task_id, is_dry_run, SummarizedTaskView};
use crate::analytics::Analytics;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData; use crate::extractors::authentication::GuardedData;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::{aggregate_methods, Opt}; use crate::{aggregate_methods, Opt};
#[derive(OpenApi)]
#[openapi(
paths(get_tasks, delete_tasks, cancel_tasks, get_task),
tags((
name = "Tasks",
description = "The tasks route gives information about the progress of the [asynchronous operations](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html).",
external_docs(url = "https://www.meilisearch.com/docs/reference/api/tasks"),
)),
)]
pub struct TaskApi;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
web::resource("") web::resource("")
@ -35,41 +50,66 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(web::resource("/{task_id}").route(web::get().to(SeqHandler(get_task)))); .service(web::resource("/{task_id}").route(web::get().to(SeqHandler(get_task))));
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, IntoParams)]
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct TasksFilterQuery { pub struct TasksFilterQuery {
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT as u32), error = DeserrQueryParamError<InvalidTaskLimit>)] /// Maximum number of results to return.
#[deserr(default = Param(DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidTaskLimit>)]
#[param(required = false, value_type = u32, example = 12, default = json!(DEFAULT_LIMIT))]
pub limit: Param<u32>, pub limit: Param<u32>,
/// Fetch the next set of results from the given uid.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskFrom>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskFrom>)]
#[param(required = false, value_type = Option<u32>, example = 12421)]
pub from: Option<Param<TaskId>>, pub from: Option<Param<TaskId>>,
/// The order you want to retrieve the objects.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskReverse>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskReverse>)]
#[param(required = false, value_type = Option<bool>, example = true)]
pub reverse: Option<Param<bool>>, pub reverse: Option<Param<bool>>,
/// Permits to filter tasks by their uid. By default, when the uids query parameter is not set, all task uids are returned. It's possible to specify several uids by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidBatchUids>)]
pub batch_uids: OptionStarOrList<BatchId>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
pub uids: OptionStarOrList<u32>, pub uids: OptionStarOrList<u32>,
/// Permits to filter tasks using the uid of the task that canceled them. It's possible to specify several task uids by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([374, "*"]))]
pub canceled_by: OptionStarOrList<u32>, pub canceled_by: OptionStarOrList<u32>,
/// Permits to filter tasks by their related type. By default, when `types` query parameter is not set, all task types are returned. It's possible to specify several types by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)]
#[param(required = false, value_type = Option<Vec<String>>, example = json!([Kind::DocumentAdditionOrUpdate, "*"]))]
pub types: OptionStarOrList<Kind>, pub types: OptionStarOrList<Kind>,
/// Permits to filter tasks by their status. By default, when `statuses` query parameter is not set, all task statuses are returned. It's possible to specify several statuses by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)]
#[param(required = false, value_type = Option<Vec<Status>>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, Status::Enqueued, Status::Processing, "*"]))]
pub statuses: OptionStarOrList<Status>, pub statuses: OptionStarOrList<Status>,
/// Permits to filter tasks by their related index. By default, when `indexUids` query parameter is not set, the tasks of all the indexes are returned. It is possible to specify several indexes by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)] #[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)]
#[param(required = false, value_type = Option<Vec<String>>, example = json!(["movies", "theater", "*"]))]
pub index_uids: OptionStarOrList<IndexUid>, pub index_uids: OptionStarOrList<IndexUid>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_enqueued_at: OptionStarOr<OffsetDateTime>, pub after_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_enqueued_at: OptionStarOr<OffsetDateTime>, pub before_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_started_at: OptionStarOr<OffsetDateTime>, pub after_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_started_at: OptionStarOr<OffsetDateTime>, pub before_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_finished_at: OptionStarOr<OffsetDateTime>, pub after_finished_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_finished_at: OptionStarOr<OffsetDateTime>, pub before_finished_at: OptionStarOr<OffsetDateTime>,
} }
@ -117,33 +157,58 @@ impl TaskDeletionOrCancelationQuery {
} }
} }
#[derive(Debug, Deserr)] #[derive(Debug, Deserr, IntoParams)]
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct TaskDeletionOrCancelationQuery { pub struct TaskDeletionOrCancelationQuery {
/// Permits to filter tasks by their uid. By default, when the `uids` query parameter is not set, all task uids are returned. It's possible to specify several uids by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)]
pub uids: OptionStarOrList<TaskId>, #[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
pub uids: OptionStarOrList<u32>,
/// Lets you filter tasks by their `batchUid`.
#[deserr(default, error = DeserrQueryParamError<InvalidBatchUids>)] #[deserr(default, error = DeserrQueryParamError<InvalidBatchUids>)]
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
pub batch_uids: OptionStarOrList<BatchId>, pub batch_uids: OptionStarOrList<BatchId>,
/// Permits to filter tasks using the uid of the task that canceled them. It's possible to specify several task uids by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)]
pub canceled_by: OptionStarOrList<TaskId>, #[param(required = false, value_type = Option<Vec<u32>>, example = json!([374, "*"]))]
pub canceled_by: OptionStarOrList<u32>,
/// Permits to filter tasks by their related type. By default, when `types` query parameter is not set, all task types are returned. It's possible to specify several types by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)]
#[param(required = false, value_type = Option<Vec<Kind>>, example = json!([Kind::DocumentDeletion, "*"]))]
pub types: OptionStarOrList<Kind>, pub types: OptionStarOrList<Kind>,
/// Permits to filter tasks by their status. By default, when `statuses` query parameter is not set, all task statuses are returned. It's possible to specify several statuses by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)]
#[param(required = false, value_type = Option<Vec<Status>>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, "*"]))]
pub statuses: OptionStarOrList<Status>, pub statuses: OptionStarOrList<Status>,
/// Permits to filter tasks by their related index. By default, when `indexUids` query parameter is not set, the tasks of all the indexes are returned. It is possible to specify several indexes by separating them with the `,` character.
#[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)] #[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)]
#[param(required = false, value_type = Option<Vec<String>>, example = json!(["movies", "theater", "*"]))]
pub index_uids: OptionStarOrList<IndexUid>, pub index_uids: OptionStarOrList<IndexUid>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_enqueued_at: OptionStarOr<OffsetDateTime>, pub after_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their enqueuedAt time. Matches tasks enqueued before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_enqueued_at: OptionStarOr<OffsetDateTime>, pub before_enqueued_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_started_at: OptionStarOr<OffsetDateTime>, pub after_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their startedAt time. Matches tasks started before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_started_at: OptionStarOr<OffsetDateTime>, pub before_started_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished after the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub after_finished_at: OptionStarOr<OffsetDateTime>, pub after_finished_at: OptionStarOr<OffsetDateTime>,
/// Permits to filter tasks based on their finishedAt time. Matches tasks finished before the given date. Supports RFC 3339 date format.
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, try_from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
pub before_finished_at: OptionStarOr<OffsetDateTime>, pub before_finished_at: OptionStarOr<OffsetDateTime>,
} }
@ -226,6 +291,51 @@ impl<Method: AggregateMethod + 'static> Aggregate for TaskFilterAnalytics<Method
} }
} }
/// Cancel tasks
///
/// Cancel enqueued and/or processing [tasks](https://www.meilisearch.com/docs/learn/async/asynchronous_operations)
#[utoipa::path(
post,
path = "/cancel",
tag = "Tasks",
security(("Bearer" = ["tasks.cancel", "tasks.*", "*"])),
params(TaskDeletionOrCancelationQuery),
responses(
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 147,
"indexUid": null,
"status": "enqueued",
"type": "taskCancelation",
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
}
)),
(status = 400, description = "A filter is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.",
"code": "missing_task_filters",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#missing_task_filters"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 404, description = "The task uid does not exists", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Task :taskUid not found.",
"code": "task_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors/#task_not_found"
}
))
)
)]
async fn cancel_tasks( async fn cancel_tasks(
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_CANCEL }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_CANCEL }>, Data<IndexScheduler>>,
params: AwebQueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>, params: AwebQueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>,
@ -275,6 +385,51 @@ async fn cancel_tasks(
Ok(HttpResponse::Ok().json(task)) Ok(HttpResponse::Ok().json(task))
} }
/// Delete tasks
///
/// Delete [tasks](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html) on filter
#[utoipa::path(
delete,
path = "",
tag = "Tasks",
security(("Bearer" = ["tasks.delete", "tasks.*", "*"])),
params(TaskDeletionOrCancelationQuery),
responses(
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
{
"taskUid": 147,
"indexUid": null,
"status": "enqueued",
"type": "taskDeletion",
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
}
)),
(status = 400, description = "A filter is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.",
"code": "missing_task_filters",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#missing_task_filters"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 404, description = "The task uid does not exists", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Task :taskUid not found.",
"code": "task_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors/#task_not_found"
}
))
)
)]
async fn delete_tasks( async fn delete_tasks(
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_DELETE }>, Data<IndexScheduler>>,
params: AwebQueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>, params: AwebQueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>,
@ -323,15 +478,70 @@ async fn delete_tasks(
Ok(HttpResponse::Ok().json(task)) Ok(HttpResponse::Ok().json(task))
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, ToSchema)]
pub struct AllTasks { pub struct AllTasks {
/// The list of tasks that matched the filter.
results: Vec<TaskView>, results: Vec<TaskView>,
/// Total number of browsable results using offset/limit parameters for the given resource.
total: u64, total: u64,
/// Limit given for the query. If limit is not provided as a query parameter, this parameter displays the default limit value.
limit: u32, limit: u32,
/// The first task uid returned.
from: Option<u32>, from: Option<u32>,
/// Represents the value to send in from to fetch the next slice of the results. The first item for the next slice starts at this exact number. When the returned value is null, it means that all the data have been browsed in the given order.
next: Option<u32>, next: Option<u32>,
} }
/// Get all tasks
///
/// Get all [tasks](https://docs.meilisearch.com/learn/advanced/asynchronous_operations.html)
#[utoipa::path(
get,
path = "",
tag = "Tasks",
security(("Bearer" = ["tasks.get", "tasks.*", "*"])),
params(TasksFilterQuery),
responses(
(status = 200, description = "Get all tasks", body = AllTasks, content_type = "application/json", example = json!(
{
"results": [
{
"uid": 144,
"indexUid": "mieli",
"status": "succeeded",
"type": "settingsUpdate",
"canceledBy": null,
"details": {
"settings": {
"filterableAttributes": [
"play_count"
]
}
},
"error": null,
"duration": "PT0.009330S",
"enqueuedAt": "2024-08-08T09:01:13.348471Z",
"startedAt": "2024-08-08T09:01:13.349442Z",
"finishedAt": "2024-08-08T09:01:13.358772Z"
}
],
"total": 1,
"limit": 1,
"from": 144,
"next": null
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
)
)]
async fn get_tasks( async fn get_tasks(
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>,
params: AwebQueryParameter<TasksFilterQuery, DeserrQueryParamError>, params: AwebQueryParameter<TasksFilterQuery, DeserrQueryParamError>,
@ -356,6 +566,52 @@ async fn get_tasks(
Ok(HttpResponse::Ok().json(tasks)) Ok(HttpResponse::Ok().json(tasks))
} }
/// Get a task
///
/// Get a [task](https://www.meilisearch.com/docs/learn/async/asynchronous_operations)
#[utoipa::path(
get,
path = "/{taskUid}",
tag = "Tasks",
security(("Bearer" = ["tasks.get", "tasks.*", "*"])),
params(("taskUid", format = UInt32, example = 0, description = "The task identifier", nullable = false)),
responses(
(status = 200, description = "Task successfully retrieved", body = TaskView, content_type = "application/json", example = json!(
{
"uid": 1,
"indexUid": "movies",
"status": "succeeded",
"type": "documentAdditionOrUpdate",
"canceledBy": null,
"details": {
"receivedDocuments": 79000,
"indexedDocuments": 79000
},
"error": null,
"duration": "PT1S",
"enqueuedAt": "2021-01-01T09:39:00.000000Z",
"startedAt": "2021-01-01T09:39:01.000000Z",
"finishedAt": "2021-01-01T09:39:02.000000Z"
}
)),
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
{
"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"
}
)),
(status = 404, description = "The task uid does not exists", body = ResponseError, content_type = "application/json", example = json!(
{
"message": "Task :taskUid not found.",
"code": "task_not_found",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors/#task_not_found"
}
))
)
)]
async fn get_task( async fn get_task(
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>,
task_uid: web::Path<String>, task_uid: web::Path<String>,

View File

@ -90,6 +90,7 @@ tracing = "0.1.40"
ureq = { version = "2.10.0", features = ["json"] } ureq = { version = "2.10.0", features = ["json"] }
url = "2.5.2" url = "2.5.2"
rayon-par-bridge = "0.1.0" rayon-par-bridge = "0.1.0"
<<<<<<< HEAD:crates/milli/Cargo.toml
hashbrown = "0.15.0" hashbrown = "0.15.0"
bumpalo = "3.16.0" bumpalo = "3.16.0"
bumparaw-collections = "0.1.2" bumparaw-collections = "0.1.2"
@ -100,6 +101,7 @@ uell = "0.1.0"
enum-iterator = "2.1.0" enum-iterator = "2.1.0"
bbqueue = { git = "https://github.com/meilisearch/bbqueue" } bbqueue = { git = "https://github.com/meilisearch/bbqueue" }
flume = { version = "0.11.1", default-features = false } flume = { version = "0.11.1", default-features = false }
utoipa = { version = "5.0.0-rc.0", features = ["non_strict_integers", "preserve_order", "uuid", "time", "openapi_extensions"] }
[dev-dependencies] [dev-dependencies]
mimalloc = { version = "0.1.43", default-features = false } mimalloc = { version = "0.1.43", default-features = false }

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use charabia::Language; use charabia::Language;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::fields_ids_map::FieldsIdsMap; use crate::fields_ids_map::FieldsIdsMap;
use crate::FieldId; use crate::FieldId;
@ -14,9 +15,10 @@ use crate::FieldId;
/// The pattern `attribute_name*` matches any attribute name that starts with `attribute_name`. /// The pattern `attribute_name*` matches any attribute name that starts with `attribute_name`.
/// The pattern `*attribute_name` matches any attribute name that ends with `attribute_name`. /// The pattern `*attribute_name` matches any attribute name that ends with `attribute_name`.
/// The pattern `*attribute_name*` matches any attribute name that contains `attribute_name`. /// The pattern `*attribute_name*` matches any attribute name that contains `attribute_name`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct LocalizedAttributesRule { pub struct LocalizedAttributesRule {
pub attribute_patterns: Vec<String>, pub attribute_patterns: Vec<String>,
#[schema(value_type = Vec<String>)]
pub locales: Vec<Language>, pub locales: Vec<Language>,
} }

View File

@ -10,6 +10,7 @@ use itertools::{EitherOrBoth, Itertools};
use roaring::RoaringBitmap; use roaring::RoaringBitmap;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::OffsetDateTime; use time::OffsetDateTime;
use utoipa::{PartialSchema, ToSchema};
use super::del_add::DelAddOperation; use super::del_add::DelAddOperation;
use super::index_documents::{IndexDocumentsConfig, Transform}; use super::index_documents::{IndexDocumentsConfig, Transform};
@ -40,6 +41,18 @@ pub enum Setting<T> {
NotSet, NotSet,
} }
impl<T: ToSchema> ToSchema for Setting<T> {
fn name() -> std::borrow::Cow<'static, str> {
T::name()
}
}
impl<T: PartialSchema> PartialSchema for Setting<T> {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
T::schema()
}
}
impl<T, E> Deserr<E> for Setting<T> impl<T, E> Deserr<E> for Setting<T>
where where
T: Deserr<E>, T: Deserr<E>,